From cede8a08266f2a4bb42dbe04f568a79b43d81055 Mon Sep 17 00:00:00 2001 From: Daniele Gobbetti Date: Fri, 28 Apr 2017 18:03:19 +0200 Subject: [PATCH] Refactor notification management - centralize the logic for skipping over unwanted notifications - use *Compat methods wherever possible - use unique and persistent ID (update notifications) - switch to using BigText style by default (since we can now update existing notifications) - for Pebble: delete and reinsert notification as updating is not possible --- .../externalevents/NotificationListener.java | 247 +++++++++--------- .../service/devices/pebble/PebbleSupport.java | 1 + .../freeyourgadget/gadgetbridge/util/GB.java | 4 +- 3 files changed, 124 insertions(+), 128 deletions(-) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java index 0b32ba7e..0d34375b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -18,7 +18,6 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.externalevents; -import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.Notification; import android.app.NotificationManager; @@ -30,17 +29,19 @@ import android.content.IntentFilter; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.media.MediaMetadata; -import android.media.session.MediaController; -import android.media.session.MediaSession; import android.media.session.PlaybackState; -import android.os.Build; import android.os.Bundle; import android.os.PowerManager; +import android.os.RemoteException; import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; -import android.support.v4.app.NotificationCompat; import android.support.v4.app.RemoteInput; import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.media.MediaMetadataCompat; +import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.MediaSessionCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import android.support.v7.app.NotificationCompat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -76,7 +77,7 @@ public class NotificationListener extends NotificationListenerService { private LimitedQueue mActionLookup = new LimitedQueue(16); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { - @SuppressLint("NewApi") + @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); @@ -89,7 +90,7 @@ public class NotificationListener extends NotificationListenerService { StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); int handle = intent.getIntExtra("handle", -1); for (StatusBarNotification sbn : sbns) { - if ((int) sbn.getPostTime() == handle) { + if ((sbn.getPackageName().hashCode() * 31 + sbn.getId()) == handle) { if (action.equals(ACTION_OPEN)) { try { PendingIntent pi = sbn.getNotification().contentIntent; @@ -112,7 +113,7 @@ public class NotificationListener extends NotificationListenerService { StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); int handle = intent.getIntExtra("handle", -1); for (StatusBarNotification sbn : sbns) { - if ((int) sbn.getPostTime() == handle) { + if ((sbn.getPackageName().hashCode() * 31 + sbn.getId()) == handle) { if (GBApplication.isRunningLollipopOrLater()) { String key = sbn.getKey(); NotificationListener.this.cancelNotification(key); @@ -177,16 +178,8 @@ public class NotificationListener extends NotificationListenerService { @Override public void onNotificationPosted(StatusBarNotification sbn) { - /* - * return early if DeviceCommunicationService is not running, - * else the service would get started every time we get a notification. - * unfortunately we cannot enable/disable NotificationListener at runtime like we do with - * broadcast receivers because it seems to invalidate the permissions that are - * necessary for NotificationListenerService - */ - if (!isServiceRunning()) { + if (shouldIgnore(sbn)) return; - } switch (GBApplication.getGrantedInterruptionFilter()) { case NotificationManager.INTERRUPTION_FILTER_ALL: @@ -201,53 +194,8 @@ public class NotificationListener extends NotificationListenerService { String source = sbn.getPackageName(); Notification notification = sbn.getNotification(); - - if (handleMediaSessionNotification(notification)) - return; - - Prefs prefs = GBApplication.getPrefs(); - if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) { - PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE); - if (powermanager.isScreenOn()) { -// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this"); - return; - } - } - - if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { -// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags); - return; - } - - /* do not display messages from "android" - * This includes keyboard selection message, usb connection messages, etc - * Hope it does not filter out too much, we will see... - */ - - if (source.equals("android") || - source.equals("com.android.systemui") || - source.equals("com.android.dialer") || - source.equals("com.cyanogenmod.eleven")) { - LOG.info("Not forwarding notification, is a system event"); - return; - } - - if (source.equals("com.moez.QKSMS") || - source.equals("com.android.mms") || - source.equals("com.sonyericsson.conversations") || - source.equals("com.android.messaging") || - source.equals("org.smssecure.smssecure")) { - if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) { - return; - } - } - - if (GBApplication.isBlacklisted(source)) { - LOG.info("Not forwarding notification, application is blacklisted"); - return; - } - NotificationSpec notificationSpec = new NotificationSpec(); + notificationSpec.id = source.hashCode() * 31 + sbn.getId(); // determinate Source App Name ("Label") PackageManager pm = getPackageManager(); @@ -261,26 +209,17 @@ public class NotificationListener extends NotificationListenerService { notificationSpec.sourceName = (String) pm.getApplicationLabel(ai); } - boolean preferBigText = false; + boolean preferBigText = true; //changed to true since now we update the former ID notificationSpec.type = AppNotificationType.getInstance().get(source); - if (source.startsWith("com.fsck.k9")) { - // we dont want group summaries at all for k9 - if ((notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { - return; - } - preferBigText = true; - } - if (notificationSpec.type == null) { notificationSpec.type = NotificationType.UNKNOWN; } - LOG.info("Processing notification from source " + source + " with flags: " + notification.flags); + LOG.info("Processing notification " + notificationSpec.id + " from source " + source + " with flags: " + notification.flags); dissectNotificationTo(notification, notificationSpec, preferBigText); - notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better // ignore Gadgetbridge's very own notifications, except for those from the debug screen if (getApplicationContext().getPackageName().equals(source)) { @@ -292,11 +231,6 @@ public class NotificationListener extends NotificationListenerService { NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification); List actions = wearableExtender.getActions(); - if (actions.isEmpty() && (notification.flags & Notification.FLAG_GROUP_SUMMARY) == Notification.FLAG_GROUP_SUMMARY) { //this could cause #395 to come back - LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags); - return; - } - for (NotificationCompat.Action act : actions) { if (act != null && act.getRemoteInputs() != null) { LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag()); @@ -306,11 +240,17 @@ public class NotificationListener extends NotificationListenerService { } } + if ((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) == 0 && NotificationCompat.isGroupSummary(notification)) { //this could cause #395 to come back + LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags); + return; + } + GBApplication.deviceService().onNotification(notificationSpec); } private void dissectNotificationTo(Notification notification, NotificationSpec notificationSpec, boolean preferBigText) { - Bundle extras = notification.extras; + + Bundle extras = NotificationCompat.getExtras(notification); //dumpExtras(extras); @@ -321,9 +261,9 @@ public class NotificationListener extends NotificationListenerService { CharSequence contentCS = null; if (preferBigText && extras.containsKey(Notification.EXTRA_BIG_TEXT)) { - contentCS = extras.getCharSequence(Notification.EXTRA_BIG_TEXT); + contentCS = extras.getCharSequence(NotificationCompat.EXTRA_BIG_TEXT); } else if (extras.containsKey(Notification.EXTRA_TEXT)) { - contentCS = extras.getCharSequence(Notification.EXTRA_TEXT); + contentCS = extras.getCharSequence(NotificationCompat.EXTRA_TEXT); } if (contentCS != null) { notificationSpec.body = contentCS.toString(); @@ -344,31 +284,18 @@ public class NotificationListener extends NotificationListenerService { /** * Try to handle media session notifications that tell info about the current play state. * - * @param notification The notification to handle. + * @param mediaSession The mediasession to handle. * @return true if notification was handled, false otherwise */ - public boolean handleMediaSessionNotification(Notification notification) { - - // this code requires Android 5.0 or newer - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { - return false; - } - + public boolean handleMediaSessionNotification(MediaSessionCompat.Token mediaSession) { MusicSpec musicSpec = new MusicSpec(); MusicStateSpec stateSpec = new MusicStateSpec(); - Bundle extras = notification.extras; - if (extras == null) - return false; - - if (extras.get(Notification.EXTRA_MEDIA_SESSION) == null) - return false; - - MediaController c; + MediaControllerCompat c; try { - c = new MediaController(getApplicationContext(), (MediaSession.Token) extras.get(Notification.EXTRA_MEDIA_SESSION)); + c = new MediaControllerCompat(getApplicationContext(), mediaSession); - PlaybackState s = c.getPlaybackState(); + PlaybackStateCompat s = c.getPlaybackState(); stateSpec.position = (int) (s.getPosition() / 1000); stateSpec.playRate = Math.round(100 * s.getPlaybackSpeed()); stateSpec.repeat = 1; @@ -388,57 +315,41 @@ public class NotificationListener extends NotificationListenerService { break; } - MediaMetadata d = c.getMetadata(); + MediaMetadataCompat d = c.getMetadata(); if (d == null) return false; if (d.containsKey(MediaMetadata.METADATA_KEY_ARTIST)) - musicSpec.artist = d.getString(MediaMetadata.METADATA_KEY_ARTIST); + musicSpec.artist = d.getString(MediaMetadataCompat.METADATA_KEY_ARTIST); if (d.containsKey(MediaMetadata.METADATA_KEY_ALBUM)) - musicSpec.album = d.getString(MediaMetadata.METADATA_KEY_ALBUM); + musicSpec.album = d.getString(MediaMetadataCompat.METADATA_KEY_ALBUM); if (d.containsKey(MediaMetadata.METADATA_KEY_TITLE)) - musicSpec.track = d.getString(MediaMetadata.METADATA_KEY_TITLE); + musicSpec.track = d.getString(MediaMetadataCompat.METADATA_KEY_TITLE); if (d.containsKey(MediaMetadata.METADATA_KEY_DURATION)) - musicSpec.duration = (int) d.getLong(MediaMetadata.METADATA_KEY_DURATION) / 1000; + musicSpec.duration = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_DURATION) / 1000; if (d.containsKey(MediaMetadata.METADATA_KEY_NUM_TRACKS)) - musicSpec.trackCount = (int) d.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS); + musicSpec.trackCount = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_NUM_TRACKS); if (d.containsKey(MediaMetadata.METADATA_KEY_TRACK_NUMBER)) - musicSpec.trackNr = (int) d.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER); + musicSpec.trackNr = (int) d.getLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER); // finally, tell the device about it GBApplication.deviceService().onSetMusicInfo(musicSpec); GBApplication.deviceService().onSetMusicState(stateSpec); return true; - } catch (NullPointerException e) { + } catch (NullPointerException | RemoteException e) { return false; } } @Override public void onNotificationRemoved(StatusBarNotification sbn) { - //FIXME: deduplicate code - if (!isServiceRunning() || sbn == null) { + if (shouldIgnore(sbn)) return; - } - - String source = sbn.getPackageName(); - Notification notification = sbn.getNotification(); - if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { - return; - } - - if (source.equals("android") || - source.equals("com.android.systemui") || - source.equals("com.android.dialer") || - source.equals("com.cyanogenmod.eleven")) { - return; - } Prefs prefs = GBApplication.getPrefs(); if (prefs.getBoolean("autoremove_notifications", false)) { LOG.info("notification removed, will ask device to delete it"); - - GBApplication.deviceService().onDeleteNotification((int) sbn.getPostTime()); //FIMXE: a truly unique id would be better + GBApplication.deviceService().onDeleteNotification(sbn.getPackageName().hashCode() * 31 + sbn.getId()); } } @@ -451,4 +362,88 @@ public class NotificationListener extends NotificationListenerService { LOG.debug(String.format("Notification extra: %s %s (%s)", key, value.toString(), value.getClass().getName())); } } + + private boolean shouldIgnore(StatusBarNotification sbn) { + /* + * return early if DeviceCommunicationService is not running, + * else the service would get started every time we get a notification. + * unfortunately we cannot enable/disable NotificationListener at runtime like we do with + * broadcast receivers because it seems to invalidate the permissions that are + * necessary for NotificationListenerService + */ + if (!isServiceRunning() || sbn == null) { + return true; + } + + if (shouldIgnoreSource(sbn.getPackageName())) + return true; + + if (shouldIgnoreNotification(sbn.getNotification())) + return true; + + return false; + } + + private boolean shouldIgnoreSource(String source) { + Prefs prefs = GBApplication.getPrefs(); + + /* do not display messages from "android" + * This includes keyboard selection message, usb connection messages, etc + * Hope it does not filter out too much, we will see... + */ + + if (source.equals("android") || + source.equals("com.android.systemui") || + source.equals("com.android.dialer") || + source.equals("com.cyanogenmod.eleven")) { + LOG.info("Ignoring notification, is a system event"); + return true; + } + + if (source.equals("com.moez.QKSMS") || + source.equals("com.android.mms") || + source.equals("com.sonyericsson.conversations") || + source.equals("com.android.messaging") || + source.equals("org.smssecure.smssecure")) { + if (!"never".equals(prefs.getString("notification_mode_sms", "when_screen_off"))) { + return true; + } + } + + if (GBApplication.isBlacklisted(source)) { + LOG.info("Ignoring notification, application is blacklisted"); + return true; + } + + return false; + } + + private boolean shouldIgnoreNotification(Notification notification) { + + MediaSessionCompat.Token mediaSession = NotificationCompat.getMediaSession(notification); + //try to handle media session notifications + if (mediaSession != null && handleMediaSessionNotification(mediaSession)) + return true; + + //ignore notifications marked as LocalOnly https://developer.android.com/reference/android/app/Notification.html#FLAG_LOCAL_ONLY + if (NotificationCompat.getLocalOnly(notification)) + return true; + + Prefs prefs = GBApplication.getPrefs(); + if (!prefs.getBoolean("notifications_generic_whenscreenon", false)) { + PowerManager powermanager = (PowerManager) getSystemService(POWER_SERVICE); + if (powermanager.isScreenOn()) { +// LOG.info("Not forwarding notification, screen seems to be on and settings do not allow this"); + return true; + } + } + + if ((notification.flags & Notification.FLAG_ONGOING_EVENT) == Notification.FLAG_ONGOING_EVENT) { +// LOG.info("Not forwarding notification, FLAG_ONGOING_EVENT is set. Notification flags: " + notification.flags); + return true; + } + + return false; + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java index b3b21b30..8ed534d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleSupport.java @@ -148,6 +148,7 @@ public class PebbleSupport extends AbstractSerialDeviceSupport { } } if (reconnect()) { + super.onDeleteNotification(notificationSpec.id); //update notification hack super.onNotification(notificationSpec); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java index 2ae943e5..fce8a881 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -76,7 +76,7 @@ public class GB { .setContentIntent(pendingIntent) .setOngoing(true); if (GBApplication.isRunningLollipopOrLater()) { - builder.setVisibility(Notification.VISIBILITY_PUBLIC); + builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC); } if (GBApplication.minimizeNotification()) { builder.setPriority(Notification.PRIORITY_MIN); @@ -268,7 +268,7 @@ public class GB { notificationIntent, 0); NotificationCompat.Builder nb = new NotificationCompat.Builder(context) - .setVisibility(Notification.VISIBILITY_PUBLIC) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) .setContentTitle(context.getString(R.string.app_name)) .setContentText(text) .setContentIntent(pendingIntent)