From d5de4503e9d31fbe784829601fe491a6f3c70c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Wed, 31 Dec 2025 20:54:58 +0000 Subject: [PATCH] GloryFit: SOS contact (experimental) --- .../DeviceSettingsPreferenceConst.java | 2 + .../DeviceSpecificSettingsFragment.java | 2 + .../devices/gloryfit/GloryFitCoordinator.kt | 4 + .../devices/gloryfit/GloryFitSupport.kt | 88 ++++++++++++++++--- app/src/main/res/values/strings.xml | 1 + .../res/xml/devicesettings_sos_contact.xml | 26 ++++++ 6 files changed, 110 insertions(+), 13 deletions(-) create mode 100644 app/src/main/res/xml/devicesettings_sos_contact.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index ede3f7f9af..93bfe4c32e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -305,6 +305,8 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_WORLD_CLOCKS = "pref_world_clocks"; public static final String PREF_CONTACTS = "pref_contacts"; + public static final String PREF_SOS_CONTACT_NAME = "sos_contact_name"; + public static final String PREF_SOS_CONTACT_NUMBER = "sos_contact_number"; public static final String PREF_WIDGETS = "pref_widgets"; public static final String PREF_MUSIC_MANAGEMENT = "pref_music_management"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index 89fc87f340..89ccad6b6f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -842,6 +842,8 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_MEDIA_PLAYBACK_MODE); addPreferenceHandlerFor(PREF_SHOKZ_EQUALIZER_BLUETOOTH); addPreferenceHandlerFor(PREF_SHOKZ_EQUALIZER_MP3); + addPreferenceHandlerFor(PREF_SOS_CONTACT_NAME); + addPreferenceHandlerFor(PREF_SOS_CONTACT_NUMBER); addPreferenceHandlerFor(PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION); addPreferenceHandlerFor(PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt index ecc2a67d80..42265488d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/gloryfit/GloryFitCoordinator.kt @@ -188,6 +188,10 @@ abstract class GloryFitCoordinator : AbstractBLEDeviceCoordinator() { ) } + override fun getSupportedDeviceSpecificExperimentalSettings(device: GBDevice): IntArray? { + return intArrayOf(R.xml.devicesettings_sos_contact) + } + override fun getDeviceSpecificSettings(device: GBDevice): DeviceSpecificSettings { val deviceSpecificSettings = DeviceSpecificSettings() diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt index 2ed635d0de..5edcb642c7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/gloryfit/GloryFitSupport.kt @@ -49,6 +49,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.wena3.protocol.packets.notification.defines.VibrationKind import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol import nodomain.freeyourgadget.gadgetbridge.util.MediaManager +import nodomain.freeyourgadget.gadgetbridge.util.kotlin.coerceIn import org.apache.commons.lang3.ArrayUtils import org.apache.commons.lang3.StringUtils import org.slf4j.Logger @@ -59,7 +60,6 @@ import java.util.Calendar import java.util.GregorianCalendar import java.util.Locale import java.util.UUID -import nodomain.freeyourgadget.gadgetbridge.util.kotlin.coerceIn class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { init { @@ -126,6 +126,7 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { } setEnableCallRejection(builder) setEnableSmsReply(builder) + setSosContact(builder) // FIXME this is probably too early builder.setDeviceState(GBDevice.State.INITIALIZED) @@ -414,6 +415,11 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { setSpo2Measurement(builder) } + DeviceSettingsPreferenceConst.PREF_SOS_CONTACT_NAME, + DeviceSettingsPreferenceConst.PREF_SOS_CONTACT_NUMBER -> { + setSosContact(builder) + } + DeviceSettingsPreferenceConst.PREF_ENABLE_SMS_QUICK_REPLY -> { setEnableSmsReply(builder) } @@ -473,21 +479,12 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { val encodedContacts: MutableList = mutableListOf() for (contact in contacts) { - val numberBytes = contact.number.toByteArray() - val nameBytes = nodomain.freeyourgadget.gadgetbridge.util.StringUtils.truncateUtf16BE(contact.name, 20) + val numberNameBytes = encodeContact(contact.name, contact.number) ?: continue - if (numberBytes.size > 15) { - LOG.warn("Contact number length {} too long, skipping", contact.number.length) - continue - } - - val buf = ByteBuffer.allocate(4 + numberBytes.size + nameBytes.size).order(ByteOrder.BIG_ENDIAN) + val buf = ByteBuffer.allocate(2 + numberNameBytes.size).order(ByteOrder.BIG_ENDIAN) buf.put(CMD_CONTACTS) buf.put(CONTACTS_ADD) - buf.put(numberBytes.size.toByte()) - buf.put(nameBytes.size.toByte()) - buf.put(numberBytes) - buf.put(nameBytes) + buf.put(numberNameBytes) encodedContacts.add(buf.array()) } @@ -558,6 +555,24 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { builder.queue() } + private fun encodeContact(name: String, number: String): ByteArray? { + val numberBytes = number.toByteArray() + val nameBytes = nodomain.freeyourgadget.gadgetbridge.util.StringUtils.truncateUtf16BE(name, 20) + + if (numberBytes.size > 15) { + LOG.warn("Contact number length {} too long, skipping", number.length) + return null + } + + val buf = ByteBuffer.allocate(2 + numberBytes.size + nameBytes.size).order(ByteOrder.BIG_ENDIAN) + buf.put(numberBytes.size.toByte()) + buf.put(nameBytes.size.toByte()) + buf.put(numberBytes) + buf.put(nameBytes) + + return buf.array() + } + override fun onSetAlarms(alarms: ArrayList) { val builder = createTransactionBuilder("set alarms") @@ -1181,6 +1196,50 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { ) } + private fun setSosContact(builder: TransactionBuilder) { + if (!GBApplication.getPrefs().experimentalSettings()) { + LOG.warn("Experimental settings disabled, ignoring SOS contact") + return + } + + val name = devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_SOS_CONTACT_NAME, "") + val number = devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_SOS_CONTACT_NUMBER, "") + + if (name.isBlank() || number.isBlank()) { + LOG.warn("SOS contact number or name is blank, ignoring") + return + } + + val numberNameBytes = encodeContact(name, number) ?: return + + LOG.debug("Setting SOS contact name = {}, number = {}", name, number) + + // Clear + builder.write( + UUID_CHARACTERISTIC_GLORYFIT_DATA_WRITE, + *byteArrayOf(CMD_CONTACTS, CONTACT_SOS_CLEAR) + ) + + // Set - 37 ac 01 37 ac 06 10 35 3536363636004a0061006e006500200044006f006500000000000000000000000000 + builder.write( + UUID_CHARACTERISTIC_GLORYFIT_DATA_WRITE, + *ByteBuffer.allocate(244).order(ByteOrder.LITTLE_ENDIAN).apply { + put(CMD_CONTACTS) + put(CONTACT_SOS_SET) + put(0x01.toByte()) + put(CMD_CONTACTS) + put(CONTACT_SOS_SET) + put(numberNameBytes) + }.array() + ) + + // End - 37 ad fd c1 + builder.write( + UUID_CHARACTERISTIC_GLORYFIT_DATA_WRITE, + *byteArrayOf(CMD_CONTACTS, CONTACT_SOS_END, 0xfd.toByte(), 0xc1.toByte()) + ) + } + private fun rearmBatteryStateRequestTimer() { mHandler.removeCallbacks(mBatteryStateRequestRunnable) val devicePrefs = getDevicePrefs() @@ -1313,6 +1372,9 @@ class GloryFitSupport() : AbstractBTLESingleDeviceSupport(LOG) { const val CONTACTS_START: Byte = 0xfa.toByte() const val CONTACTS_ADD: Byte = 0xfb.toByte() const val CONTACTS_END: Byte = 0xfc.toByte() + const val CONTACT_SOS_CLEAR: Byte = 0xae.toByte() + const val CONTACT_SOS_SET: Byte = 0xac.toByte() + const val CONTACT_SOS_END: Byte = 0xad.toByte() const val CMD_FACTORY_RESET: Byte = 0xad.toByte() } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 808e29f86c..303ce8c4da 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -4503,4 +4503,5 @@ Allow battery bypass Search for app updates Automatically search for watch app and watchface updates when opening the app manager + SOS contact diff --git a/app/src/main/res/xml/devicesettings_sos_contact.xml b/app/src/main/res/xml/devicesettings_sos_contact.xml new file mode 100644 index 0000000000..389299e9eb --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sos_contact.xml @@ -0,0 +1,26 @@ + + + + + + + + + + +