From e3533a2b18e248121f184c664a1fe0025cf327eb Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Fri, 25 Sep 2015 00:53:40 +0200 Subject: [PATCH] Pebble: Allow muting (blacklisting) Apps from within generic notifications on the watch Closes #113 --- CHANGELOG.md | 1 + .../gadgetbridge/GBApplication.java | 40 ++++++++- .../activities/AppBlacklistActivity.java | 41 +-------- .../GBDeviceEventNotificationControl.java | 3 +- .../externalevents/NotificationListener.java | 89 ++++++++++++------- .../service/AbstractDeviceSupport.java | 3 + .../devices/pebble/PebbleProtocol.java | 66 ++++++++++---- .../freeyourgadget/gadgetbridge/util/GB.java | 2 +- 8 files changed, 158 insertions(+), 87 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 372b19567..a70bdfb4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ###Changelog ####Next Version +* Pebble: Allow muting (blacklisting) Apps from within generic notifications on the watch * Pebble: Detect all known Pebble Versions including new "chalk" platform (Pebble Time Round) * Option to ignore phone calls (useful for Pebble Dialer) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java index 91cc0a97c..e3cb2d6f1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/GBApplication.java @@ -13,6 +13,7 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.io.IOException; +import java.util.HashSet; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; @@ -31,6 +32,7 @@ public class GBApplication extends Application { private static ActivityDatabaseHandler mActivityDatabaseHandler; private static final Lock dbLock = new ReentrantLock(); private static DeviceService deviceService; + private static SharedPreferences sharedPrefs; public GBApplication() { context = this; @@ -46,6 +48,8 @@ public class GBApplication extends Application { public void onCreate() { super.onCreate(); + sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context); + // don't do anything here before we set up logging, otherwise // slf4j may be implicitly initialized before we properly configured it. setupLogging(); @@ -57,14 +61,14 @@ public class GBApplication extends Application { GB.environment = GBEnvironment.createDeviceEnvironment(); mActivityDatabaseHandler = new ActivityDatabaseHandler(context); + loadBlackList(); // for testing DB stuff // SQLiteDatabase db = mActivityDatabaseHandler.getWritableDatabase(); // db.close(); } public static boolean isFileLoggingEnabled() { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(GBApplication.getContext()); - return prefs.getBoolean("log_to_file", false); + return sharedPrefs.getBoolean("log_to_file", false); } private void setupLogging() { @@ -130,4 +134,36 @@ public class GBApplication extends Application { public static boolean isRunningLollipopOrLater() { return VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP; } + + public static HashSet blacklist = null; + + public static void loadBlackList() { + blacklist = (HashSet) sharedPrefs.getStringSet("package_blacklist", null); + if (blacklist == null) { + blacklist = new HashSet<>(); + } + } + + public static void saveBlackList() { + SharedPreferences.Editor editor = sharedPrefs.edit(); + if (blacklist.isEmpty()) { + editor.putStringSet("package_blacklist", null); + } else { + editor.putStringSet("package_blacklist", blacklist); + } + editor.apply(); + } + + public static void addToBlacklist(String packageName) { + if (!blacklist.contains(packageName)) { + blacklist.add(packageName); + saveBlackList(); + } + } + + public static synchronized void removeFromBlacklist(String packageName) { + blacklist.remove(packageName); + saveBlackList(); + } + } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java index aec7cd9a6..d95fd7677 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/AppBlacklistActivity.java @@ -26,9 +26,9 @@ import android.widget.TextView; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashSet; import java.util.List; +import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; @@ -47,37 +47,6 @@ public class AppBlacklistActivity extends Activity { private SharedPreferences sharedPrefs; - private HashSet blacklist = null; - - private void loadBlackList() { - blacklist = (HashSet) sharedPrefs.getStringSet("package_blacklist", null); - if (blacklist == null) { - blacklist = new HashSet<>(); - } - } - - private void saveBlackList() { - SharedPreferences.Editor editor = sharedPrefs.edit(); - if (blacklist.isEmpty()) { - editor.putStringSet("package_blacklist", null); - } else { - editor.putStringSet("package_blacklist", blacklist); - } - editor.apply(); - } - - private synchronized void addToBlacklist(String packageName) { - if (!blacklist.contains(packageName)) { - blacklist.add(packageName); - saveBlackList(); - } - } - - private synchronized void removeFromBlacklist(String packageName) { - blacklist.remove(packageName); - saveBlackList(); - } - @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -87,8 +56,6 @@ public class AppBlacklistActivity extends Activity { final PackageManager pm = getPackageManager(); sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); - loadBlackList(); - final List packageList = pm.getInstalledApplications(PackageManager.GET_META_DATA); ListView appListView = (ListView) findViewById(R.id.appListView); @@ -110,7 +77,7 @@ public class AppBlacklistActivity extends Activity { deviceAppNameLabel.setText(appInfo.loadLabel(pm)); deviceImageView.setImageDrawable(appInfo.loadIcon(pm)); - if (blacklist.contains(appInfo.packageName)) { + if (GBApplication.blacklist.contains(appInfo.packageName)) { checkbox.setChecked(true); } @@ -126,9 +93,9 @@ public class AppBlacklistActivity extends Activity { CheckBox checkBox = ((CheckBox) v.findViewById(R.id.item_checkbox)); checkBox.toggle(); if (checkBox.isChecked()) { - addToBlacklist(packageName); + GBApplication.addToBlacklist(packageName); } else { - removeFromBlacklist(packageName); + GBApplication.removeFromBlacklist(packageName); } } }); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java index 010ff0af3..567782cda 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventNotificationControl.java @@ -9,6 +9,7 @@ public class GBDeviceEventNotificationControl extends GBDeviceEvent { UNKNOWN, DISMISS, DISMISS_ALL, - OPEN + OPEN, + MUTE } } 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 c2da97c7a..11a88c191 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -9,6 +9,8 @@ import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Bundle; import android.os.PowerManager; import android.preference.PreferenceManager; @@ -19,8 +21,6 @@ import android.support.v4.content.LocalBroadcastManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.util.HashSet; - import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; @@ -36,45 +36,60 @@ public class NotificationListener extends NotificationListenerService { = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.dismiss_all"; public static final String ACTION_OPEN = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.open"; + public static final String ACTION_MUTE + = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.mute"; private BroadcastReceiver mReceiver = new BroadcastReceiver() { @SuppressLint("NewApi") @Override public void onReceive(Context context, Intent intent) { String action = intent.getAction(); - if (action.equals(ACTION_OPEN)) { - StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); - int handle = intent.getIntExtra("handle", -1); - for (StatusBarNotification sbn : sbns) { - if ((int) sbn.getPostTime() == handle) { - try { - PendingIntent pi = sbn.getNotification().contentIntent; - if (pi != null) { - pi.send(); + switch (action) { + case ACTION_MUTE: + case ACTION_OPEN: { + StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); + int handle = intent.getIntExtra("handle", -1); + for (StatusBarNotification sbn : sbns) { + if ((int) sbn.getPostTime() == handle) { + if (action.equals(ACTION_OPEN)) { + try { + PendingIntent pi = sbn.getNotification().contentIntent; + if (pi != null) { + pi.send(); + } + } catch (PendingIntent.CanceledException e) { + e.printStackTrace(); + } + } else { + // ACTION_MUTE + LOG.info("going to mute " + sbn.getPackageName()); + GBApplication.addToBlacklist(sbn.getPackageName()); } - } catch (PendingIntent.CanceledException e) { - e.printStackTrace(); } } + break; } - } else if (action.equals(ACTION_DISMISS)) { - StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); - int handle = intent.getIntExtra("handle", -1); - for (StatusBarNotification sbn : sbns) { - if ((int) sbn.getPostTime() == handle) { - if (GBApplication.isRunningLollipopOrLater()) { - String key = sbn.getKey(); - NotificationListener.this.cancelNotification(key); - } else { - int id = sbn.getId(); - String pkg = sbn.getPackageName(); - String tag = sbn.getTag(); - NotificationListener.this.cancelNotification(pkg, tag, id); + case ACTION_DISMISS: { + StatusBarNotification[] sbns = NotificationListener.this.getActiveNotifications(); + int handle = intent.getIntExtra("handle", -1); + for (StatusBarNotification sbn : sbns) { + if ((int) sbn.getPostTime() == handle) { + if (GBApplication.isRunningLollipopOrLater()) { + String key = sbn.getKey(); + NotificationListener.this.cancelNotification(key); + } else { + int id = sbn.getId(); + String pkg = sbn.getPackageName(); + String tag = sbn.getTag(); + NotificationListener.this.cancelNotification(pkg, tag, id); + } } } + break; } - } else if (action.equals(ACTION_DISMISS_ALL)) { - NotificationListener.this.cancelAllNotifications(); + case ACTION_DISMISS_ALL: + NotificationListener.this.cancelAllNotifications(); + break; } } @@ -87,6 +102,7 @@ public class NotificationListener extends NotificationListenerService { filterLocal.addAction(ACTION_OPEN); filterLocal.addAction(ACTION_DISMISS); filterLocal.addAction(ACTION_DISMISS_ALL); + filterLocal.addAction(ACTION_MUTE); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); } @@ -154,13 +170,24 @@ public class NotificationListener extends NotificationListenerService { } } - HashSet blacklist = (HashSet) sharedPrefs.getStringSet("package_blacklist", null); - if (blacklist != null && blacklist.contains(source)) { + if (GBApplication.blacklist != null && GBApplication.blacklist.contains(source)) { return; } - // Set application icons for generic notifications NotificationSpec notificationSpec = new NotificationSpec(); + + // determinate Source App Name ("Label") + PackageManager pm = getPackageManager(); + ApplicationInfo ai = null; + try { + ai = pm.getApplicationInfo(source, 0); + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + if (ai != null) { + notificationSpec.sourceName = (String) pm.getApplicationLabel(ai); + } + switch (source) { case "org.mariotaku.twidere": case "com.twitter.android": diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java index 3bc4e64de..cc499efdc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -220,6 +220,9 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { case OPEN: action = NotificationListener.ACTION_OPEN; break; + case MUTE: + action = NotificationListener.ACTION_MUTE; + break; } if (action != null) { Intent notificationListenerIntent = new Intent(action); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java index 523b467ef..1eda96881 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/pebble/PebbleProtocol.java @@ -409,10 +409,10 @@ public class PebbleProtocol extends GBDeviceProtocol { if (isFw3x) { // 3.x notification //return encodeTimelinePin(id, (int) ((ts + 600) & 0xffffffffL), (short) 90, PebbleIconID.TIMELINE_CALENDAR, title); // really, this is just for testing - return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, hasHandle, notificationSpec.type); + return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle, notificationSpec.type); } else if (mForceProtocol || notificationSpec.type != NotificationType.EMAIL) { // 2.x notification - return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, hasHandle); + return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, notificationSpec.sourceName, hasHandle); } else { // 1.x notification on FW 2.X String[] parts = {title, notificationSpec.body, ts.toString(), subtitle}; @@ -455,7 +455,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeSetCallState("Where are you?", "Gadgetbridge", start ? ServiceCommand.CALL_INCOMING : ServiceCommand.CALL_END); } - private static byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, boolean hasHandle) { + private static byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle) { final short ACTION_LENGTH_MIN = 10; String[] parts = {title, subtitle, body}; @@ -465,12 +465,18 @@ public class PebbleProtocol extends GBDeviceProtocol { short actions_length; String dismiss_string; String open_string = "Open on phone"; + String mute_string = "Mute"; + if (sourceName != null) { + mute_string += " " + sourceName; + } + byte dismiss_action_id; + if (hasHandle) { - actions_count = 2; + actions_count = 3; dismiss_string = "Dismiss"; dismiss_action_id = 0x02; - actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length + open_string.getBytes().length); + actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length + open_string.getBytes().length + mute_string.getBytes().length); } else { actions_count = 1; dismiss_string = "Dismiss all"; @@ -539,11 +545,19 @@ public class PebbleProtocol extends GBDeviceProtocol { // open action if (hasHandle) { buf.put((byte) 0x01); - buf.put((byte) 0x02); // dissmiss - FIXME: find out how to answer to 2.x generic actions + buf.put((byte) 0x02); // generic buf.put((byte) 0x01); // number attributes buf.put((byte) 0x01); // attribute id (title) buf.putShort((short) open_string.getBytes().length); buf.put(open_string.getBytes()); + + buf.put((byte) 0x04); + buf.put((byte) 0x02); // generic + buf.put((byte) 0x01); // number attributes + buf.put((byte) 0x01); // attribute id (title) + buf.putShort((short) mute_string.getBytes().length); + buf.put(mute_string.getBytes()); + } return buf.array(); @@ -619,7 +633,7 @@ public class PebbleProtocol extends GBDeviceProtocol { return encodeBlobdb(uuid, BLOBDB_INSERT, BLOBDB_PIN, buf.array()); } - private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, boolean hasHandle, NotificationType notificationType) { + private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, NotificationType notificationType) { final short NOTIFICATION_PIN_LENGTH = 46; final short ACTION_LENGTH_MIN = 10; @@ -670,12 +684,17 @@ public class PebbleProtocol extends GBDeviceProtocol { short actions_length; String dismiss_string; String open_string = "Open on phone"; + String mute_string = "Mute"; + if (sourceName != null) { + mute_string += " " + sourceName; + } + byte dismiss_action_id; if (hasHandle) { - actions_count = 2; + actions_count = 3; dismiss_string = "Dismiss"; dismiss_action_id = 0x02; - actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length + open_string.getBytes().length); + actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length + open_string.getBytes().length + mute_string.getBytes().length); } else { actions_count = 1; dismiss_string = "Dismiss all"; @@ -751,7 +770,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.putShort((short) dismiss_string.getBytes().length); buf.put(dismiss_string.getBytes()); - // open action + // open and mute actions if (hasHandle) { buf.put((byte) 0x01); buf.put((byte) 0x02); // generic action @@ -759,11 +778,18 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.put((byte) 0x01); // attribute id (title) buf.putShort((short) open_string.getBytes().length); buf.put(open_string.getBytes()); + + buf.put((byte) 0x04); + buf.put((byte) 0x02); // generic action + buf.put((byte) 0x01); // number attributes + buf.put((byte) 0x01); // attribute id (title) + buf.putShort((short) mute_string.getBytes().length); + buf.put(mute_string.getBytes()); } return encodeBlobdb(UUID.randomUUID(), BLOBDB_INSERT, BLOBDB_NOTIFICATION, buf.array()); } - public byte[] encodeActionResponse2x(int id, int iconId, String caption) { + public byte[] encodeActionResponse2x(int id, byte actionId, int iconId, String caption) { short length = (short) (18 + caption.getBytes().length); ByteBuffer buf = ByteBuffer.allocate(LENGTH_PREFIX + length); buf.order(ByteOrder.BIG_ENDIAN); @@ -772,7 +798,7 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.order(ByteOrder.LITTLE_ENDIAN); buf.put(NOTIFICATIONACTION_RESPONSE); buf.putInt(id); - buf.put((byte) 0x01); // action id? + buf.put(actionId); buf.put(NOTIFICATIONACTION_ACK); buf.put((byte) 2); //nr of attributes buf.put((byte) 6); // icon @@ -1386,7 +1412,7 @@ public class PebbleProtocol extends GBDeviceProtocol { if (command == 0x02) { int id = buf.getInt(); byte action = buf.get(); - if (action >= 0x01 && action <= 0x03) { + if (action >= 0x01 && action <= 0x04) { GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl(); devEvtNotificationControl.handle = id; GBDeviceEventSendBytes sendBytesAck = null; @@ -1395,7 +1421,7 @@ public class PebbleProtocol extends GBDeviceProtocol { case 0x01: devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.OPEN; sendBytesAck = new GBDeviceEventSendBytes(); - sendBytesAck.encodedBytes = encodeActionResponse2x(id, 6, "Opened"); + sendBytesAck.encodedBytes = encodeActionResponse2x(id, action, 6, "Opened"); break; case 0x02: devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS; @@ -1403,6 +1429,11 @@ public class PebbleProtocol extends GBDeviceProtocol { case 0x03: devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.DISMISS_ALL; break; + case 0x04: + devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.MUTE; + sendBytesAck = new GBDeviceEventSendBytes(); + sendBytesAck.encodedBytes = encodeActionResponse2x(id, action, 6, "Muted"); + break; default: return null; } @@ -1424,7 +1455,7 @@ public class PebbleProtocol extends GBDeviceProtocol { long uuid_low = buf.getLong(); int id = (int) (uuid_low & 0xffffffff); byte action = buf.get(); - if (action >= 0x01 && action <= 0x03) { + if (action >= 0x01 && action <= 0x04) { GBDeviceEventNotificationControl dismissNotification = new GBDeviceEventNotificationControl(); dismissNotification.handle = id; String caption = "undefined"; @@ -1445,6 +1476,11 @@ public class PebbleProtocol extends GBDeviceProtocol { caption = "All dismissed"; icon_id = PebbleIconID.RESULT_DISMISSED; break; + case 0x04: + dismissNotification.event = GBDeviceEventNotificationControl.Event.MUTE; + caption = "Muted"; + icon_id = PebbleIconID.RESULT_MUTE; + break; } GBDeviceEventSendBytes sendBytesAck = new GBDeviceEventSendBytes(); sendBytesAck.encodedBytes = encodeActionResponse(new UUID(uuid_high, uuid_low), icon_id, caption); 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 5af7227e1..8660f9977 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/GB.java @@ -308,7 +308,7 @@ public class GB { } public static void updateTransferNotification(String text, boolean ongoing, int percentage, Context context) { - if(percentage == 100) { + if (percentage == 100) { removeNotification(NOTIFICATION_ID_TRANSFER, context); } else { Notification notification = createTransferNotification(text, ongoing, percentage, context);