Fossil Hybrid HR: Dismiss incoming call with a quick SMS reply (#2264)

Merge branch 'master' into fossil_hr_quick_replies

Fossil Hybrid HR: Allow between 1 and 16 quick replies to be configured

Fossil Hybrid HR: Dismiss incoming call with a quick SMS reply

Co-authored-by: Arjan Schrijver <a_gadgetbridge@anymore.nl>
Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2264
Co-Authored-By: Arjan Schrijver <arjan5@noreply.codeberg.org>
Co-Committed-By: Arjan Schrijver <arjan5@noreply.codeberg.org>
This commit is contained in:
Arjan Schrijver 2021-04-27 12:51:14 +02:00 committed by Andreas Shimokawa
parent 830197168b
commit 2f37d4c839
15 changed files with 264 additions and 15 deletions

View File

@ -196,6 +196,7 @@ public class QHybridCoordinator extends AbstractDeviceCoordinator {
return new int[]{
R.xml.devicesettings_fossilhybridhr,
R.xml.devicesettings_autoremove_notifications,
R.xml.devicesettings_canned_dismisscall_16,
R.xml.devicesettings_pairingkey,
R.xml.devicesettings_custom_deviceicon
};

View File

@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
import nodomain.freeyourgadget.gadgetbridge.model.GenericItem;
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
@ -767,4 +768,10 @@ public class QHybridSupport extends QHybridBaseSupport {
return watchAdapter.onCharacteristicChanged(gatt, characteristic);
}
@Override
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
if(this.watchAdapter instanceof FossilHRWatchAdapter){
((FossilHRWatchAdapter) watchAdapter).setQuickRepliesConfiguration();
}
}
}

View File

