From d6190e6e59b0b0ab0e3724cdc770606e921d05f5 Mon Sep 17 00:00:00 2001 From: abettenburg Date: Mon, 3 Dec 2018 09:45:43 +0100 Subject: [PATCH] Apps Notification can now be configured to filter notification content based on black- and whitelists Go to notification blacklist, allow an app if blacklisted, than configure it's behavior with the menu icon on the right hand side. Should be pretty much self explanatory. Database Scheme raised to 20 --- .../gadgetbridge/daogen/GBDaoGenerator.java | 30 ++- app/build.gradle | 1 + app/src/main/AndroidManifest.xml | 9 +- .../NotificationFilterActivity.java | 206 ++++++++++++++++++ .../adapter/AppBlacklistAdapter.java | 18 +- .../externalevents/NotificationListener.java | 139 ++++++++++-- .../layout/activity_notification_filter.xml | 65 ++++++ app/src/main/res/values/arrays.xml | 11 + app/src/main/res/values/strings.xml | 15 +- .../NotificationListenerTest.java | 109 +++++++++ 10 files changed, 579 insertions(+), 24 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationFilterActivity.java create mode 100644 app/src/main/res/layout/activity_notification_filter.xml create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListenerTest.java diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 354c6e275..7a9907c30 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -45,7 +45,7 @@ public class GBDaoGenerator { public static void main(String[] args) throws Exception { - Schema schema = new Schema(19, MAIN_PACKAGE + ".entities"); + Schema schema = new Schema(20, MAIN_PACKAGE + ".entities"); Entity userAttributes = addUserAttributes(schema); Entity user = addUserInfo(schema, userAttributes); @@ -75,6 +75,10 @@ public class GBDaoGenerator { addCalendarSyncState(schema, device); addAlarms(schema, user, device); + Entity notificationFilter = addNotificationFilters(schema); + + addNotificationFilterEntry(schema, notificationFilter); + addBipActivitySummary(schema, user, device); new DaoGenerator().generateAll(schema, "app/src/main/java"); @@ -363,6 +367,30 @@ public class GBDaoGenerator { alarm.addToOne(device, deviceId); } + private static void addNotificationFilterEntry(Schema schema, Entity notificationFilterEntity) { + Entity notificatonFilterEntry = addEntity(schema, "NotificationFilterEntry"); + notificatonFilterEntry.addIdProperty().autoincrement(); + Property notificationFilterId = notificatonFilterEntry.addLongProperty("notificationFilterId").notNull().getProperty(); + notificatonFilterEntry.addStringProperty("notificationFilterContent").notNull().getProperty(); + notificatonFilterEntry.addToOne(notificationFilterEntity, notificationFilterId); + } + + private static Entity addNotificationFilters(Schema schema) { + Entity notificatonFilter = addEntity(schema, "NotificationFilter"); + Property appIdentifier = notificatonFilter.addStringProperty("appIdentifier").notNull().getProperty(); + + notificatonFilter.addIdProperty().autoincrement(); + + Index indexUnique = new Index(); + indexUnique.addProperty(appIdentifier); + indexUnique.makeUnique(); + notificatonFilter.addIndex(indexUnique); + + Property notificationFilterMode = notificatonFilter.addIntProperty("notificationFilterMode").notNull().getProperty(); + Property notificationFilterSubMode = notificatonFilter.addIntProperty("notificationFilterSubMode").notNull().getProperty(); + return notificatonFilter; + } + private static void addBipActivitySummary(Schema schema, Entity user, Entity device) { Entity summary = addEntity(schema, "BaseActivitySummary"); summary.implementsInterface(ACTIVITY_SUMMARY); diff --git a/app/build.gradle b/app/build.gradle index b637e8ea8..8cf87c4ff 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -60,6 +60,7 @@ pmd { dependencies { // testImplementation "ch.qos.logback:logback-classic:1.1.3" // testImplementation "ch.qos.logback:logback-core:1.1.3" + implementation 'com.android.support.constraint:constraint-layout:1.1.3' testImplementation "junit:junit:4.12" testImplementation "org.mockito:mockito-core:1.10.19" testImplementation "org.robolectric:robolectric:3.6.1" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index c9ecb863a..79cca0024 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -405,12 +405,17 @@ + android:parentActivityName=".activities.ConfigureAlarms" + android:screenOrientation="portrait" /> + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationFilterActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationFilterActivity.java new file mode 100644 index 000000000..1e8ff7900 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/NotificationFilterActivity.java @@ -0,0 +1,206 @@ +package nodomain.freeyourgadget.gadgetbridge.activities; + +import android.os.Bundle; +import android.view.View; +import android.widget.*; +import de.greenrobot.dao.query.Query; +import nodomain.freeyourgadget.gadgetbridge.BuildConfig; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.adapter.AppBlacklistAdapter; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilter; +import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterDao; +import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntry; +import nodomain.freeyourgadget.gadgetbridge.entities.NotificationFilterEntryDao; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.List; + +public class NotificationFilterActivity extends AbstractGBActivity { + + private static final String TAG = NotificationFilterActivity.class.getName(); + + public static final int NOTIFICATION_FILTER_MODE_NONE = 0; + public static final int NOTIFICATION_FILTER_MODE_WHITELIST = 1; + public static final int NOTIFICATION_FILTER_MODE_BLACKLIST = 2; + public static final int NOTIFICATION_FILTER_SUBMODE_ANY = 0; + public static final int NOTIFICATION_FILTER_SUBMODE_ALL = 1; + + private Button mButtonSave; + private Spinner mSpinnerFilterMode; + private Spinner mSpinnerFilterSubMode; + private NotificationFilter mNotificationFilter; + private EditText mEditTextWords; + private DBHandler db = null; + private List mWordsList = new ArrayList<>(); + private List mFilterEntryIds = new ArrayList<>(); + + private static final Logger LOG = LoggerFactory.getLogger(NotificationFilterActivity.class); + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_notification_filter); + + String packageName = getIntent().getStringExtra(AppBlacklistAdapter.STRING_EXTRA_PACKAGE_NAME); + + if (StringUtils.isBlank(packageName)) { + this.finish(); + } + + try { + db = GBApplication.acquireDB(); + } catch (GBException e) { + LOG.error("Could not acquire DB.", e); + this.finish(); + } + + NotificationFilterDao notificationFilterDao = db.getDaoSession().getNotificationFilterDao(); + NotificationFilterEntryDao notificationFilterEntryDao = db.getDaoSession().getNotificationFilterEntryDao(); + + Query query = notificationFilterDao.queryBuilder().where(NotificationFilterDao.Properties.AppIdentifier.eq(packageName)).build(); + mNotificationFilter = query.unique(); + + if (mNotificationFilter == null) { + mNotificationFilter = new NotificationFilter(); + mNotificationFilter.setAppIdentifier(packageName); + LOG.debug("New Notification Filter"); + } else { + LOG.debug("Loaded existing notification filter"); + Query queryEntries = notificationFilterEntryDao.queryBuilder().where(NotificationFilterEntryDao.Properties.NotificationFilterId.eq(mNotificationFilter.getId())).build(); + List filterEntries = queryEntries.list(); + if (!filterEntries.isEmpty()) { + for (NotificationFilterEntry temp : filterEntries) { + mWordsList.add(temp.getNotificationFilterContent()); + mFilterEntryIds.add(temp.getId()); + LOG.debug("Loaded filter word: " + temp.getNotificationFilterContent()); + } + } + } + + setupView(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + + if (db != null) { + GBApplication.releaseDB(); + } + } + + private void setupView() { + + mSpinnerFilterMode = findViewById(R.id.spinnerFilterMode); + mSpinnerFilterMode.setSelection(mNotificationFilter.getNotificationFilterMode()); + + mSpinnerFilterSubMode = findViewById(R.id.spinnerSubMode); + mSpinnerFilterMode.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView adapterView, View view, int pos, long id) { + switch (pos) { + case NOTIFICATION_FILTER_MODE_NONE: + mEditTextWords.setEnabled(false); + mSpinnerFilterSubMode.setEnabled(false); + break; + case NOTIFICATION_FILTER_MODE_BLACKLIST: + case NOTIFICATION_FILTER_MODE_WHITELIST: + mEditTextWords.setEnabled(true); + mSpinnerFilterSubMode.setEnabled(true); + break; + + } + } + + @Override + public void onNothingSelected(AdapterView adapterView) { + + } + }); + + mSpinnerFilterSubMode.setSelection(mNotificationFilter.getNotificationFilterSubMode()); + + mEditTextWords = findViewById(R.id.editTextWords); + + if (!mWordsList.isEmpty()) { + StringBuilder builder = new StringBuilder(); + for (String temp : mWordsList) { + builder.append(temp); + builder.append("\n"); + } + mEditTextWords.setText(builder.toString()); + } + + mEditTextWords.setEnabled(mSpinnerFilterMode.getSelectedItemPosition() == NOTIFICATION_FILTER_MODE_NONE); + + mButtonSave = findViewById(R.id.buttonSaveFilter); + mButtonSave.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + + // TODO: check for modifications, only save if something changed + + String words = mEditTextWords.getText().toString(); + + if (StringUtils.isBlank(words) && mSpinnerFilterMode.getSelectedItemPosition() != NOTIFICATION_FILTER_MODE_NONE) { + Toast.makeText(NotificationFilterActivity.this, R.string.toast_notification_filter_words_empty_hint, Toast.LENGTH_SHORT).show(); + return; + } + + try { + db = GBApplication.acquireDB(); + NotificationFilterDao notificationFilterDao = db.getDaoSession().getNotificationFilterDao(); + NotificationFilterEntryDao notificationFilterEntryDao = db.getDaoSession().getNotificationFilterEntryDao(); + + debugOutput(notificationFilterDao); + + mNotificationFilter.setNotificationFilterMode(mSpinnerFilterMode.getSelectedItemPosition()); + mNotificationFilter.setNotificationFilterSubMode(mSpinnerFilterSubMode.getSelectedItemPosition()); + + notificationFilterEntryDao.deleteByKeyInTx(mFilterEntryIds); + + Long filterId = notificationFilterDao.insertOrReplace(mNotificationFilter); + + // only save words if filter mode != none + if (mNotificationFilter.getNotificationFilterMode() != NOTIFICATION_FILTER_MODE_NONE) { + String[] wordsSplitted = words.split("\n"); + for (String temp : wordsSplitted) { + temp = temp.trim(); + NotificationFilterEntry notificationFilterEntry = new NotificationFilterEntry(); + notificationFilterEntry.setNotificationFilterContent(temp); + notificationFilterEntry.setNotificationFilterId(filterId); + notificationFilterEntryDao.insert(notificationFilterEntry); + } + } + + Toast.makeText(NotificationFilterActivity.this, R.string.toast_notification_filter_saved_successfully, Toast.LENGTH_SHORT).show(); + NotificationFilterActivity.this.finish(); + + } catch (GBException e) { + LOG.error("Could not acquire DB.", e); + Toast.makeText(NotificationFilterActivity.this, "Database Error: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + } + } + }); + } + + private void debugOutput(NotificationFilterDao notificationFilterDao) { + if (BuildConfig.DEBUG) { + + List filters = notificationFilterDao.loadAll(); + + LOG.info(TAG, "Saved filters"); + + for (NotificationFilter temp : filters) { + LOG.info(TAG, "Filter: " + temp.getId() + " " + temp.getAppIdentifier()); + } + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AppBlacklistAdapter.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AppBlacklistAdapter.java index 7888db2cf..9c14e606d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AppBlacklistAdapter.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/adapter/AppBlacklistAdapter.java @@ -17,6 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.adapter; import android.content.Context; +import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.support.v7.widget.RecyclerView; @@ -40,11 +41,14 @@ import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.NotificationFilterActivity; import static nodomain.freeyourgadget.gadgetbridge.GBApplication.packageNameToPebbleMsgSender; public class AppBlacklistAdapter extends RecyclerView.Adapter implements Filterable { + public static final String STRING_EXTRA_PACKAGE_NAME = "packageName"; + private List applicationInfoList; private final int mLayoutId; private final Context mContext; @@ -92,7 +96,7 @@ public class AppBlacklistAdapter extends RecyclerView.Adapter wordsList = new ArrayList<>(); + + NotificationFilterDao notificationFilterDao = db.getDaoSession().getNotificationFilterDao(); + NotificationFilterEntryDao notificationFilterEntryDao = db.getDaoSession().getNotificationFilterEntryDao(); + + Query query = notificationFilterDao.queryBuilder().where(NotificationFilterDao.Properties.AppIdentifier.eq(packageName)).build(); + NotificationFilter notificationFilter = query.unique(); + + if (notificationFilter == null) { + LOG.debug("No Notification Filter found"); + return true; + } + + LOG.debug("Loaded notification filter for '{}'", packageName); + Query queryEntries = notificationFilterEntryDao.queryBuilder().where(NotificationFilterEntryDao.Properties.NotificationFilterId.eq(notificationFilter.getId())).build(); + + List filterEntries = queryEntries.list(); + + if (BuildConfig.DEBUG) { + LOG.info("Database lookup took '{}' ms", System.currentTimeMillis() - start); + } + + if (!filterEntries.isEmpty()) { + for (NotificationFilterEntry temp : filterEntries) { + wordsList.add(temp.getNotificationFilterContent()); + LOG.debug("Loaded filter word: " + temp.getNotificationFilterContent()); + } + } + + return shouldContinueAfterFilter(body, wordsList, notificationFilter); + } + + boolean shouldContinueAfterFilter(@NonNull String body, @NonNull List wordsList, @NonNull NotificationFilter notificationFilter) { + + LOG.debug("Mode: '{}' Submode: '{}' WordsList: '{}'", notificationFilter.getNotificationFilterMode(), notificationFilter.getNotificationFilterSubMode(), wordsList); + + boolean allMode = notificationFilter.getNotificationFilterSubMode() == NOTIFICATION_FILTER_SUBMODE_ALL; + + switch (notificationFilter.getNotificationFilterMode()) { + case NOTIFICATION_FILTER_MODE_BLACKLIST: + if (allMode) { + for (String word : wordsList) { + if (!body.contains(word)) { + LOG.info("Not every word was found, blacklist has no effect, processing continues."); + return true; + } + } + LOG.info("Every word was found, blacklist has effect, processing stops."); + return false; + } else { + boolean notContainsAny = !StringUtils.containsAny(body, wordsList.toArray(new CharSequence[0])); + if (notContainsAny) { + LOG.info("Not matching word was found, blacklist has no effect, processing continues."); + } else { + LOG.info("At least one matching word was found, blacklist has effect, processing stops."); + } + return notContainsAny; + } + + case NOTIFICATION_FILTER_MODE_WHITELIST: + if (allMode) { + for (String word : wordsList) { + if (!body.contains(word)) { + LOG.info("Not every word was found, whitelist has no effect, processing stops."); + return false; + } + } + LOG.info("Every word was found, whitelist has effect, processing continues."); + return true; + } else { + boolean containsAny = StringUtils.containsAny(body, wordsList.toArray(new CharSequence[0])); + if (containsAny) { + LOG.info("At least one matching word was found, whitelist has effect, processing continues."); + } else { + LOG.info("No matching word was found, whitelist has no effect, processing stops."); + } + return containsAny; + } + + default: + return true; + } + } + // Strip Unicode control sequences: some apps like Telegram add a lot of them for unknown reasons private String sanitizeUnicode(String orig) { return orig.replaceAll("\\p{C}", ""); } - private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) { + private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, + boolean preferBigText) { Bundle extras = NotificationCompat.getExtras(notification); diff --git a/app/src/main/res/layout/activity_notification_filter.xml b/app/src/main/res/layout/activity_notification_filter.xml new file mode 100644 index 000000000..9f0d01513 --- /dev/null +++ b/app/src/main/res/layout/activity_notification_filter.xml @@ -0,0 +1,65 @@ + + + + + + + + +