Various improvements and bugfixes to notification handling

Prevent duplicate notifications with a dedicated data structure (not reusing
the anti-burst one) #1062, #657
Pebble: Forward the actions attached to notifications (not only reply)
inspired by the work of dnastase #705
This commit is contained in:
Daniele Gobbetti 2018-10-31 21:47:12 +01:00
parent b9999edf2a
commit eede85a9c9
12 changed files with 145 additions and 74 deletions

View File

@ -122,7 +122,6 @@ public class DebugActivity extends AbstractGBActivity {
notificationSpec.subject = testString; notificationSpec.subject = testString;
notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()]; notificationSpec.type = NotificationType.values()[sendTypeSpinner.getSelectedItemPosition()];
notificationSpec.pebbleColor = notificationSpec.type.color; notificationSpec.pebbleColor = notificationSpec.type.color;
notificationSpec.id = -1;
GBApplication.deviceService().onNotification(notificationSpec); GBApplication.deviceService().onNotification(notificationSpec);
} }
}); });

View File

@ -20,6 +20,7 @@ public class GBDeviceEventNotificationControl extends GBDeviceEvent {
public int handle; public int handle;
public String phoneNumber; public String phoneNumber;
public String reply; public String reply;
public String title;
public Event event = Event.UNKNOWN; public Event event = Event.UNKNOWN;
public enum Event { public enum Event {

View File

@ -64,10 +64,10 @@ public class AlarmClockReceiver extends BroadcastReceiver {
private synchronized void sendAlarm(boolean on) { private synchronized void sendAlarm(boolean on) {
dismissLastAlarm(); dismissLastAlarm();
if (on) { if (on) {
lastId = generateId();
NotificationSpec spec = new NotificationSpec(); NotificationSpec spec = new NotificationSpec();
//TODO: can we attach a dismiss action to the notification and not use the notification ID explicitly?
lastId = spec.getId();
spec.type = NotificationType.GENERIC_ALARM_CLOCK; spec.type = NotificationType.GENERIC_ALARM_CLOCK;
spec.id = lastId;
spec.sourceName = "ALARMCLOCKRECEIVER"; spec.sourceName = "ALARMCLOCKRECEIVER";
// can we get the alarm title somehow? // can we get the alarm title somehow?
GBApplication.deviceService().onNotification(spec); GBApplication.deviceService().onNotification(spec);
@ -81,8 +81,4 @@ public class AlarmClockReceiver extends BroadcastReceiver {
} }
} }
private int generateId() {
// lacks negative values, but should be sufficient
return (int) (Math.random() * Integer.MAX_VALUE);
}
} }

View File

@ -50,6 +50,7 @@ import android.support.v7.graphics.Palette;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Objects; import java.util.Objects;
@ -87,7 +88,8 @@ public class NotificationListener extends NotificationListenerService {
private LimitedQueue mActionLookup = new LimitedQueue(16); private LimitedQueue mActionLookup = new LimitedQueue(16);
private HashMap<String, Long> notificationTimes = new HashMap<>(); private HashMap<String, Long> notificationBurstPrevention = new HashMap<>();
private HashMap<String, Long> notificationOldRepeatPrevention = new HashMap<>();
private final BroadcastReceiver mReceiver = new BroadcastReceiver() { private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
@ -145,19 +147,20 @@ public class NotificationListener extends NotificationListenerService {
break; break;
case ACTION_REPLY: case ACTION_REPLY:
int id = intent.getIntExtra("handle", -1); int id = intent.getIntExtra("handle", -1);
NotificationCompat.Action wearableAction = (NotificationCompat.Action) mActionLookup.lookup(id);
String reply = intent.getStringExtra("reply"); String reply = intent.getStringExtra("reply");
NotificationCompat.Action replyAction = (NotificationCompat.Action) mActionLookup.lookup(id); if (wearableAction != null) {
if (replyAction != null && replyAction.getRemoteInputs() != null) { PendingIntent actionIntent = wearableAction.getActionIntent();
RemoteInput[] remoteInputs = replyAction.getRemoteInputs();
PendingIntent actionIntent = replyAction.getActionIntent();
Intent localIntent = new Intent(); Intent localIntent = new Intent();
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
Bundle extras = new Bundle(); if(wearableAction.getRemoteInputs()!=null) {
extras.putCharSequence(remoteInputs[0].getResultKey(), reply); RemoteInput[] remoteInputs = wearableAction.getRemoteInputs();
RemoteInput.addResultsToIntent(remoteInputs, localIntent, extras); Bundle extras = new Bundle();
extras.putCharSequence(remoteInputs[0].getResultKey(), reply);
RemoteInput.addResultsToIntent(remoteInputs, localIntent, extras);
}
try { try {
LOG.info("will send reply intent to remote application"); LOG.info("will send exec intent to remote application");
actionIntent.send(context, 0, localIntent); actionIntent.send(context, 0, localIntent);
mActionLookup.remove(id); mActionLookup.remove(id);
} catch (PendingIntent.CanceledException e) { } catch (PendingIntent.CanceledException e) {
@ -209,15 +212,13 @@ public class NotificationListener extends NotificationListenerService {
String source = sbn.getPackageName().toLowerCase(); String source = sbn.getPackageName().toLowerCase();
Notification notification = sbn.getNotification(); Notification notification = sbn.getNotification();
if (notificationTimes.containsKey(source)) { if (notificationOldRepeatPrevention.containsKey(source)) {
long last_time = notificationTimes.get(source); if (notification.when <= notificationOldRepeatPrevention.get(source)) {
if (notification.when <= last_time) { LOG.info("NOT processing notification, already sent newer notifications from this source.");
LOG.info("NOT processing notification, too old. notification.when: " + notification.when + " last notification for this source: " + last_time);
return; return;
} }
} }
NotificationSpec notificationSpec = new NotificationSpec(); NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = (int) sbn.getPostTime(); //FIXME: a truly unique id would be better
// determinate Source App Name ("Label") // determinate Source App Name ("Label")
PackageManager pm = getPackageManager(); PackageManager pm = getPackageManager();
@ -249,7 +250,7 @@ public class NotificationListener extends NotificationListenerService {
// Get color // Get color
notificationSpec.pebbleColor = getPebbleColorForNotification(notificationSpec); notificationSpec.pebbleColor = getPebbleColorForNotification(notificationSpec);
LOG.info("Processing notification " + notificationSpec.id + " from source " + source + " with flags: " + notification.flags); LOG.info("Processing notification " + notificationSpec.getId() + " age: " + (System.currentTimeMillis() - notification.when) + " from source " + source + " with flags: " + notification.flags);
dissectNotificationTo(notification, notificationSpec, preferBigText); dissectNotificationTo(notification, notificationSpec, preferBigText);
@ -263,16 +264,23 @@ public class NotificationListener extends NotificationListenerService {
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification); NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
List<NotificationCompat.Action> actions = wearableExtender.getActions(); List<NotificationCompat.Action> actions = wearableExtender.getActions();
notificationSpec.attachedActions = new ArrayList<>();
for (NotificationCompat.Action act : actions) { for (NotificationCompat.Action act : actions) {
if (act != null && act.getRemoteInputs() != null) { if (act != null) {
LOG.info("found wearable action: " + act.getTitle() + " " + sbn.getTag()); NotificationSpec.Action wearableAction = new NotificationSpec.Action();
mActionLookup.add(notificationSpec.id, act); wearableAction.title = act.getTitle().toString();
notificationSpec.flags |= NotificationSpec.FLAG_WEARABLE_REPLY; if(act.getRemoteInputs()!=null) {
break; wearableAction.isReply = true;
}
notificationSpec.flags |= NotificationSpec.FLAG_WEARABLE_ACTIONS;
notificationSpec.attachedActions.add(wearableAction);
mActionLookup.add((notificationSpec.getId()<<4) + notificationSpec.attachedActions.size(), act);
LOG.info("found wearable action: " + notificationSpec.attachedActions.size() + " - "+ act.getTitle() + " " + sbn.getTag());
} }
} }
if ((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) == 0 && NotificationCompat.isGroupSummary(notification)) { //this could cause #395 to come back
if ((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_ACTIONS) == 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); LOG.info("Not forwarding notification, FLAG_GROUP_SUMMARY is set and no wearable action present. Notification flags: " + notification.flags);
return; return;
} }
@ -280,14 +288,15 @@ public class NotificationListener extends NotificationListenerService {
// Ignore too frequent notifications, according to user preference // Ignore too frequent notifications, according to user preference
long min_timeout = prefs.getInt("notifications_timeout", 0) * 1000; long min_timeout = prefs.getInt("notifications_timeout", 0) * 1000;
long cur_time = System.currentTimeMillis(); long cur_time = System.currentTimeMillis();
if (notificationTimes.containsKey(source)) { if (notificationBurstPrevention.containsKey(source)) {
long last_time = notificationTimes.get(source); long last_time = notificationBurstPrevention.get(source);
if (cur_time - last_time < min_timeout) { if (cur_time - last_time < min_timeout) {
LOG.info("Ignoring frequent notification, last one was " + (cur_time - last_time) + "ms ago"); LOG.info("Ignoring frequent notification, last one was " + (cur_time - last_time) + "ms ago");
return; return;
} }
} }
notificationTimes.put(source, cur_time); notificationBurstPrevention.put(source, cur_time);
notificationOldRepeatPrevention.put(source, notification.when);
GBApplication.deviceService().onNotification(notificationSpec); GBApplication.deviceService().onNotification(notificationSpec);
} }

View File

@ -62,7 +62,6 @@ public class PebbleReceiver extends BroadcastReceiver {
} }
NotificationSpec notificationSpec = new NotificationSpec(); NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
String notificationData = intent.getStringExtra("notificationData"); String notificationData = intent.getStringExtra("notificationData");
try { try {

View File

@ -50,7 +50,6 @@ public class SMSReceiver extends BroadcastReceiver {
} }
NotificationSpec notificationSpec = new NotificationSpec(); NotificationSpec notificationSpec = new NotificationSpec();
notificationSpec.id = -1;
notificationSpec.type = NotificationType.GENERIC_SMS; notificationSpec.type = NotificationType.GENERIC_SMS;
Bundle bundle = intent.getExtras(); Bundle bundle = intent.getExtras();

View File

@ -150,8 +150,9 @@ public class GBDeviceService implements DeviceService {
.putExtra(EXTRA_NOTIFICATION_SUBJECT, notificationSpec.subject) .putExtra(EXTRA_NOTIFICATION_SUBJECT, notificationSpec.subject)
.putExtra(EXTRA_NOTIFICATION_TITLE, notificationSpec.title) .putExtra(EXTRA_NOTIFICATION_TITLE, notificationSpec.title)
.putExtra(EXTRA_NOTIFICATION_BODY, notificationSpec.body) .putExtra(EXTRA_NOTIFICATION_BODY, notificationSpec.body)
.putExtra(EXTRA_NOTIFICATION_ID, notificationSpec.id) .putExtra(EXTRA_NOTIFICATION_ID, notificationSpec.getId())
.putExtra(EXTRA_NOTIFICATION_TYPE, notificationSpec.type) .putExtra(EXTRA_NOTIFICATION_TYPE, notificationSpec.type)
.putExtra(EXTRA_NOTIFICATION_ACTIONS, notificationSpec.attachedActions)
.putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName) .putExtra(EXTRA_NOTIFICATION_SOURCENAME, notificationSpec.sourceName)
.putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor) .putExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR, notificationSpec.pebbleColor)
.putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId); .putExtra(EXTRA_NOTIFICATION_SOURCEAPPID, notificationSpec.sourceAppId);

View File

@ -77,6 +77,7 @@ public interface DeviceService extends EventHandler {
String EXTRA_NOTIFICATION_SUBJECT = "notification_subject"; String EXTRA_NOTIFICATION_SUBJECT = "notification_subject";
String EXTRA_NOTIFICATION_TITLE = "notification_title"; String EXTRA_NOTIFICATION_TITLE = "notification_title";
String EXTRA_NOTIFICATION_TYPE = "notification_type"; String EXTRA_NOTIFICATION_TYPE = "notification_type";
String EXTRA_NOTIFICATION_ACTIONS = "notification_actions";
String EXTRA_NOTIFICATION_PEBBLE_COLOR = "notification_pebble_color"; String EXTRA_NOTIFICATION_PEBBLE_COLOR = "notification_pebble_color";
String EXTRA_FIND_START = "find_start"; String EXTRA_FIND_START = "find_start";
String EXTRA_VIBRATION_INTENSITY = "vibration_intensity"; String EXTRA_VIBRATION_INTENSITY = "vibration_intensity";

View File

@ -16,11 +16,16 @@
along with this program. If not, see <http://www.gnu.org/licenses/>. */ along with this program. If not, see <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.model; package nodomain.freeyourgadget.gadgetbridge.model;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
public class NotificationSpec { public class NotificationSpec {
public static final int FLAG_WEARABLE_REPLY = 0x00000001; public static final int FLAG_WEARABLE_ACTIONS = 0x00000001;
public int flags; public int flags;
public int id; private static final AtomicInteger c = new AtomicInteger((int) (System.currentTimeMillis()/1000));
private int id;
public String sender; public String sender;
public String phoneNumber; public String phoneNumber;
public String title; public String title;
@ -29,7 +34,10 @@ public class NotificationSpec {
public NotificationType type; public NotificationType type;
public String sourceName; public String sourceName;
public String[] cannedReplies; public String[] cannedReplies;
/**
* Wearable actions that were attached to the incoming notifications and will be passed to the gadget (includes the "reply" action)
*/
public ArrayList<Action> attachedActions;
/** /**
* The application that generated the notification. * The application that generated the notification.
*/ */
@ -39,4 +47,24 @@ public class NotificationSpec {
* The color that should be assigned to this notification when displayed on a Pebble * The color that should be assigned to this notification when displayed on a Pebble
*/ */
public byte pebbleColor; public byte pebbleColor;
public NotificationSpec() {
this.id = c.incrementAndGet();
}
public NotificationSpec(int id) {
if (id != -1)
this.id = id;
else
this.id = c.incrementAndGet();
}
public int getId() {
return id;
}
public static class Action implements Serializable {
public boolean isReply = false;
public String title;
}
} }

View File

@ -52,8 +52,8 @@ import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventAppInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventDisplayMessage;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFmFrequency;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventLEDColor;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
@ -337,6 +337,7 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
if (action != null) { if (action != null) {
Intent notificationListenerIntent = new Intent(action); Intent notificationListenerIntent = new Intent(action);
notificationListenerIntent.putExtra("handle", deviceEvent.handle); notificationListenerIntent.putExtra("handle", deviceEvent.handle);
notificationListenerIntent.putExtra("title", deviceEvent.title);
if (deviceEvent.reply != null) { if (deviceEvent.reply != null) {
Prefs prefs = GBApplication.getPrefs(); Prefs prefs = GBApplication.getPrefs();
String suffix = prefs.getString("canned_reply_suffix", null); String suffix = prefs.getString("canned_reply_suffix", null);

View File

@ -147,6 +147,7 @@ import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUS
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACK;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKCOUNT; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKCOUNT;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKNR; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_MUSIC_TRACKNR;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ACTIONS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_BODY; 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_FLAGS;
import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID; import static nodomain.freeyourgadget.gadgetbridge.model.DeviceService.EXTRA_NOTIFICATION_ID;
@ -356,7 +357,8 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
mGBDevice.sendDeviceUpdateIntent(this); mGBDevice.sendDeviceUpdateIntent(this);
break; break;
case ACTION_NOTIFICATION: { case ACTION_NOTIFICATION: {
NotificationSpec notificationSpec = new NotificationSpec(); int desiredId = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
NotificationSpec notificationSpec = new NotificationSpec(desiredId);
notificationSpec.phoneNumber = intent.getStringExtra(EXTRA_NOTIFICATION_PHONENUMBER); notificationSpec.phoneNumber = intent.getStringExtra(EXTRA_NOTIFICATION_PHONENUMBER);
notificationSpec.sender = intent.getStringExtra(EXTRA_NOTIFICATION_SENDER); notificationSpec.sender = intent.getStringExtra(EXTRA_NOTIFICATION_SENDER);
notificationSpec.subject = intent.getStringExtra(EXTRA_NOTIFICATION_SUBJECT); notificationSpec.subject = intent.getStringExtra(EXTRA_NOTIFICATION_SUBJECT);
@ -364,17 +366,17 @@ public class DeviceCommunicationService extends Service implements SharedPrefere
notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY); notificationSpec.body = intent.getStringExtra(EXTRA_NOTIFICATION_BODY);
notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME); notificationSpec.sourceName = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCENAME);
notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE); notificationSpec.type = (NotificationType) intent.getSerializableExtra(EXTRA_NOTIFICATION_TYPE);
notificationSpec.attachedActions = (ArrayList<NotificationSpec.Action>) intent.getSerializableExtra(EXTRA_NOTIFICATION_ACTIONS);
notificationSpec.pebbleColor = (byte) intent.getSerializableExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR); notificationSpec.pebbleColor = (byte) intent.getSerializableExtra(EXTRA_NOTIFICATION_PEBBLE_COLOR);
notificationSpec.id = intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1);
notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0); notificationSpec.flags = intent.getIntExtra(EXTRA_NOTIFICATION_FLAGS, 0);
notificationSpec.sourceAppId = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCEAPPID); notificationSpec.sourceAppId = intent.getStringExtra(EXTRA_NOTIFICATION_SOURCEAPPID);
if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) { if (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null) {
notificationSpec.id = mRandom.nextInt(); // FIXME: add this in external SMS Receiver? GBApplication.getIDSenderLookup().add(notificationSpec.getId(), notificationSpec.phoneNumber);
GBApplication.getIDSenderLookup().add(notificationSpec.id, notificationSpec.phoneNumber);
} }
if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_REPLY) > 0) //TODO: check if at least one of the attached actions is a reply action instead?
if (((notificationSpec.flags & NotificationSpec.FLAG_WEARABLE_ACTIONS) > 0)
|| (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null)) { || (notificationSpec.type == NotificationType.GENERIC_SMS && notificationSpec.phoneNumber != null)) {
// NOTE: maybe not where it belongs // NOTE: maybe not where it belongs
if (prefs.getBoolean("pebble_force_untested", false)) { if (prefs.getBoolean("pebble_force_untested", false)) {

View File

@ -60,6 +60,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec.Action;
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
import nodomain.freeyourgadget.gadgetbridge.model.Weather; import nodomain.freeyourgadget.gadgetbridge.model.Weather;
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
@ -484,8 +485,9 @@ public class PebbleProtocol extends GBDeviceProtocol {
@Override @Override
public byte[] encodeNotification(NotificationSpec notificationSpec) { public byte[] encodeNotification(NotificationSpec notificationSpec) {
boolean hasHandle = notificationSpec.id != -1 && notificationSpec.phoneNumber == null; //TODO: simplify this logic? is hasHandle still needed?
int id = notificationSpec.id != -1 ? notificationSpec.id : mRandom.nextInt(); boolean hasHandle = notificationSpec.getId() != -1 && notificationSpec.phoneNumber == null;
int id = notificationSpec.getId() != -1 ? notificationSpec.getId() : mRandom.nextInt();
String title; String title;
String subtitle = null; String subtitle = null;
@ -507,11 +509,11 @@ public class PebbleProtocol extends GBDeviceProtocol {
// 3.x notification // 3.x notification
return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, return encodeBlobdbNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body,
notificationSpec.sourceName, hasHandle, notificationSpec.type, notificationSpec.pebbleColor, notificationSpec.sourceName, hasHandle, notificationSpec.type, notificationSpec.pebbleColor,
notificationSpec.cannedReplies); notificationSpec.cannedReplies, notificationSpec.attachedActions);
} else if (mForceProtocol || notificationSpec.type != NotificationType.GENERIC_EMAIL) { } else if (mForceProtocol || notificationSpec.type != NotificationType.GENERIC_EMAIL) {
// 2.x notification // 2.x notification
return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body, return encodeExtensibleNotification(id, (int) (ts & 0xffffffffL), title, subtitle, notificationSpec.body,
notificationSpec.sourceName, hasHandle, notificationSpec.cannedReplies); notificationSpec.sourceName, hasHandle, notificationSpec.cannedReplies, notificationSpec.attachedActions);
} else { } else {
// 1.x notification on FW 2.X // 1.x notification on FW 2.X
String[] parts = {title, notificationSpec.body, String.valueOf(ts), subtitle}; String[] parts = {title, notificationSpec.body, String.valueOf(ts), subtitle};
@ -594,7 +596,8 @@ public class PebbleProtocol extends GBDeviceProtocol {
*/ */
} }
private byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, String[] cannedReplies) { //TODO: add support for attachedActions
private byte[] encodeExtensibleNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, boolean hasHandle, String[] cannedReplies, ArrayList attachedActions) {
final short ACTION_LENGTH_MIN = 10; final short ACTION_LENGTH_MIN = 10;
String[] parts = {title, subtitle, body}; String[] parts = {title, subtitle, body};
@ -930,7 +933,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName, private byte[] encodeBlobdbNotification(int id, int timestamp, String title, String subtitle, String body, String sourceName,
boolean hasHandle, NotificationType notificationType, byte backgroundColor, boolean hasHandle, NotificationType notificationType, byte backgroundColor,
String[] cannedReplies) { String[] cannedReplies, ArrayList<Action> attachedActions) {
final short NOTIFICATION_PIN_LENGTH = 46; final short NOTIFICATION_PIN_LENGTH = 46;
final short ACTION_LENGTH_MIN = 10; final short ACTION_LENGTH_MIN = 10;
@ -943,36 +946,43 @@ public class PebbleProtocol extends GBDeviceProtocol {
int icon_id = notificationType.icon; int icon_id = notificationType.icon;
// Calculate length first // Calculate length first
byte actions_count; byte actions_count = 0;
short actions_length; short actions_length = 0;
String dismiss_string; String dismiss_string;
String open_string = "Open on phone"; String open_string = "Open on phone";
String mute_string = "Mute"; String mute_string = "Mute";
String reply_string = "Reply";
if (sourceName != null) { if (sourceName != null) {
mute_string += " " + sourceName; mute_string += " " + sourceName;
} }
byte dismiss_action_id; byte dismiss_action_id;
if (hasHandle && !"ALARMCLOCKRECEIVER".equals(sourceName)) { if (hasHandle && !"ALARMCLOCKRECEIVER".equals(sourceName)) {
actions_count = 3; actions_count += 3;
dismiss_string = "Dismiss"; dismiss_string = "Dismiss";
dismiss_action_id = 0x02; dismiss_action_id = 0x02;
actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length + open_string.getBytes().length + mute_string.getBytes().length); //TODO: ACTION_LENGTH_MIN disagrees with my observation of the needed bytes. I used 6 instead of 10
actions_length += (short) (6 * 3 + dismiss_string.getBytes().length + open_string.getBytes().length + mute_string.getBytes().length);
} else { } else {
actions_count = 1; actions_count += 1;
dismiss_string = "Dismiss all"; dismiss_string = "Dismiss all";
dismiss_action_id = 0x03; dismiss_action_id = 0x03;
actions_length = (short) (ACTION_LENGTH_MIN * actions_count + dismiss_string.getBytes().length); actions_length += (short) (ACTION_LENGTH_MIN + dismiss_string.getBytes().length);
}
if (attachedActions != null && attachedActions.size() > 0) {
for (Action act : attachedActions) {
actions_count++;
actions_length += (short) (6 + act.title.getBytes().length);
}
} }
int replies_length = -1; int replies_length = -1;
if (cannedReplies != null && cannedReplies.length > 0) { if (cannedReplies != null && cannedReplies.length > 0) {
actions_count++; //do not increment actions_count! reply is an action and was already added above
for (String reply : cannedReplies) { for (String reply : cannedReplies) {
replies_length += reply.getBytes().length + 1; replies_length += reply.getBytes().length + 1;
} }
actions_length += ACTION_LENGTH_MIN + reply_string.getBytes().length + replies_length + 3; // 3 = attribute id (byte) + length(short) //similarly, only the replies lenght has to be added, the lenght for the bare action was already added above
actions_length += replies_length + 3; // 3 = attribute id (byte) + length(short)
} }
byte attributes_count = 2; // icon byte attributes_count = 2; // icon
@ -1053,21 +1063,31 @@ public class PebbleProtocol extends GBDeviceProtocol {
buf.put(mute_string.getBytes()); buf.put(mute_string.getBytes());
} }
if (cannedReplies != null && replies_length > 0) { if (attachedActions != null && attachedActions.size() > 0) {
buf.put((byte) 0x05); for (int ai = 0 ; ai<attachedActions.size(); ai++) {
buf.put((byte) 0x03); // reply action Action act = attachedActions.get(ai);
buf.put((byte) 0x02); // number attributes buf.put((byte) (0x05 + ai));
buf.put((byte) 0x01); // title if(act.isReply) {
buf.putShort((short) reply_string.getBytes().length); buf.put((byte) 0x03); // reply action
buf.put(reply_string.getBytes()); buf.put((byte) 0x02); // number attributes
buf.put((byte) 0x08); // canned replies } else {
buf.putShort((short) replies_length); buf.put((byte) 0x02); // generic action, dismiss did not do anything
for (int i = 0; i < cannedReplies.length - 1; i++) { buf.put((byte) 0x01); // number attributes
buf.put(cannedReplies[i].getBytes()); }
buf.put((byte) 0x00); buf.put((byte) 0x01); // attribute id (title)
buf.putShort((short) act.title.getBytes().length);
buf.put(act.title.getBytes());
if(act.isReply && cannedReplies != null ) {
buf.put((byte) 0x08); // canned replies
buf.putShort((short) replies_length);
for (int i = 0; i < cannedReplies.length - 1; i++) {
buf.put(cannedReplies[i].getBytes());
buf.put((byte) 0x00);
}
// last one must not be zero terminated, else we get an additional emply reply
buf.put(cannedReplies[cannedReplies.length - 1].getBytes());
}
} }
// last one must not be zero terminated, else we get an additional emply reply
buf.put(cannedReplies[cannedReplies.length - 1].getBytes());
} }
return encodeBlobdb(UUID.randomUUID(), BLOBDB_INSERT, BLOBDB_NOTIFICATION, buf.array()); return encodeBlobdb(UUID.randomUUID(), BLOBDB_INSERT, BLOBDB_NOTIFICATION, buf.array());
@ -2096,7 +2116,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
id = buf.getInt(); id = buf.getInt();
} }
byte action = buf.get(); byte action = buf.get();
if (action >= 0x00 && action <= 0x05) { if (action >= 0x00 && action <= 0xf) {
GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl(); GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl();
devEvtNotificationControl.handle = id; devEvtNotificationControl.handle = id;
String caption = "undefined"; String caption = "undefined";
@ -2125,6 +2145,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
caption = "Muted"; caption = "Muted";
icon_id = PebbleIconID.RESULT_MUTE; icon_id = PebbleIconID.RESULT_MUTE;
break; break;
//TODO: 0x05 is not a special case anymore, and reply action might have an index that is higher. see default below
case 0x05: case 0x05:
case 0x00: case 0x00:
boolean failed = true; boolean failed = true;
@ -2145,6 +2166,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
} }
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY; devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
devEvtNotificationControl.reply = new String(reply); devEvtNotificationControl.reply = new String(reply);
devEvtNotificationControl.handle = (devEvtNotificationControl.handle << 4) + 1;
caption = "SENT"; caption = "SENT";
icon_id = PebbleIconID.RESULT_SENT; icon_id = PebbleIconID.RESULT_SENT;
failed = false; failed = false;
@ -2156,6 +2178,19 @@ public class PebbleProtocol extends GBDeviceProtocol {
devEvtNotificationControl = null; // error devEvtNotificationControl = null; // error
} }
break; break;
default:
if (action > 0x05) {
int simpleActionId = action - 0x05;
caption = "EXECUTED";
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
devEvtNotificationControl.handle = (devEvtNotificationControl.handle << 4) + simpleActionId;
LOG.info("detected simple action, subId:" + simpleActionId + " title:" + devEvtNotificationControl.title);
} else {
caption = "FAILED";
icon_id = PebbleIconID.RESULT_FAILED;
devEvtNotificationControl = null; // error
}
break;
} }
GBDeviceEventSendBytes sendBytesAck = null; GBDeviceEventSendBytes sendBytesAck = null;
if (mFwMajor >= 3 || needsAck2x) { if (mFwMajor >= 3 || needsAck2x) {