From 0b53f60b0d285b5889394abc2b8bcd05a5ce470d Mon Sep 17 00:00:00 2001 From: Andreas Shimokawa Date: Sat, 9 Jan 2016 17:54:17 +0100 Subject: [PATCH] Pebble: EXPERIMENTAL support for replying to wearable notifications Tested with Signal, more could work. --- .../externalevents/NotificationListener.java | 44 +++++++++++++++++++ .../gadgetbridge/impl/GBDeviceService.java | 1 + .../gadgetbridge/model/DeviceService.java | 1 + .../gadgetbridge/model/NotificationSpec.java | 3 ++ .../service/AbstractDeviceSupport.java | 8 +++- .../service/DeviceCommunicationService.java | 6 ++- .../devices/pebble/PebbleProtocol.java | 4 +- .../gadgetbridge/util/LimitedQueue.java | 14 +++++- 8 files changed, 75 insertions(+), 6 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 5fc8c9629..81eaaf807 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/externalevents/NotificationListener.java @@ -16,15 +16,20 @@ import android.os.PowerManager; import android.preference.PreferenceManager; 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 org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; + import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.service.DeviceCommunicationService; +import nodomain.freeyourgadget.gadgetbridge.util.LimitedQueue; public class NotificationListener extends NotificationListenerService { @@ -38,6 +43,10 @@ public class NotificationListener extends NotificationListenerService { = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.open"; public static final String ACTION_MUTE = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.mute"; + public static final String ACTION_REPLY + = "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.reply"; + + private LimitedQueue mActionLookup = new LimitedQueue(16); private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @SuppressLint("NewApi") @@ -90,6 +99,28 @@ public class NotificationListener extends NotificationListenerService { case ACTION_DISMISS_ALL: NotificationListener.this.cancelAllNotifications(); break; + case ACTION_REPLY: + int id = intent.getIntExtra("handle", -1); + String reply = intent.getStringExtra("reply"); + NotificationCompat.Action replyAction = (NotificationCompat.Action) mActionLookup.lookup(id); + if (replyAction != null && replyAction.getRemoteInputs() != null) { + RemoteInput[] remoteInputs = replyAction.getRemoteInputs(); + PendingIntent actionIntent = replyAction.getActionIntent(); + Intent localIntent = new Intent(); + localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + Bundle extras = new Bundle(); + extras.putCharSequence(remoteInputs[0].getResultKey(), reply); + RemoteInput.addResultsToIntent(remoteInputs, localIntent, extras); + + try { + LOG.info("will send reply intent to remote application"); + actionIntent.send(context, 0, localIntent); + mActionLookup.remove(id); + } catch (PendingIntent.CanceledException e) { + LOG.warn("replyToLastNotification error: " + e.getLocalizedMessage()); + } + } + break; } } @@ -103,6 +134,7 @@ public class NotificationListener extends NotificationListenerService { filterLocal.addAction(ACTION_DISMISS); filterLocal.addAction(ACTION_DISMISS_ALL); filterLocal.addAction(ACTION_MUTE); + filterLocal.addAction(ACTION_REPLY); LocalBroadcastManager.getInstance(this).registerReceiver(mReceiver, filterLocal); } @@ -222,6 +254,18 @@ public class NotificationListener extends NotificationListenerService { dissectNotificationTo(notification, notificationSpec); notificationSpec.id = (int) sbn.getPostTime(); //FIMXE: a truly unique id would be better + + NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification); + List actions = wearableExtender.getActions(); + for (NotificationCompat.Action act : actions) { + if (act != null && act.getRemoteInputs() != null) { + LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag()); + mActionLookup.add(notificationSpec.id, act); + notificationSpec.flags |= NotificationSpec.FLAG_WEARABLE_REPLY; + break; + } + } + GBApplication.deviceService().onNotification(notificationSpec); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java index b1f745675..cee7a5e2e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/impl/GBDeviceService.java @@ -89,6 +89,7 @@ public class GBDeviceService implements DeviceService { @Override public void onNotification(NotificationSpec notificationSpec) { Intent intent = createIntent().setAction(ACTION_NOTIFICATION) + .putExtra(EXTRA_NOTIFICATION_FLAGS, notificationSpec.flags) .putExtra(EXTRA_NOTIFICATION_PHONENUMBER, notificationSpec.phoneNumber) .putExtra(EXTRA_NOTIFICATION_SENDER, notificationSpec.sender) .putExtra(EXTRA_NOTIFICATION_SUBJECT, notificationSpec.subject) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java index 342c66d22..af4a6d1b8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceService.java @@ -35,6 +35,7 @@ public interface DeviceService extends EventHandler { String EXTRA_DEVICE_ADDRESS = "device_address"; String EXTRA_NOTIFICATION_BODY = "notification_body"; + String EXTRA_NOTIFICATION_FLAGS = "notification_flags"; String EXTRA_NOTIFICATION_ID = "notification_id"; String EXTRA_NOTIFICATION_PHONENUMBER = "notification_phonenumber"; String EXTRA_NOTIFICATION_SENDER = "notification_sender"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java index fde544d50..e140ba47c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationSpec.java @@ -1,6 +1,9 @@ package nodomain.freeyourgadget.gadgetbridge.model; public class NotificationSpec { + public static final int FLAG_WEARABLE_REPLY = 0x00000001; + + public int flags; public int id; public String sender; public String phoneNumber; 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 09a0ddaab..56c9cd811 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractDeviceSupport.java @@ -231,14 +231,20 @@ public abstract class AbstractDeviceSupport implements DeviceSupport { case REPLY: String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(deviceEvent.handle); if (phoneNumber != null) { - LOG.info("got notfication reply for " + phoneNumber + " : " + deviceEvent.reply); + LOG.info("got notfication reply for SMS from " + phoneNumber + " : " + deviceEvent.reply); SmsManager.getDefault().sendTextMessage(phoneNumber, null, deviceEvent.reply, null, null); + } else { + LOG.info("got notfication reply for notification id " + deviceEvent.handle + " : " + deviceEvent.reply); + action = NotificationListener.ACTION_REPLY; } break; } if (action != null) { Intent notificationListenerIntent = new Intent(action); notificationListenerIntent.putExtra("handle", deviceEvent.handle); + if (deviceEvent.reply != null) { + notificationListenerIntent.putExtra("reply", deviceEvent.reply); + } LocalBroadcastManager.getInstance(context).sendBroadcast(notificationListenerIntent); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java index 20bff1839..d85c12909 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceCommunicationService.java @@ -70,6 +70,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUS import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_ARTIST; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY; +import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_FLAGS; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_PHONENUMBER; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_SENDER; @@ -218,13 +219,16 @@ public class DeviceCommunicationService extends Service { notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY); notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE); notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1); + notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0); notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME); if (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null) { notificationSpec.sender = getContactDisplayNameByNumber(notificationSpec.phoneNumber); notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver? GBApplication.getIDSenderLookup().add(notificationSpec.id, notificationSpec.phoneNumber); - + } + if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0) + || (notificationSpec.type == NotificationType.SMS && notificationSpec.phoneNumber != null)) { // NOTE: maybe not where it belongs SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this); if (sharedPrefs.getBoolean("pebble_force_untested", false)) { 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 bb606b092..f0373381b 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 @@ -1591,13 +1591,13 @@ public class PebbleProtocol extends GBDeviceProtocol { buf.get(reply); // FIXME: this does not belong here, but we want at least check if there is no chance at all to send out the SMS later before we report success String phoneNumber = (String) GBApplication.getIDSenderLookup().lookup(id); - if (phoneNumber != null) { + //if (phoneNumber != null) { devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; devEvtNotificationControl.reply = new String(reply); caption = "SENT"; icon_id = PebbleIconID.RESULT_SENT; failed = false; - } + //} } } if (failed) { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java index 4058762bc..42bc1566a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/LimitedQueue.java @@ -2,6 +2,7 @@ package nodomain.freeyourgadget.gadgetbridge.util; import android.util.Pair; +import java.util.Iterator; import java.util.LinkedList; public class LimitedQueue { @@ -12,11 +13,20 @@ public class LimitedQueue { this.limit = limit; } - public void add(int id, Object sender) { + public void add(int id, Object obj) { if (list.size() > limit - 1) { list.removeFirst(); } - list.add(new Pair<>(id, sender)); + list.add(new Pair<>(id, obj)); + } + + public void remove(int id) { + for (Iterator iter = list.iterator(); iter.hasNext(); ) { + Pair pair = iter.next(); + if (pair.first == id) { + iter.remove(); + } + } } public Object lookup(int id) {