mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Compare commits
7 Commits
61431c6116
...
98cc6c5497
Author | SHA1 | Date | |
---|---|---|---|
|
98cc6c5497 | ||
|
965e22bf34 | ||
|
7893ace986 | ||
|
c0883de546 | ||
|
1a21f01071 | ||
|
b1cccae3ac | ||
|
790e81a6f6 |
@ -38,6 +38,13 @@
|
||||
<!-- Take wake locks (e.g. for time sync) -->
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
|
||||
<!--
|
||||
Necessary for asking the user to disable battery optimizations.
|
||||
GB falls under the acceptable use cases documented here:
|
||||
https://developer.android.com/training/monitoring-device-state/doze-standby.html#exemption-cases
|
||||
-->
|
||||
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
|
||||
|
||||
<!-- Read loyalty cards from Catima -->
|
||||
<uses-permission android:name="me.hackerchick.catima.READ_CARDS"/>
|
||||
<uses-permission android:name="me.hackerchick.catima.debug.READ_CARDS"/>
|
||||
|
@ -217,6 +217,12 @@ public class DebugActivity extends AbstractGBActivity {
|
||||
notificationSpec.pebbleColor = notificationSpec.type.color;
|
||||
notificationSpec.attachedActions = new ArrayList<>();
|
||||
|
||||
// DISMISS action
|
||||
NotificationSpec.Action dismissAction = new NotificationSpec.Action();
|
||||
dismissAction.title = "Dismiss";
|
||||
dismissAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS;
|
||||
notificationSpec.attachedActions.add(dismissAction);
|
||||
|
||||
if (notificationSpec.type == NotificationType.GENERIC_SMS) {
|
||||
// REPLY action
|
||||
NotificationSpec.Action replyAction = new NotificationSpec.Action();
|
||||
|
@ -0,0 +1,34 @@
|
||||
/* Copyright (C) 2024 José Rebelo
|
||||
|
||||
This file is part of Gadgetbridge.
|
||||
|
||||
Gadgetbridge is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
Gadgetbridge is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner;
|
||||
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.GarminCoordinator;
|
||||
|
||||
public class GarminForerunner45Coordinator extends GarminCoordinator {
|
||||
@Override
|
||||
protected Pattern getSupportedDeviceName() {
|
||||
return Pattern.compile("^Forerunner 45$");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDeviceNameResource() {
|
||||
return R.string.devicetype_garmin_forerunner_45;
|
||||
}
|
||||
}
|
@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiTLV;
|
||||
@ -376,13 +378,12 @@ public class Notifications {
|
||||
public int type = 0;
|
||||
public int encoding = 0; // 3 - "utf-16"
|
||||
public int subId = 0;
|
||||
public String sender;
|
||||
public String key;
|
||||
public String addData;
|
||||
public String text;
|
||||
|
||||
public ReplyResponse(ParamsProvider paramsProvider) {
|
||||
super(paramsProvider);
|
||||
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = id;
|
||||
}
|
||||
@ -398,13 +399,17 @@ public class Notifications {
|
||||
if (this.tlv.contains(0x04))
|
||||
this.key = this.tlv.getString(0x04);
|
||||
if (this.tlv.contains(0x05))
|
||||
this.sender = this.tlv.getString(0x05);
|
||||
if (this.tlv.contains(0x06))
|
||||
this.text = this.tlv.getString(0x06);
|
||||
this.addData = this.tlv.getString(0x05);
|
||||
if (this.tlv.contains(0x06)) {
|
||||
if(this.encoding == 3) {
|
||||
this.text = new String(this.tlv.getBytes(0x06), StandardCharsets.UTF_16);
|
||||
} else {
|
||||
this.text = this.tlv.getString(0x06);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//TODO: send ack if required, 7f on error.
|
||||
public static class ReplyAck extends HuaweiPacket {
|
||||
|
||||
public ReplyAck(
|
||||
|
@ -22,6 +22,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.externalevents;
|
||||
|
||||
import android.app.ActivityOptions;
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
@ -46,6 +47,7 @@ import android.service.notification.NotificationListenerService;
|
||||
import android.service.notification.StatusBarNotification;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.RemoteInput;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@ -114,7 +116,7 @@ public class NotificationListener extends NotificationListenerService {
|
||||
public static final String ACTION_REPLY
|
||||
= "nodomain.freeyourgadget.gadgetbridge.notificationlistener.action.reply";
|
||||
|
||||
private final LimitedQueue<Integer, NotificationCompat.Action> mActionLookup = new LimitedQueue<>(32);
|
||||
private final LimitedQueue<Integer, NotificationAction> mActionLookup = new LimitedQueue<>(32);
|
||||
private final LimitedQueue<Integer, String> mPackageLookup = new LimitedQueue<>(64);
|
||||
private final LimitedQueue<Integer, Long> mNotificationHandleLookup = new LimitedQueue<>(128);
|
||||
|
||||
@ -220,31 +222,43 @@ public class NotificationListener extends NotificationListenerService {
|
||||
NotificationListener.this.cancelAllNotifications();
|
||||
break;
|
||||
case ACTION_REPLY:
|
||||
NotificationCompat.Action wearableAction = mActionLookup.lookup(handle);
|
||||
NotificationAction wearableAction = mActionLookup.lookup(handle);
|
||||
String reply = intent.getStringExtra("reply");
|
||||
if (wearableAction != null) {
|
||||
PendingIntent actionIntent = wearableAction.getActionIntent();
|
||||
PendingIntent actionIntent = wearableAction.getIntent();
|
||||
if (actionIntent == null) {
|
||||
LOG.warn("Action intent is null");
|
||||
break;
|
||||
}
|
||||
Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
if (wearableAction.getRemoteInputs() != null && wearableAction.getRemoteInputs().length > 0) {
|
||||
RemoteInput[] remoteInputs = wearableAction.getRemoteInputs();
|
||||
Bundle extras = new Bundle();
|
||||
extras.putCharSequence(remoteInputs[0].getResultKey(), reply);
|
||||
RemoteInput.addResultsToIntent(remoteInputs, localIntent, extras);
|
||||
}
|
||||
|
||||
final RemoteInput remoteInput = wearableAction.getRemoteInput();
|
||||
|
||||
try {
|
||||
LOG.info("will send exec intent to remote application");
|
||||
actionIntent.send(context, 0, localIntent);
|
||||
LOG.info("Will send exec intent to remote application");
|
||||
|
||||
if (remoteInput != null) {
|
||||
final Intent localIntent = new Intent();
|
||||
localIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
final Bundle extras = new Bundle();
|
||||
extras.putCharSequence(remoteInput.getResultKey(), reply);
|
||||
RemoteInput.addResultsToIntent(new RemoteInput[]{remoteInput}, localIntent, extras);
|
||||
actionIntent.send(context, 0, localIntent);
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
final ActivityOptions activityOptions = ActivityOptions.makeBasic();
|
||||
final Bundle bundle = activityOptions.setPendingIntentBackgroundActivityStartMode(ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
|
||||
.toBundle();
|
||||
actionIntent.send(bundle);
|
||||
} else {
|
||||
actionIntent.send();
|
||||
}
|
||||
}
|
||||
mActionLookup.remove(handle);
|
||||
} catch (final PendingIntent.CanceledException e) {
|
||||
LOG.warn("replyToLastNotification error", e);
|
||||
}
|
||||
} else {
|
||||
LOG.warn("Received ACTION_REPLY but cannot find the corresponding wearableAction");
|
||||
LOG.warn("Received ACTION_REPLY for handle {}, but cannot find the corresponding wearableAction", handle);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -431,11 +445,11 @@ public class NotificationListener extends NotificationListenerService {
|
||||
}
|
||||
|
||||
NotificationCompat.WearableExtender wearableExtender = new NotificationCompat.WearableExtender(notification);
|
||||
List<NotificationCompat.Action> actions = wearableExtender.getActions();
|
||||
List<NotificationCompat.Action> wearableActions = wearableExtender.getActions();
|
||||
|
||||
// Some apps such as Telegram send both a group + normal notifications, which would get sent in duplicate to the devices
|
||||
// Others only send the group summary, so they need to be whitelisted
|
||||
if (actions.isEmpty() && NotificationCompat.isGroupSummary(notification)
|
||||
if (wearableActions.isEmpty() && NotificationCompat.isGroupSummary(notification)
|
||||
&& !GROUP_SUMMARY_WHITELIST.contains(source)) { //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;
|
||||
@ -450,19 +464,51 @@ public class NotificationListener extends NotificationListenerService {
|
||||
dismissAction.type = NotificationSpec.Action.TYPE_SYNTECTIC_DISMISS;
|
||||
notificationSpec.attachedActions.add(dismissAction);
|
||||
|
||||
for (NotificationCompat.Action act : actions) {
|
||||
boolean hasWearableActions = false;
|
||||
for (NotificationCompat.Action act : wearableActions) {
|
||||
if (act != null) {
|
||||
NotificationSpec.Action wearableAction = new NotificationSpec.Action();
|
||||
wearableAction.title = act.getTitle().toString();
|
||||
wearableAction.title = String.valueOf(act.getTitle());
|
||||
final RemoteInput remoteInput;
|
||||
if (act.getRemoteInputs() != null && act.getRemoteInputs().length > 0) {
|
||||
wearableAction.type = NotificationSpec.Action.TYPE_WEARABLE_REPLY;
|
||||
remoteInput = act.getRemoteInputs()[0];
|
||||
} else {
|
||||
wearableAction.type = NotificationSpec.Action.TYPE_WEARABLE_SIMPLE;
|
||||
remoteInput = null;
|
||||
}
|
||||
notificationSpec.attachedActions.add(wearableAction);
|
||||
wearableAction.handle = (notificationSpec.getId() << 4) + notificationSpec.attachedActions.size();
|
||||
mActionLookup.add((int)wearableAction.handle, act);
|
||||
LOG.info("Found wearable action: {} - {} {}", notificationSpec.attachedActions.size(), act.getTitle(), sbn.getTag());
|
||||
wearableAction.handle = ((long) notificationSpec.getId() << 4) + notificationSpec.attachedActions.size();
|
||||
mActionLookup.add((int) wearableAction.handle, new NotificationAction(act.getActionIntent(), remoteInput));
|
||||
LOG.debug("Found wearable action {}: {} - {} {}", notificationSpec.attachedActions.size(), (int) wearableAction.handle, act.getTitle(), sbn.getTag());
|
||||
hasWearableActions = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasWearableActions && notification.actions != null) {
|
||||
// If no wearable actions are sent, fallback to normal custom actions
|
||||
for (final Notification.Action act : notification.actions) {
|
||||
final NotificationSpec.Action customAction = new NotificationSpec.Action();
|
||||
customAction.title = String.valueOf(act.title);
|
||||
final RemoteInput remoteInput;
|
||||
if (act.getRemoteInputs() != null && act.getRemoteInputs().length > 0) {
|
||||
customAction.type = NotificationSpec.Action.TYPE_CUSTOM_REPLY;
|
||||
android.app.RemoteInput ri = act.getRemoteInputs()[0];
|
||||
// FIXME this is not very clean
|
||||
remoteInput = new RemoteInput.Builder(ri.getResultKey())
|
||||
.setLabel(ri.getLabel())
|
||||
.setChoices(ri.getChoices())
|
||||
.setAllowFreeFormInput(ri.getAllowFreeFormInput())
|
||||
.addExtras(ri.getExtras())
|
||||
.build();
|
||||
} else {
|
||||
customAction.type = NotificationSpec.Action.TYPE_CUSTOM_SIMPLE;
|
||||
remoteInput = null;
|
||||
}
|
||||
notificationSpec.attachedActions.add(customAction);
|
||||
customAction.handle = ((long) notificationSpec.getId() << 4) + notificationSpec.attachedActions.size();
|
||||
mActionLookup.add((int) customAction.handle, new NotificationAction(act.actionIntent, remoteInput));
|
||||
LOG.info("Found custom action {}: {} - {}", notificationSpec.attachedActions.size(), (int) customAction.handle, act.title);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1118,4 +1164,23 @@ public class NotificationListener extends NotificationListenerService {
|
||||
|
||||
return PebbleUtils.getPebbleColor(iconPrimaryColor);
|
||||
}
|
||||
|
||||
private static class NotificationAction {
|
||||
private final PendingIntent intent;
|
||||
@Nullable
|
||||
private final RemoteInput remoteInput;
|
||||
|
||||
private NotificationAction(final PendingIntent pendingIntent, @Nullable final RemoteInput remoteInput) {
|
||||
this.intent = pendingIntent;
|
||||
this.remoteInput = remoteInput;
|
||||
}
|
||||
|
||||
public PendingIntent getIntent() {
|
||||
return intent;
|
||||
}
|
||||
|
||||
public RemoteInput getRemoteInput() {
|
||||
return remoteInput;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ public class ActivityUser {
|
||||
|
||||
private static final String defaultUserName = "gadgetbridge-user";
|
||||
public static final int defaultUserGender = GENDER_FEMALE;
|
||||
public static final String defaultUserDateOfBirth = "1970-01-01";
|
||||
public static final String defaultUserDateOfBirth = "2000-01-01";
|
||||
public static final int defaultUserAge = 0;
|
||||
public static final int defaultUserHeightCm = 175;
|
||||
public static final int defaultUserWeightKg = 70;
|
||||
|
@ -83,6 +83,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.Ga
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner255SMusicCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner265SCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner45Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner55Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner620Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.garmin.watches.forerunner.GarminForerunner955Coordinator;
|
||||
@ -457,6 +458,7 @@ public enum DeviceType {
|
||||
GARMIN_FENIX_7X(GarminFenix7XCoordinator.class),
|
||||
GARMIN_FENIX_7_PRO(GarminFenix7ProCoordinator.class),
|
||||
GARMIN_FENIX_8(GarminFenix8Coordinator.class),
|
||||
GARMIN_FORERUNNER_45(GarminForerunner45Coordinator.class),
|
||||
GARMIN_FORERUNNER_55(GarminForerunner55Coordinator.class),
|
||||
GARMIN_FORERUNNER_165(GarminForerunner165Coordinator.class),
|
||||
GARMIN_FORERUNNER_235(GarminForerunner235Coordinator.class),
|
||||
|
@ -83,9 +83,15 @@ public class NotificationSpec {
|
||||
public static final int TYPE_SYNTECTIC_DISMISS_ALL = 4;
|
||||
public static final int TYPE_SYNTECTIC_MUTE = 5;
|
||||
public static final int TYPE_SYNTECTIC_OPEN = 6;
|
||||
public static final int TYPE_CUSTOM_SIMPLE = 7;
|
||||
public static final int TYPE_CUSTOM_REPLY = 8;
|
||||
|
||||
public int type = TYPE_UNDEFINED;
|
||||
public long handle;
|
||||
public String title;
|
||||
|
||||
public boolean isReply() {
|
||||
return type == TYPE_WEARABLE_REPLY || type == TYPE_SYNTECTIC_REPLY_PHONENR || type == TYPE_CUSTOM_REPLY;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -522,10 +522,10 @@ public abstract class AbstractDeviceSupport implements DeviceSupport {
|
||||
deviceEvent.phoneNumber = GBApplication.getIDSenderLookup().lookup((int) (deviceEvent.handle >> 4));
|
||||
}
|
||||
if (deviceEvent.phoneNumber != null) {
|
||||
LOG.info("Got notification reply for SMS from " + deviceEvent.phoneNumber + " : " + deviceEvent.reply);
|
||||
LOG.info("Got notification reply for SMS from {} : {}", deviceEvent.phoneNumber, deviceEvent.reply);
|
||||
SmsManager.getDefault().sendTextMessage(deviceEvent.phoneNumber, null, deviceEvent.reply, null, null);
|
||||
} else {
|
||||
LOG.info("Got notification reply for notification id " + deviceEvent.handle + " : " + deviceEvent.reply);
|
||||
LOG.info("Got notification reply for notification id {} : {}", deviceEvent.handle, deviceEvent.reply);
|
||||
action = NotificationListener.ACTION_REPLY;
|
||||
}
|
||||
break;
|
||||
|
@ -1467,7 +1467,7 @@ public class BangleJSDeviceSupport extends AbstractBTLEDeviceSupport {
|
||||
if (notificationSpec.attachedActions!=null)
|
||||
for (int i=0;i<notificationSpec.attachedActions.size();i++) {
|
||||
NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
if (action.type==NotificationSpec.Action.TYPE_WEARABLE_REPLY) {
|
||||
if (action.isReply()) {
|
||||
mNotificationReplyAction.add(notificationSpec.getId(), action.handle);
|
||||
canReply = true;
|
||||
}
|
||||
|
@ -222,6 +222,30 @@ public class NotificationsHandler implements MessageHandler {
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.MUTE;
|
||||
message.addGbDeviceEvent(deviceEvtNotificationControl);
|
||||
break;
|
||||
case CUSTOM_ACTION_1:
|
||||
case CUSTOM_ACTION_2:
|
||||
case CUSTOM_ACTION_3:
|
||||
case CUSTOM_ACTION_4:
|
||||
case CUSTOM_ACTION_5:
|
||||
deviceEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
|
||||
|
||||
// We need to map back to the handle of the action - the custom actions are added in order
|
||||
final int customActionIndex = message.getNotificationAction().ordinal();
|
||||
int i = 0;
|
||||
for (NotificationSpec.Action attachedAction : notificationSpec.attachedActions) {
|
||||
if (attachedAction.type == NotificationSpec.Action.TYPE_WEARABLE_SIMPLE || attachedAction.type == NotificationSpec.Action.TYPE_CUSTOM_SIMPLE) {
|
||||
if (i == customActionIndex) {
|
||||
deviceEvtNotificationControl.handle = attachedAction.handle;
|
||||
break;
|
||||
} else {
|
||||
i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
message.addGbDeviceEvent(deviceEvtNotificationControl);
|
||||
break;
|
||||
default:
|
||||
LOG.warn("Unknown notification action {}", message.getNotificationAction());
|
||||
}
|
||||
}
|
||||
|
||||
@ -330,7 +354,7 @@ public class NotificationsHandler implements MessageHandler {
|
||||
break;
|
||||
case TITLE:
|
||||
if (NotificationType.GENERIC_SMS.equals(notificationSpec.type))
|
||||
toReturn = notificationSpec.sender == null ? "" : notificationSpec.sender;
|
||||
toReturn = StringUtils.firstNonBlank(notificationSpec.sender, notificationSpec.phoneNumber, "-");
|
||||
else
|
||||
toReturn = notificationSpec.title == null ? "" : notificationSpec.title;
|
||||
break;
|
||||
@ -368,9 +392,12 @@ public class NotificationsHandler implements MessageHandler {
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.ACCEPT_INCOMING_CALL, " ")); //text is not shown on watch
|
||||
}
|
||||
if (null != notificationSpec.attachedActions) {
|
||||
int customActionsIdx = 1;
|
||||
|
||||
for (NotificationSpec.Action action : notificationSpec.attachedActions) {
|
||||
switch (action.type) {
|
||||
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
|
||||
case NotificationSpec.Action.TYPE_CUSTOM_REPLY:
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.REPLY_MESSAGES, action.title));
|
||||
break;
|
||||
@ -380,11 +407,20 @@ public class NotificationsHandler implements MessageHandler {
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_MUTE:
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.BLOCK_APPLICATION, action.title));
|
||||
break;
|
||||
|
||||
case NotificationSpec.Action.TYPE_WEARABLE_SIMPLE:
|
||||
case NotificationSpec.Action.TYPE_CUSTOM_SIMPLE:
|
||||
if (customActionsIdx <= 5) {
|
||||
garminActions.add(encodeNotificationAction(NotificationAction.valueOf("CUSTOM_ACTION_" + customActionsIdx), action.title));
|
||||
} else {
|
||||
LOG.error("Too many custom actions!");
|
||||
}
|
||||
customActionsIdx++;
|
||||
break;
|
||||
}
|
||||
// LOG.info("Notification has action {} with title {}", action.type, action.title);
|
||||
}
|
||||
}
|
||||
|
||||
if (garminActions.isEmpty())
|
||||
return new String(new byte[]{0x00, 0x00, 0x00, 0x00});
|
||||
|
||||
@ -414,6 +450,11 @@ public class NotificationsHandler implements MessageHandler {
|
||||
}
|
||||
|
||||
public enum NotificationAction {
|
||||
CUSTOM_ACTION_1(1, null),
|
||||
CUSTOM_ACTION_2(2, null),
|
||||
CUSTOM_ACTION_3(3, null),
|
||||
CUSTOM_ACTION_4(4, null),
|
||||
CUSTOM_ACTION_5(5, null),
|
||||
REPLY_INCOMING_CALL(94, NotificationActionIconPosition.BOTTOM),
|
||||
REPLY_MESSAGES(95, NotificationActionIconPosition.BOTTOM),
|
||||
ACCEPT_INCOMING_CALL(96, NotificationActionIconPosition.RIGHT),
|
||||
|
@ -343,14 +343,10 @@ public class ZeppOsNotificationService extends AbstractZeppOsService {
|
||||
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
|
||||
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
|
||||
switch (action.type) {
|
||||
case NotificationSpec.Action.TYPE_WEARABLE_REPLY:
|
||||
case NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR:
|
||||
hasReply = true;
|
||||
mNotificationReplyAction.add(notificationSpec.getId(), action.handle);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
if (action.isReply()) {
|
||||
hasReply = true;
|
||||
mNotificationReplyAction.add(notificationSpec.getId(), action.handle);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendNotificationRemoveRequest;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests.SendSMSReplyAck;
|
||||
|
||||
public class HuaweiNotificationsManager {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(HuaweiNotificationsManager.class);
|
||||
@ -105,16 +106,25 @@ public class HuaweiNotificationsManager {
|
||||
LOG.info("Reply is empty");
|
||||
return;
|
||||
}
|
||||
if(response.type != 1 && response.type != 2) {
|
||||
LOG.info("Reply: only type 1 and 2 supported");
|
||||
return;
|
||||
}
|
||||
|
||||
NotificationSpec notificationSpec = null;
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
if (getNotificationKey(spec).equals(response.key)) {
|
||||
notificationSpec = spec;
|
||||
break;
|
||||
if(response.type == 1) { // generic SMS notification reply. Find by phone number
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
if (spec.phoneNumber.equals(response.key)) {
|
||||
notificationSpec = spec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if(response.type == 2) {
|
||||
for (NotificationSpec spec : notificationSpecCache) {
|
||||
if (getNotificationKey(spec).equals(response.key)) {
|
||||
notificationSpec = spec;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOG.info("Reply type {} is not supported", response.type);
|
||||
return;
|
||||
}
|
||||
if (notificationSpec == null) {
|
||||
LOG.info("Notification for reply is not found");
|
||||
@ -131,7 +141,7 @@ public class HuaweiNotificationsManager {
|
||||
if (hasActions) {
|
||||
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
|
||||
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
if (action.isReply()) {
|
||||
deviceEvtNotificationControl.handle = action.handle; //handle of wearable action is needed
|
||||
break;
|
||||
}
|
||||
@ -139,7 +149,21 @@ public class HuaweiNotificationsManager {
|
||||
}
|
||||
}
|
||||
this.support.evaluateGBDeviceEvent(deviceEvtNotificationControl);
|
||||
//TODO: maybe should be send reply. Service: 0x2, command: 0x10, tlv 7 and/or 1, type byte, 7f on error
|
||||
if(response.type == 1) {
|
||||
// NOTE: send response only for SMS reply
|
||||
try {
|
||||
// 0xff - OK
|
||||
// 0x7f - error
|
||||
// TODO: get response from SMSManager. Send pending intent result.
|
||||
// result can be one of the RESULT_ERROR_* from SmsManager. Not sure, need to check.
|
||||
// currently always send OK.
|
||||
byte resultCode = (byte)0xff;
|
||||
SendSMSReplyAck sendNotificationReq = new SendSMSReplyAck(this.support, resultCode);
|
||||
sendNotificationReq.doPerform();
|
||||
} catch (IOException e) {
|
||||
LOG.error("Sending sns reply ACK", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -72,7 +72,7 @@ public class SendNotificationRequest extends Request {
|
||||
if (hasActions) {
|
||||
for (int i = 0; i < notificationSpec.attachedActions.size(); i++) {
|
||||
final NotificationSpec.Action action = notificationSpec.attachedActions.get(i);
|
||||
if (action.type == NotificationSpec.Action.TYPE_WEARABLE_REPLY || action.type == NotificationSpec.Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
if (action.isReply()) {
|
||||
//NOTE: store notification key instead action key. The watch returns this key so it is more easier to find action by notification key
|
||||
replyKey = getNotificationKey(notificationSpec);
|
||||
break;
|
||||
|
@ -0,0 +1,29 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.requests;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiPacket;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.packets.Notifications;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.huawei.HuaweiSupportProvider;
|
||||
|
||||
public class SendSMSReplyAck extends Request {
|
||||
private final byte resultCode;
|
||||
|
||||
public SendSMSReplyAck(HuaweiSupportProvider support,
|
||||
byte resultCode) {
|
||||
super(support);
|
||||
this.serviceId = Notifications.id;
|
||||
this.commandId = Notifications.NotificationReply.id;
|
||||
this.resultCode = resultCode;
|
||||
this.addToResponse = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<byte[]> createRequest() throws RequestCreationException {
|
||||
try {
|
||||
return new Notifications.NotificationReply.ReplyAck(this.paramsProvider, this.resultCode).serialize();
|
||||
} catch(HuaweiPacket.CryptoException e) {
|
||||
throw new RequestCreationException(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
@ -840,7 +840,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
for (Action act : attachedActions) {
|
||||
actions_count++;
|
||||
actions_length += (short) (ACTION_LENGTH_MIN + act.title.getBytes().length);
|
||||
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
if (act.isReply()) {
|
||||
actions_length += (short) replies_length + 3; // 3 = attribute id (byte) + length(short)
|
||||
}
|
||||
}
|
||||
@ -955,7 +955,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
buf.put((byte) (0x05 + ai));
|
||||
}
|
||||
|
||||
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
if (act.isReply()) {
|
||||
buf.put((byte) 0x03); // reply action
|
||||
buf.put((byte) 0x02); // number attributes
|
||||
} else {
|
||||
@ -970,7 +970,7 @@ public class PebbleProtocol extends GBDeviceProtocol {
|
||||
buf.put((byte) 0x01); // attribute id (title)
|
||||
buf.putShort((short) act.title.getBytes().length);
|
||||
buf.put(act.title.getBytes());
|
||||
if (act.type == Action.TYPE_WEARABLE_REPLY || act.type == Action.TYPE_SYNTECTIC_REPLY_PHONENR) {
|
||||
if (act.isReply()) {
|
||||
buf.put((byte) 0x08); // canned replies
|
||||
buf.putShort((short) replies_length);
|
||||
if (cannedReplies != null && cannedReplies.length > 0) {
|
||||
|
@ -17,6 +17,7 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
@ -28,6 +29,7 @@ import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.PowerManager;
|
||||
import android.provider.Settings;
|
||||
import android.widget.Toast;
|
||||
|
||||
@ -52,11 +54,15 @@ import nodomain.freeyourgadget.gadgetbridge.externalevents.NotificationListener;
|
||||
public class PermissionsUtils {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(PermissionsUtils.class);
|
||||
|
||||
public static final String CUSTOM_PERM_IGNORE_BATT_OPTIM = "custom_perm_ignore_battery_optimization";
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_LISTENER = "custom_perm_notifications_listener";
|
||||
public static final String CUSTOM_PERM_NOTIFICATION_SERVICE = "custom_perm_notifications_service";
|
||||
public static final String CUSTOM_PERM_DISPLAY_OVER = "custom_perm_display_over";
|
||||
|
||||
public static final List<String> specialPermissions = new ArrayList<String>() {{
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
add(CUSTOM_PERM_IGNORE_BATT_OPTIM);
|
||||
}
|
||||
add(CUSTOM_PERM_NOTIFICATION_LISTENER);
|
||||
add(CUSTOM_PERM_NOTIFICATION_SERVICE);
|
||||
add(CUSTOM_PERM_DISPLAY_OVER);
|
||||
@ -68,6 +74,12 @@ public class PermissionsUtils {
|
||||
|
||||
public static ArrayList<PermissionDetails> getRequiredPermissionsList(Activity activity) {
|
||||
ArrayList<PermissionDetails> permissionsList = new ArrayList<>();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_IGNORE_BATT_OPTIM,
|
||||
activity.getString(R.string.permission_disable_doze_title),
|
||||
activity.getString(R.string.permission_disable_doze_summary)));
|
||||
}
|
||||
permissionsList.add(new PermissionDetails(
|
||||
CUSTOM_PERM_NOTIFICATION_LISTENER,
|
||||
activity.getString(R.string.menuitem_notifications),
|
||||
@ -189,6 +201,9 @@ public class PermissionsUtils {
|
||||
return ((NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE)).isNotificationPolicyAccessGranted();
|
||||
} else if (permission.equals(CUSTOM_PERM_DISPLAY_OVER) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
return Settings.canDrawOverlays(context);
|
||||
} else if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
|
||||
return pm.isIgnoringBatteryOptimizations(context.getApplicationContext().getPackageName());
|
||||
} else {
|
||||
return ContextCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_DENIED;
|
||||
}
|
||||
@ -205,7 +220,9 @@ public class PermissionsUtils {
|
||||
}
|
||||
|
||||
public static void requestPermission(Activity activity, String permission) {
|
||||
if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
if (permission.equals(CUSTOM_PERM_IGNORE_BATT_OPTIM)) {
|
||||
showRequestIgnoreBatteryOptimizationDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_LISTENER)) {
|
||||
showNotifyListenerPermissionsDialog(activity);
|
||||
} else if (permission.equals(CUSTOM_PERM_NOTIFICATION_SERVICE) && (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)) {
|
||||
showNotifyPolicyPermissionsDialog(activity);
|
||||
@ -242,6 +259,14 @@ public class PermissionsUtils {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("BatteryLife")
|
||||
private static void showRequestIgnoreBatteryOptimizationDialog(Activity activity) {
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
|
||||
intent.setData(Uri.parse("package:" + activity.getApplicationContext().getPackageName()));
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
|
||||
private static void showNotifyListenerPermissionsDialog(Activity activity) {
|
||||
new MaterialAlertDialogBuilder(activity)
|
||||
.setMessage(activity.getString(R.string.permission_notification_listener,
|
||||
|
@ -27,9 +27,9 @@ import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
public class XDatePreference extends DialogPreference {
|
||||
private int year;
|
||||
private int month;
|
||||
private int day;
|
||||
private int year = 1970;
|
||||
private int month = 1;
|
||||
private int day = 1;
|
||||
private long minDate; // TODO actually read minDate
|
||||
private long maxDate; // TODO actually read maxDate
|
||||
|
||||
|
@ -1774,6 +1774,7 @@
|
||||
<string name="devicetype_garmin_instinct_2_solar">Garmin Instinct 2 Solar</string>
|
||||
<string name="devicetype_garmin_instinct_2_soltac">Garmin Instinct 2 SolTac</string>
|
||||
<string name="devicetype_garmin_instinct_crossover">Garmin Instinct Crossover</string>
|
||||
<string name="devicetype_garmin_forerunner_45">Garmin Forerunner 45</string>
|
||||
<string name="devicetype_garmin_forerunner_55">Garmin Forerunner 55</string>
|
||||
<string name="devicetype_garmin_forerunner_165">Garmin Forerunner 165</string>
|
||||
<string name="devicetype_garmin_forerunner_235">Garmin Forerunner 235</string>
|
||||
@ -3584,4 +3585,6 @@
|
||||
<string name="battery_allow_pass_though_summary">When enabled, the battery can be charged while discharging</string>
|
||||
<string name="battery_allow_pass_through">Allow battery pass-through</string>
|
||||
<string name="canned_replies_not_empty">There should be at least one canned reply.</string>
|
||||
<string name="permission_disable_doze_title">Ignore battery optimizations</string>
|
||||
<string name="permission_disable_doze_summary">Allows running in the background unhindered by Android\'s battery optimizations</string>
|
||||
</resources>
|
||||
|
@ -15,6 +15,7 @@
|
||||
|
||||
<nodomain.freeyourgadget.gadgetbridge.util.XDatePreference
|
||||
app:iconSpaceReserved="false"
|
||||
android:defaultValue="2000-01-01"
|
||||
android:key="activity_user_date_of_birth"
|
||||
android:title="@string/activity_prefs_date_birth" />
|
||||
|
||||
|
@ -0,0 +1,19 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.messages;
|
||||
|
||||
import junit.framework.TestCase;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.garmin.communicator.CobsCoDec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
|
||||
public class GFDIMessageTest extends TestCase {
|
||||
@Test
|
||||
@Ignore("helper test for development, remove this while debugging")
|
||||
public void testDecode() {
|
||||
final CobsCoDec cobsCoDec = new CobsCoDec();
|
||||
cobsCoDec.receivedBytes(GB.hexStringToByteArray("00020c0baa1380a4bd796705196600"));
|
||||
final byte[] deCobs = cobsCoDec.retrieveMessage();
|
||||
final GFDIMessage gfdiMessage = GFDIMessage.parseIncoming(deCobs);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user