@ -64,6 +64,7 @@ import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventFindPhone;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventNotificationControl;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HRConfigActivity;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.HybridHRActivitySampleProvider;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.NotificationHRConfiguration;
@ -114,6 +115,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationFilterPutHRRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImage;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.notification.NotificationImagePutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply.QuickReplyConfigurationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply.QuickReplyConfirmationPutRequest;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomBackgroundWidgetElement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomTextWidgetElement;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.widget.CustomWidget;
@ -139,6 +142,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
private NotificationHRConfiguration[] notificationConfigurations;
private CallSpec currentCallSpec = null;
private MusicSpec currentSpec = null;
int imageNameIndex = 0;
@ -201,6 +205,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
GB.toast(getContext().getString(R.string.fossil_hr_auth_failed), Toast.LENGTH_LONG, GB.ERROR);
setNotificationConfigurations();
setQuickRepliesConfiguration();
if (authenticated) {
setVibrationStrength();
@ -290,6 +295,27 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
queueWrite(new NotificationFilterPutHRRequest(this.notificationConfigurations, this));
}
private String[] getQuickReplies() {
ArrayList<String> configuredReplies = new ArrayList<>();
Prefs prefs = new Prefs(getDeviceSpecificPreferences());
for (int i=1; i<=16; i++) {
String quickReply = prefs.getString("canned_message_dismisscall_" + i, null);
if (quickReply != null) {
configuredReplies.add(quickReply);
}
}
return configuredReplies.toArray(new String[0]);
}
public void setQuickRepliesConfiguration() {
String[] quickReplies = getQuickReplies();
if (quickReplies.length > 0) {
NotificationImage quickReplyIcon = new NotificationImage("icMessage.icon", NotificationImage.getEncodedIconFromDrawable(getContext().getResources().getDrawable(R.drawable.ic_message_outline)), 24, 24);
queueWrite(new NotificationImagePutRequest(quickReplyIcon, this));
queueWrite(new QuickReplyConfigurationPutRequest(quickReplies, this));
}
}
private File getBackgroundFile() {
return new File(getContext().getExternalFilesDir(null), "hr_background.bin");
}
@ -994,7 +1020,15 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
@Override
public void onSetCallState(CallSpec callSpec) {
super.onSetCallState(callSpec);
queueWrite(new PlayCallNotificationRequest(StringUtils.getFirstOf(callSpec.name, callSpec.number), callSpec.command == CallSpec.CALL_INCOMING, this));
String[] quickReplies = getQuickReplies();
boolean quickRepliesEnabled = quickReplies.length > 0 && callSpec.number != null && callSpec.number.matches("^\\+(?:[0-9] ?){6,14}[0-9]$");
if (callSpec.command == CallSpec.CALL_INCOMING) {
currentCallSpec = callSpec;
queueWrite(new PlayCallNotificationRequest(StringUtils.getFirstOf(callSpec.name, callSpec.number), true, quickRepliesEnabled, this));
} else {
currentCallSpec = null;
queueWrite(new PlayCallNotificationRequest(StringUtils.getFirstOf(callSpec.name, callSpec.number), false, quickRepliesEnabled, this));
}
}
// this method is based on the one from AppMessageHandlerYWeather.java
@ -1314,6 +1348,8 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
handleCallRequest(value);
} else if (value[7] == 0x02) {
handleDeleteNotification(value);
} else if (value[7] == 0x03) {
handleQuickReplyRequest(value);
}
} else if (requestType == (byte) 0x05) {
handleMusicRequest(value);
@ -1410,7 +1446,7 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
private void handleCallRequest(byte[] value) {
boolean acceptCall = value[7] == (byte) 0x00;
queueWrite(new PlayCallNotificationRequest("", false, this));
queueWrite(new PlayCallNotificationRequest("", false, false, this));
GBDeviceEventCallControl callControlEvent = new GBDeviceEventCallControl();
callControlEvent.event = acceptCall ? GBDeviceEventCallControl.Event.START : GBDeviceEventCallControl.Event.REJECT;
@ -1418,6 +1454,25 @@ public class FossilHRWatchAdapter extends FossilWatchAdapter {
getDeviceSupport().evaluateGBDeviceEvent(callControlEvent);
}
private void handleQuickReplyRequest(byte[] value) {
if (currentCallSpec == null) {
return;
}
String[] quickReplies = getQuickReplies();
byte callId = value[3];
byte replyChoice = value[8];
if (replyChoice >= quickReplies.length) {
return;
}
GBDeviceEventNotificationControl devEvtNotificationControl = new GBDeviceEventNotificationControl();
devEvtNotificationControl.handle = callId;
devEvtNotificationControl.phoneNumber = currentCallSpec.number;
devEvtNotificationControl.reply = quickReplies[replyChoice];
devEvtNotificationControl.event = GBDeviceEventNotificationControl.Event.REPLY;
getDeviceSupport().evaluateGBDeviceEvent(devEvtNotificationControl);
queueWrite(new QuickReplyConfirmationPutRequest(callId));
}
private void handleMusicRequest(byte[] value) {
byte command = value[3];
LOG.info("got music command: " + command);

View File

@ -36,6 +36,9 @@ public class SupportedFileVersionsInfo implements DeviceInfo {
supportedFileVersions.put(handle, version);
}
// Add quick replies packet type
supportedFileVersions.put((byte) 0x13, (short) 0x0002);
// Add phone app packet type
supportedFileVersions.put((byte) 0x15, (short) 0x0003);
}

View File

@ -42,7 +42,11 @@ public class FilePutRequest extends FilePutRawRequest {
buffer.putShort(fileHandle.getHandle());
buffer.putShort(fileVersion);
buffer.putInt(0);
if (fileHandle == FileHandle.REPLY_MESSAGES) {
buffer.put(new byte[]{(byte) 0x00, (byte) 0x00, (byte) 0x0d, (byte) 0x00});
} else {
buffer.putInt(0);
}
buffer.putInt(file.length);
buffer.put(file);

View File

@ -20,6 +20,6 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.foss
public class DismissTextNotificationRequest extends PlayNotificationRequest {
public DismissTextNotificationRequest(int messageId, FossilWatchAdapter adapter) {
super(7, 2, "", "", "", messageId, adapter);
super(NotificationType.DISMISS_NOTIFICATION, 2, "", "", "", messageId, adapter);
}
}

View File

@ -0,0 +1,37 @@
/* Copyright (C) 2020-2021 Arjan Schrijver
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.notification;
public enum NotificationType {
INCOMING_CALL(1),
TEXT(2),
NOTIFICATION(3),
EMAIL(4),
CALENDAR(5),
MISSED_CALL(6),
DISMISS_NOTIFICATION(7);
private int type;
NotificationType(int type) {
this.type = type;
}
public int getType() {
return type;
}
}

View File

@ -24,8 +24,18 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.foss
public class PlayCallNotificationRequest extends PlayNotificationRequest {
private final static int MESSAGE_ID_CALL = 1;
public PlayCallNotificationRequest(String number, boolean callStart, FossilWatchAdapter adapter) {
super(callStart ? 1 : 7, callStart ? 0b00011000 : 2,
private static int notificationFlags(boolean callStart, boolean quickReplies) {
if (callStart && quickReplies) {
return 0b00111000;
} else if (callStart) {
return 0b00011000;
} else {
return 0b00000010;
}
}
public PlayCallNotificationRequest(String number, boolean callStart, boolean quickReplies, FossilWatchAdapter adapter) {
super(callStart ? NotificationType.INCOMING_CALL : NotificationType.DISMISS_NOTIFICATION, notificationFlags(callStart, quickReplies),
ByteBuffer.wrap(new byte[]{(byte) 0x80, (byte) 0x00, (byte) 0x59, (byte) 0xB7}).order(ByteOrder.LITTLE_ENDIAN).getInt(),
number, "Incoming Call", MESSAGE_ID_CALL, adapter);
}

View File

@ -28,15 +28,15 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fos
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public abstract class PlayNotificationRequest extends FilePutRequest {
public PlayNotificationRequest(int notificationType, int flags, String packageName, FossilWatchAdapter adapter) {
public PlayNotificationRequest(NotificationType notificationType, int flags, String packageName, FossilWatchAdapter adapter) {
super(FileHandle.NOTIFICATION_PLAY, createFile(notificationType, flags, packageName, packageName, packageName, getCurrentMessageId()), adapter);
}
public PlayNotificationRequest(int notificationType, int flags, String packageName, String sender, String message, int notificationId, FossilWatchAdapter adapter) {
public PlayNotificationRequest(NotificationType notificationType, int flags, String packageName, String sender, String message, int notificationId, FossilWatchAdapter adapter) {
super(FileHandle.NOTIFICATION_PLAY, createFile(notificationType, flags, packageName, sender, message, notificationId), adapter);
}
public PlayNotificationRequest(int notificationType, int flags, int packageCRC, String sender, String message, int messageId, FossilWatchAdapter adapter) {
public PlayNotificationRequest(NotificationType notificationType, int flags, int packageCRC, String sender, String message, int messageId, FossilWatchAdapter adapter) {
super(FileHandle.NOTIFICATION_PLAY, createFile(notificationType, flags, "whatever", sender, message, packageCRC, messageId), adapter);
}
@ -44,13 +44,13 @@ public abstract class PlayNotificationRequest extends FilePutRequest {
return (int) System.currentTimeMillis();
}
private static byte[] createFile(int notificationType, int flags, String packageName, String sender, String message, int messageId){
private static byte[] createFile(NotificationType notificationType, int flags, String packageName, String sender, String message, int messageId){
CRC32 crc = new CRC32();
crc.update(packageName.getBytes());
return createFile(notificationType, flags, packageName, sender, message, (int)crc.getValue(), messageId);
}
private static byte[] createFile(int notificationType, int flags, String title, String sender, String message, int packageCrc, int messageId) {
private static byte[] createFile(NotificationType notificationType, int flags, String title, String sender, String message, int packageCrc, int messageId) {
byte lengthBufferLength = (byte) 10;
byte uidLength = (byte) 4;
byte appBundleCRCLength = (byte) 4;
@ -74,7 +74,7 @@ public abstract class PlayNotificationRequest extends FilePutRequest {
mainBuffer.putShort(mainBufferLength);
mainBuffer.put(lengthBufferLength);
mainBuffer.put((byte) notificationType);
mainBuffer.put((byte) notificationType.getType());
mainBuffer.put((byte) flags);
mainBuffer.put(uidLength);
mainBuffer.put(appBundleCRCLength);

View File

@ -20,10 +20,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.foss
public class PlayTextNotificationRequest extends PlayNotificationRequest {
public PlayTextNotificationRequest(String packageName, String sender, String message, int messageId, FossilWatchAdapter adapter) {
super(3, 2, packageName, sender, message, messageId, adapter);
super(NotificationType.NOTIFICATION, 2, packageName, sender, message, messageId, adapter);
}
public PlayTextNotificationRequest(String packageName, FossilWatchAdapter adapter) {
super(3, 2, packageName, adapter);
super(NotificationType.NOTIFICATION, 2, packageName, adapter);
}
}

View File

@ -64,6 +64,8 @@ public class NotificationImage extends AssetFile {
ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
paint.setColorFilter(f);
c.drawBitmap(bitmap, 0, 0, paint);
// Increase brightness
// bitmap = changeBitmapContrastBrightness(bitmap, 1, -50);
// Return result
return bitmap;
}

View File

@ -30,6 +30,10 @@ public class NotificationImagePutRequest extends FilePutRequest {
super(FileHandle.ASSET_NOTIFICATION_IMAGES, prepareFileData(images), adapter);
}
public NotificationImagePutRequest(NotificationImage image, FossilWatchAdapter adapter) {
super(FileHandle.ASSET_REPLY_IMAGES, prepareFileData(image), adapter);
}
private static byte[] prepareFileData(NotificationImage[] images) throws IOException {
ByteArrayOutputStream stream = new ByteArrayOutputStream();

View File

@ -0,0 +1,71 @@
/* Copyright (C) 2019-2021 Arjan Schrijver
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.charset.Charset;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.adapter.fossil_hr.FossilHRWatchAdapter;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.file.FileHandle;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.file.FilePutRequest;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
public class QuickReplyConfigurationPutRequest extends FilePutRequest {
public QuickReplyConfigurationPutRequest(String[] replies, FossilHRWatchAdapter adapter) {
super(FileHandle.REPLY_MESSAGES, createFile(replies), adapter);
}
private static byte[] createFile(String[] replies) {
String[] processedReplies = new String[replies.length];
int fileLength = 0;
byte[] mysteryHeader = new byte[]{(byte) 0x02, (byte) 0x0b, (byte) 0x46, (byte) 0x00, (byte) 0x03, (byte) 0x19, (byte) 0x00, (byte) 0x00, (byte) 0x00};
Charset charsetUTF8 = Charset.forName("UTF-8");
String iconName = StringUtils.terminateNull("icMessage.icon");
byte[] iconNameBytes = iconName.getBytes(charsetUTF8);
for (int index=0; index< replies.length; index++) {
String reply = replies[index];
if (reply.length() > 50) {
reply = reply.substring(0, 50);
}
processedReplies[index] = StringUtils.terminateNull(reply);
fileLength += 8 + processedReplies[index].length() + iconNameBytes.length;
}
ByteBuffer mainBuffer = ByteBuffer.allocate(mysteryHeader.length + 4 + fileLength);
mainBuffer.order(ByteOrder.LITTLE_ENDIAN);
mainBuffer.put(mysteryHeader);
mainBuffer.putInt(fileLength);
for (int index=0; index < processedReplies.length; index++) {
byte[] msgBytes = processedReplies[index].getBytes(charsetUTF8);
mainBuffer.putShort((short) (8 + msgBytes.length + iconNameBytes.length));
mainBuffer.put((byte) 0x08);
mainBuffer.put((byte) index);
mainBuffer.putShort((short) msgBytes.length);
mainBuffer.putShort((short) iconNameBytes.length);
mainBuffer.put(msgBytes);
mainBuffer.put(iconNameBytes);
}
return mainBuffer.array();
}
}

View File

@ -0,0 +1,55 @@
/* Copyright (C) 2019-2021 Arjan Schrijver
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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil_hr.quickreply;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.devices.qhybrid.requests.fossil.FossilRequest;
public class QuickReplyConfirmationPutRequest extends FossilRequest {
/**
* Contains the bytes to confirm to the watch that a quick reply SMS has been sent.
* @param callId
*/
public QuickReplyConfirmationPutRequest(byte callId) {
this.data = new byte[]{
(byte) 0x02,
(byte) 0x04,
callId,
(byte) 0x00,
(byte) 0x00,
(byte) 0x00,
(byte) 0x03,
(byte) 0x00
};
}
@Override
public boolean isFinished() {
return true;
}
@Override
public byte[] getStartSequence() {
return null;
}
@Override
public UUID getRequestUUID() {
return UUID.fromString("3dda0006-957f-7d4a-34a6-74696673696d");
}
}

View File

@ -150,7 +150,7 @@
<string name="pref_title_canned_replies">Replies</string>
<string name="pref_title_canned_reply_suffix">Common suffix</string>
<string name="pref_title_canned_messages_dismisscall">Call Dismissal</string>
<string name="pref_title_canned_messages_set">Update on Pebble</string>
<string name="pref_title_canned_messages_set">Update on device</string>
<string name="pref_header_development">Developer options</string>
<string name="pref_title_development_miaddr">Mi Band address</string>
<string name="pref_title_pebble_settings">Pebble settings</string>