Pebble: EXPERIMENTAL support for replying to wearable notifications

Tested with Signal, more could work.
here
Andreas Shimokawa 2016-01-09 17:54:17 +01:00
parent 46bbab7df0
commit 0b53f60b0d
8 changed files with 75 additions and 6 deletions

View File

@ -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<NotificationCompat.Action> 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);
}

View File

@ -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)

View File

@ -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";

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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)) {

View File

@ -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) {

View File

@ -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<Pair> iter = list.iterator(); iter.hasNext(); ) {
Pair pair = iter.next();
if (pair.first == id) {
iter.remove();
}
}
}
public Object lookup(int id) {