diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml
index 62cfa2e832..4366d93e72 100644
--- a/.idea/dictionaries/t.xml
+++ b/.idea/dictionaries/t.xml
@@ -147,6 +147,7 @@
sevostyanova
shahrabani
shimokawa
+ shokz
skype
slezak
sophanimus
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 3a53b96481..b5888778c1 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
@@ -516,6 +516,13 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SONY_ADAPTIVE_VOLUME_CONTROL = "pref_adaptive_volume_control";
public static final String PREF_SONY_WIDE_AREA_TAP = "pref_wide_area_tap";
+ public static final String PREF_MEDIA_SOURCE = "pref_media_source";
+ public static final String PREF_MEDIA_PLAYBACK_MODE = "pref_media_playback_mode";
+ public static final String PREF_SHOKZ_EQUALIZER_BLUETOOTH = "pref_shokz_equalizer_bluetooth";
+ public static final String PREF_SHOKZ_EQUALIZER_MP3 = "pref_shokz_equalizer_mp3";
+ public static final String PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION = "shokz_controls_long_press_multi_function";
+ public static final String PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN = "shokz_controls_simultaneous_volume_up_down";
+
public static final String PREF_OVERRIDE_FEATURES_ENABLED = "override_features_enabled";
public static final String PREF_OVERRIDE_FEATURES_LIST = "override_features_list";
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettings.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettings.java
index 5cd514f96a..f42f19d7d2 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettings.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettings.java
@@ -24,6 +24,7 @@ import androidx.annotation.Nullable;
import androidx.annotation.XmlRes;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -47,6 +48,9 @@ public class DeviceSpecificSettings implements Parcelable {
private final List rootScreens = new ArrayList<>();
private final Map> subScreens = new LinkedHashMap<>();
+ /// Settings that should only be available while connected
+ private final List connectedPreferences = new ArrayList<>();
+
public DeviceSpecificSettings() {
}
@@ -115,6 +119,7 @@ public class DeviceSpecificSettings implements Parcelable {
Objects.requireNonNull(subScreens.get(e.getKey())).add(screen);
}
}
+ connectedPreferences.addAll(deviceSpecificSettings.connectedPreferences);
}
public List getRootScreens() {
@@ -126,6 +131,14 @@ public class DeviceSpecificSettings implements Parcelable {
return subScreens.get(key);
}
+ public void addConnectedPreferences(final String... preferences) {
+ connectedPreferences.addAll(Arrays.asList(preferences));
+ }
+
+ public List getConnectedPreferences() {
+ return connectedPreferences;
+ }
+
public List getAllNonRootScreens() {
final List allScreens = new ArrayList<>();
for (final List screens : subScreens.values()) {
@@ -143,7 +156,7 @@ public class DeviceSpecificSettings implements Parcelable {
return allScreens;
}
- public static final Creator CREATOR = new Creator() {
+ public static final Creator CREATOR = new Creator<>() {
@Override
public DeviceSpecificSettings createFromParcel(final Parcel in) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
@@ -161,6 +174,7 @@ public class DeviceSpecificSettings implements Parcelable {
}
deviceSpecificSettings.addSubScreen(key, screens);
}
+ in.readStringList(deviceSpecificSettings.connectedPreferences);
return deviceSpecificSettings;
}
@@ -189,5 +203,6 @@ public class DeviceSpecificSettings implements Parcelable {
dest.writeInt(s);
}
}
+ dest.writeStringList(connectedPreferences);
}
}
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 eb6e0005f1..19609ca622 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
@@ -103,6 +103,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
static final String FRAGMENT_TAG = "DEVICE_SPECIFIC_SETTINGS_FRAGMENT";
+ private DeviceSpecificSettings deviceSpecificSettings;
private DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer;
private GBDevice device;
@@ -142,6 +143,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
LOG.debug("{} changed, notifying customizer", changedDevice);
deviceSpecificSettingsCustomizer.onDeviceChanged(DeviceSpecificSettingsFragment.this);
}
+ reloadEnabledPreferences();
}
}
}
@@ -174,7 +176,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
return;
}
String settingsFileSuffix = arguments.getString("settingsFileSuffix", null);
- DeviceSpecificSettings deviceSpecificSettings = arguments.getParcelable("deviceSpecificSettings");
+ this.deviceSpecificSettings = arguments.getParcelable("deviceSpecificSettings");
this.deviceSpecificSettingsCustomizer = arguments.getParcelable("deviceSpecificSettingsCustomizer");
this.device = arguments.getParcelable("device");
@@ -241,6 +243,19 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
}
setChangeListener(rootKey);
+
+ reloadEnabledPreferences();
+ }
+
+ private void reloadEnabledPreferences() {
+ if (deviceSpecificSettings != null) {
+ for (String connectedPreference : deviceSpecificSettings.getConnectedPreferences()) {
+ final Preference pref = findPreference(connectedPreference);
+ if (pref != null) {
+ pref.setEnabled(device.isInitialized());
+ }
+ }
+ }
}
private void addDynamicSettings(final String rootKey) {
@@ -811,6 +826,14 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL);
addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP);
+ addPreferenceHandlerFor(PREF_MEDIA_SOURCE);
+ addPreferenceHandlerFor(PREF_MEDIA_PLAYBACK_MODE);
+ addPreferenceHandlerFor(PREF_SHOKZ_EQUALIZER_BLUETOOTH);
+ addPreferenceHandlerFor(PREF_SHOKZ_EQUALIZER_MP3);
+
+ addPreferenceHandlerFor(PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION);
+ addPreferenceHandlerFor(PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN);
+
addPreferenceHandlerFor(PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL);
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java
index 18e2655374..09ad624c83 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java
@@ -16,6 +16,7 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
+import androidx.annotation.NonNull;
import androidx.annotation.XmlRes;
import nodomain.freeyourgadget.gadgetbridge.R;
@@ -50,6 +51,7 @@ public enum DeviceSpecificSettingsScreen {
this.xml = xml;
}
+ @NonNull
public String getKey() {
return key;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEvent.java
index 5254b6739a..088136135d 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEvent.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEvent.java
@@ -50,9 +50,6 @@ public abstract class GBDeviceEvent {
/**
* Helper method to run specific actions configured in the device preferences, upon wear state
* or awake/asleep events.
- *
- * @param actions
- * @param message
*/
protected void handleDeviceAction(final Context context, final GBDevice device, Set actions, String message, String broadcastPackage) {
if (actions.isEmpty()) {
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventUpdatePreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventUpdatePreferences.java
index 337b5de3d0..9f5dbb1311 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventUpdatePreferences.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/deviceevents/GBDeviceEventUpdatePreferences.java
@@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
import android.content.Context;
import android.content.SharedPreferences;
+import androidx.annotation.NonNull;
+
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -34,6 +36,18 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
public final Map preferences;
+ @NonNull
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder(super.toString());
+ sb.append("preferences: ");
+ for (Map.Entry e : preferences.entrySet()) {
+ sb.append(e.getKey()).append("=").append(e.getValue()).append(", ");
+ }
+ sb.setLength(sb.length() - 2);
+ return sb.toString();
+ }
+
public GBDeviceEventUpdatePreferences() {
this.preferences = new HashMap<>();
}
@@ -73,10 +87,11 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
private void update(final SharedPreferences prefs) {
final SharedPreferences.Editor editor = prefs.edit();
- for (String key : preferences.keySet()) {
- final Object value = preferences.get(key);
+ for (Map.Entry e : preferences.entrySet()) {
+ final String key = e.getKey();
+ final Object value = e.getValue();
- LOG.debug("Updating {} = {}", key, value);
+ LOG.trace("Updating {} = {}", key, value);
if (value == null) {
editor.remove(key);
@@ -93,6 +108,7 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
} else if (value instanceof Long) {
editor.putLong(key, (Long) value);
} else if (value instanceof Set) {
+ //noinspection unchecked,rawtypes
editor.putStringSet(key, (Set) value);
} else {
LOG.warn("Unknown preference value type {} for {}", value.getClass(), key);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzCoordinator.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzCoordinator.kt
new file mode 100644
index 0000000000..6b56f44d97
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzCoordinator.kt
@@ -0,0 +1,70 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.shokz
+
+import nodomain.freeyourgadget.gadgetbridge.R
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen
+import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
+import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport
+import nodomain.freeyourgadget.gadgetbridge.service.devices.shokz.ShokzSupport
+
+abstract class ShokzCoordinator : AbstractBLClassicDeviceCoordinator() {
+ override fun getManufacturer(): String? {
+ return "Shokz"
+ }
+
+ override fun getDeviceSupportClass(device: GBDevice): Class {
+ return ShokzSupport::class.java
+ }
+
+ override fun suggestUnbindBeforePair(): Boolean {
+ return false
+ }
+
+ override fun getDefaultIconResource(): Int {
+ // TODO dedicated icon
+ return R.drawable.ic_device_headphones
+ }
+
+ override fun getSupportedLanguageSettings(device: GBDevice): Array? {
+ return arrayOf(
+ "en",
+ "zh",
+ "ja",
+ "ko",
+ )
+ }
+
+ override fun getDeviceSpecificSettings(device: GBDevice): DeviceSpecificSettings {
+ val settings = DeviceSpecificSettings()
+
+ settings.addRootScreen(R.xml.devicesettings_media_source)
+ settings.addRootScreen(R.xml.devicesettings_shokz_equalizer)
+ settings.addRootScreen(R.xml.devicesettings_playback_mode)
+
+ settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS)
+ settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_shokz_controls)
+
+ settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS)
+ settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones)
+
+ settings.addConnectedPreferences(
+ DeviceSettingsPreferenceConst.PREF_LANGUAGE,
+ DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_MP3,
+ DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE,
+ DeviceSpecificSettingsScreen.TOUCH_OPTIONS.key,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN,
+ )
+
+ return settings
+ }
+
+ override fun getDeviceSpecificSettingsCustomizer(device: GBDevice): DeviceSpecificSettingsCustomizer? {
+ return ShokzSettingsCustomizer()
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzOpenSwimProCoordinator.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzOpenSwimProCoordinator.kt
new file mode 100644
index 0000000000..2097b56df7
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzOpenSwimProCoordinator.kt
@@ -0,0 +1,14 @@
+package nodomain.freeyourgadget.gadgetbridge.devices.shokz
+
+import nodomain.freeyourgadget.gadgetbridge.R
+import java.util.regex.Pattern
+
+class ShokzOpenSwimProCoordinator: ShokzCoordinator() {
+ override fun getSupportedDeviceName(): Pattern? {
+ return Pattern.compile("^OpenSwim Pro by Shokz$")
+ }
+
+ override fun getDeviceNameResource(): Int {
+ return R.string.devicetype_shokz_openswim_pro
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzSettingsCustomizer.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzSettingsCustomizer.kt
new file mode 100644
index 0000000000..03ebe7dbed
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/shokz/ShokzSettingsCustomizer.kt
@@ -0,0 +1,75 @@
+/* Copyright (C) 2025 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 . */
+package nodomain.freeyourgadget.gadgetbridge.devices.shokz
+
+import androidx.preference.Preference
+import kotlinx.parcelize.Parcelize
+import nodomain.freeyourgadget.gadgetbridge.GBApplication
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler
+import nodomain.freeyourgadget.gadgetbridge.service.devices.shokz.ShokzMediaSource
+import nodomain.freeyourgadget.gadgetbridge.util.Prefs
+import org.slf4j.LoggerFactory
+
+@Parcelize
+class ShokzSettingsCustomizer : DeviceSpecificSettingsCustomizer {
+ override fun onPreferenceChange(
+ preference: Preference,
+ handler: DeviceSpecificSettingsHandler
+ ) {
+ }
+
+ override fun onDeviceChanged(handler: DeviceSpecificSettingsHandler) {
+ hideEqualizers(handler, GBApplication.getDevicePrefs(handler.device))
+ }
+
+ override fun customizeSettings(
+ handler: DeviceSpecificSettingsHandler,
+ genericDevicePrefs: Prefs,
+ rootKey: String?
+ ) {
+ hideEqualizers(handler, genericDevicePrefs)
+ }
+
+ private fun hideEqualizers(
+ handler: DeviceSpecificSettingsHandler,
+ devicePrefs: Prefs
+ ) {
+ val mediaSourceValue: String =
+ devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE, "")
+ val mediaSource = ShokzMediaSource.fromPreference(mediaSourceValue) ?: run {
+ LOG.warn("Unknown media source {}", mediaSourceValue)
+ null
+ }
+
+ handler.findPreference(DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH)?.isVisible =
+ mediaSource == ShokzMediaSource.BLUETOOTH
+ handler.findPreference(DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_MP3)?.isVisible =
+ mediaSource == ShokzMediaSource.MP3
+ handler.findPreference(DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE)?.isVisible =
+ mediaSource == ShokzMediaSource.MP3
+ }
+
+ override fun getPreferenceKeysWithSummary(): Set {
+ return setOf()
+ }
+
+ companion object {
+ private val LOG = LoggerFactory.getLogger(ShokzSettingsCustomizer::class.java)
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
index df8851dc61..70b0398bc3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java
@@ -315,6 +315,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.redmibuds.RedmiBuds6ProCoord
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator;
+import nodomain.freeyourgadget.gadgetbridge.devices.shokz.ShokzOpenSwimProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.smaq2oss.SMAQ2OSSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soflow.SoFlowCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyLinkBudsCoordinator;
@@ -662,6 +663,7 @@ public enum DeviceType {
SONY_WF_C700N(SonyWFC700NCoordinator.class),
SONY_WF_C710N(SonyWFC710NCoordinator.class),
PIXEL_BUDS_A(PixelBudsACoordinator.class),
+ SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneBTBRDeviceSupport.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneBTBRDeviceSupport.kt
new file mode 100644
index 0000000000..7ae8e060a0
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/AbstractHeadphoneBTBRDeviceSupport.kt
@@ -0,0 +1,41 @@
+package nodomain.freeyourgadget.gadgetbridge.service
+
+import android.bluetooth.BluetoothAdapter
+import android.content.Context
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
+import nodomain.freeyourgadget.gadgetbridge.model.CallSpec
+import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec
+import nodomain.freeyourgadget.gadgetbridge.service.btbr.AbstractBTBRDeviceSupport
+import org.slf4j.Logger
+
+abstract class AbstractHeadphoneBTBRDeviceSupport(logger: Logger, bufferSize: Int = 1024) :
+ AbstractBTBRDeviceSupport(logger, bufferSize), HeadphoneHelper.Callback {
+
+ private lateinit var headphoneHelper: HeadphoneHelper
+
+ override fun setContext(gbDevice: GBDevice, btAdapter: BluetoothAdapter, context: Context) {
+ super.setContext(gbDevice, btAdapter, context)
+ headphoneHelper = HeadphoneHelper(getContext(), gbDevice, this)
+ }
+
+ override fun dispose() {
+ synchronized(ConnectionMonitor) {
+ headphoneHelper.dispose()
+ super.dispose()
+ }
+ }
+
+ override fun onSetCallState(callSpec: CallSpec) {
+ headphoneHelper.onSetCallState(callSpec)
+ }
+
+ override fun onNotification(notificationSpec: NotificationSpec) {
+ headphoneHelper.onNotification(notificationSpec)
+ }
+
+ override fun onSendConfiguration(config: String) {
+ if (!headphoneHelper.onSendConfiguration(config)) {
+ super.onSendConfiguration(config)
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java
index fcffc4d2fd..1b6d04f363 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/AbstractBTBRDeviceSupport.java
@@ -16,6 +16,10 @@
along with this program. If not, see . */
package nodomain.freeyourgadget.gadgetbridge.service.btbr;
+import android.annotation.SuppressLint;
+import android.bluetooth.BluetoothDevice;
+import android.os.ParcelUuid;
+
import org.slf4j.Logger;
import java.io.IOException;
@@ -24,6 +28,7 @@ import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.Logging;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
+import nodomain.freeyourgadget.gadgetbridge.service.btle.BleNamesResolver;
/**
* Abstract base class for devices connected through a serial protocol, like RFCOMM BT or TCP socket.
@@ -42,11 +47,15 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
private BtBRQueue mQueue;
private UUID mSupportedService = null;
- private int mBufferSize = 1024;
+ private final int mBufferSize;
private final Logger logger;
- public AbstractBTBRDeviceSupport(Logger logger) {
+ /**
+ * @param bufferSize should be larger than the maximum expected message side, or messages might be lost.
+ */
+ public AbstractBTBRDeviceSupport(Logger logger, final int bufferSize) {
this.logger = logger;
+ this.mBufferSize = bufferSize;
if (logger == null) {
throw new IllegalArgumentException("logger must not be null");
}
@@ -57,6 +66,21 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
synchronized (ConnectionMonitor) {
final UUID supportedService = getSupportedService();
if (supportedService == null) {
+ // Before throwing the exception, list the available UUIDs
+ final BluetoothDevice btDevice = getBluetoothAdapter().getRemoteDevice(gbDevice.getAddress());
+ @SuppressLint("MissingPermission") final ParcelUuid[] uuids = btDevice.getUuids();
+ if (uuids == null || uuids.length == 0) {
+ logger.warn("Device provided no UUIDs to connect to: {}", gbDevice);
+ } else {
+ for (ParcelUuid uuid : uuids) {
+ logger.debug(
+ "discovered service: {}: {}",
+ BleNamesResolver.resolveServiceName(uuid.toString()),
+ uuid
+ );
+ }
+ }
+
throw new NullPointerException("No supported service UUID specified");
}
@@ -136,10 +160,6 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
return mSupportedService;
}
- protected void setBufferSize(int bufferSize) {
- mBufferSize = bufferSize;
- }
-
protected int getBufferSize() {
return mBufferSize;
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/SocketCallback.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/SocketCallback.java
index e8aeaae5eb..a5e23b2bb0 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/SocketCallback.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/SocketCallback.java
@@ -26,8 +26,6 @@ public interface SocketCallback {
/**
* Read data from InputStream of BluetoothSocket
- *
- * @param data
*/
void onSocketRead(byte[] data);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/actions/WriteAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/actions/WriteAction.java
index 1125ea41ca..3c1f724ec1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/actions/WriteAction.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btbr/actions/WriteAction.java
@@ -30,7 +30,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.btbr.BtBRAction;
/**
* Invokes a write operation on a given socket.
* The result status will be made available asynchronously through the
- * {@link SocketCallback}
+ * {@link nodomain.freeyourgadget.gadgetbridge.service.btbr.SocketCallback}
*/
public class WriteAction extends BtBRAction {
private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class);
@@ -59,7 +59,7 @@ public class WriteAction extends BtBRAction {
protected boolean writeValue(byte[] value) {
if (LOG.isDebugEnabled()) {
- LOG.debug("writing to socket: " + Logging.formatBytes(value));
+ LOG.debug("writing to socket: {}", Logging.formatBytes(value));
}
try {
mOutputStream.write(value);
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java
index c9fd689b6d..7ed89e9116 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btle/BleNamesResolver.java
@@ -485,6 +485,11 @@ public class BleNamesResolver {
mServices.put("0000185a-0000-1000-8000-00805f9b34fb", "Industrial Measurement Device");
mServices.put("0000185b-0000-1000-8000-00805f9b34fb", "Ranging");
+ mServices.put("00001101-0000-1000-8000-00805f9b34fb", "SPP (Serial Port Profile)");
+ mServices.put("0000111e-0000-1000-8000-00805f9b34fb", "HFP HS (Hands-Free Profile)");
+ mServices.put("0000110b-0000-1000-8000-00805f9b34fb", "A2DP Sink");
+ mServices.put("0000110e-0000-1000-8000-00805f9b34fb", "AVRCP Remote");
+
mServices.put("0000fdab-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Proximity Unlock Service)");
mServices.put("0000fe95-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Wear Service)");
mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi Service)");
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/aawireless/AAWirelessSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/aawireless/AAWirelessSupport.java
index 2923d9e152..072f548743 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/aawireless/AAWirelessSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/aawireless/AAWirelessSupport.java
@@ -70,9 +70,8 @@ public class AAWirelessSupport extends AbstractBTBRDeviceSupport {
private final BroadcastReceiver commandReceiver = new AAWirelessCommandReceiver();
public AAWirelessSupport() {
- super(LOG);
+ super(LOG, MAX_MTU);
addSupportedService(UUID_SERVICE_AAWIRELESS);
- setBufferSize(MAX_MTU);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsBtbrSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsBtbrSupport.java
index c6479d40cf..7c6c9a7af3 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsBtbrSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/zeppos/ZeppOsBtbrSupport.java
@@ -101,9 +101,8 @@ public class ZeppOsBtbrSupport extends AbstractBTBRDeviceSupport implements Zepp
private long lastWrite = -1;
public ZeppOsBtbrSupport() {
- super(LOG);
+ super(LOG, MAX_MTU);
addSupportedService(HuamiService.UUID_BT_SERIAL_SERVICE);
- setBufferSize(MAX_MTU);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HonorBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HonorBRSupport.java
index 3baae57fe5..2ad15b0434 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HonorBRSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HonorBRSupport.java
@@ -17,17 +17,11 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
public class HonorBRSupport extends HuaweiBRSupport {
- private static final Logger LOG = LoggerFactory.getLogger(HonorBRSupport.class);
public HonorBRSupport() {
super();
addSupportedService(HuaweiConstants.UUID_SERVICE_HONOR_SDP);
- setBufferSize(1032);
}
-
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
index dfc5348e27..487d9baffc 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiBRSupport.java
@@ -49,9 +49,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
private final HuaweiSupportProvider supportProvider;
public HuaweiBRSupport() {
- super(LOG);
+ super(LOG, 1032);
addSupportedService(HuaweiConstants.UUID_SERVICE_HUAWEI_SDP);
- setBufferSize(1032);
supportProvider = new HuaweiSupportProvider(this);
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java
index e7b3db6a3c..e77c307947 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huawei/HuaweiFreebudsSupport.java
@@ -56,7 +56,6 @@ public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneH
public HuaweiFreebudsSupport() {
super();
addSupportedService(UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"));
- setBufferSize(1032);
}
@Override
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzArguments.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzArguments.kt
new file mode 100644
index 0000000000..f1ef4b5ce1
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzArguments.kt
@@ -0,0 +1,74 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
+
+/// Naming: LONG_PRESS_MULTI_FUNCTION __ SIMULTANEOUS_VOLUME_UP_DOWN
+enum class ShokzControls(val code: Int) {
+ ASSISTANT__MEDIA_SOURCE(0x01),
+ MEDIA_SOURCE__ASSISTANT(0x02),
+ MEDIA_SOURCE__MEDIA_SOURCE(0x03),
+ ASSISTANT__ASSISTANT(0x04),
+ ;
+
+ companion object {
+ fun fromPreference(value: String): ShokzControls? = entries.find { it.name == value.uppercase() }
+ fun fromCode(code: Int): ShokzControls? = entries.find { it.code == code }
+ }
+}
+
+enum class ShokzEqualizer(val code: Int) {
+ STANDARD(0x01),
+ VOCAL(0x02),
+ SWIMMING(0x07),
+ ;
+
+ companion object {
+ fun fromPreference(value: String): ShokzEqualizer? = entries.find { it.name == value.uppercase() }
+ fun fromCode(code: Int): ShokzEqualizer? = entries.find { it.code == code }
+ }
+}
+
+enum class ShokzMp3PlaybackMode(val code: Int) {
+ NORMAL(0x00),
+ SHUFFLE(0x01),
+ REPEAT(0x02),
+ ;
+
+ companion object {
+ fun fromPreference(value: String): ShokzMp3PlaybackMode? = entries.find { it.name == value.uppercase() }
+ fun fromCode(code: Int): ShokzMp3PlaybackMode? = entries.find { it.code == code }
+ }
+}
+
+enum class ShokzMediaSource(val code: Int) {
+ BLUETOOTH(0x00),
+ MP3(0x01),
+ ;
+
+ companion object {
+ fun fromPreference(value: String): ShokzMediaSource? = entries.find { it.name == value.uppercase() }
+ fun fromCode(code: Int): ShokzMediaSource? = entries.find { it.code == code }
+ }
+}
+
+enum class ShokzPlaybackStatus(val code: Int) {
+ PAUSED(0x00),
+ PLAYING(0xff),
+ ;
+
+ companion object {
+ fun fromPreference(value: String): ShokzPlaybackStatus? = entries.find { it.name == value.uppercase() }
+ fun fromCode(code: Int): ShokzPlaybackStatus? = entries.find { it.code == code }
+ }
+}
+
+enum class ShokzLanguage(val locale: String, val code: Int) {
+ ENGLISH("en", 0x00),
+ CHINESE("zh", 0x01),
+ JAPANESE("ja", 0x02),
+ KOREAN("ko", 0x03),
+ ;
+
+ companion object {
+ fun fromLocale(locale: String): ShokzLanguage? = entries.find { it.locale == locale }
+ fun fromCode(code: Int): ShokzLanguage? = entries.find { it.code == code }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzCommand.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzCommand.kt
new file mode 100644
index 0000000000..7106283f3d
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzCommand.kt
@@ -0,0 +1,75 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
+
+enum class ShokzCommand(
+ val group: Int,
+ val code: Int
+) {
+ BATTERY_GET(0x02, 0x0003),
+ BATTERY_RET(0x02, 0x8003),
+ CONTROLS_ACK(0x01, 0x8024),
+ CONTROLS_GET(0x02, 0x001b),
+ CONTROLS_RET(0x02, 0x801b),
+ CONTROLS_SET(0x01, 0x0024),
+ BUTTON_NEXT_ACK(0x01, 0x8008),
+ BUTTON_NEXT(0x01, 0x0008),
+ BUTTON_PAUSE_ACK(0x01, 0x8004),
+ BUTTON_PAUSE(0x01, 0x0004),
+ BUTTON_PLAY_ACK(0x01, 0x8003),
+ BUTTON_PLAY(0x01, 0x0003),
+ BUTTON_PREV_ACK(0x01, 0x8007),
+ BUTTON_PREV(0x01, 0x0007),
+ EQ_UNK_ACK_1(0x01, 0x800e),
+ EQUALIZER_ACK(0x03, 0x0009),
+ EQUALIZER_GET(0x02, 0x0008),
+ EQUALIZER_RET(0x02, 0x8008),
+ EQUALIZER_SET(0x01, 0x000e),
+ FIRMWARE_GET(0x02, 0x0002),
+ FIRMWARE_RET(0x02, 0x8002),
+ LANGUAGE_ACK(0x01, 0x8002),
+ LANGUAGE_GET(0x02, 0x0006),
+ LANGUAGE_RET(0x02, 0x8006),
+ LANGUAGE_SET(0x01, 0x0002),
+ MEDIA_SOURCE_ACK(0x01, 0x8025),
+ MEDIA_SOURCE_NOTIFY(0x03, 0x000c),
+ MEDIA_SOURCE_SET(0x01, 0x0025),
+ MODE_BLUETOOTH_UNK_0(0x03, 0x000d),
+ MEDIA_SOURCE_GET(0x02, 0x001c),
+ MEDIA_SOURCE_RET(0x02, 0x801c),
+ MODE_MP3_ACK(0x02, 0x801e),
+ MODE_MP3_SET(0x02, 0x001e),
+ MP3_PLAYBACK_MODE_ACK(0x01, 0x8026),
+ MP3_PLAYBACK_MODE_GET(0x02, 0x001d),
+ MP3_PLAYBACK_MODE_RET(0x02, 0x801d),
+ MP3_PLAYBACK_MODE_SET(0x01, 0x0026),
+ MULTIPOINT_CONNECT_ACK(0x01, 0x800b),
+ MULTIPOINT_CONNECT_REQ(0x01, 0x000b),
+ MULTIPOINT_DEVICE_CONNECTION_NOTIFY(0x03, 0x000b),
+ MULTIPOINT_DEVICES_GET(0x02, 0x0004),
+ MULTIPOINT_DEVICES_RET(0x02, 0x8004),
+ MULTIPOINT_DISCONNECT_ACK(0x01, 0x800c),
+ MULTIPOINT_DISCONNECT_REQ(0x01, 0x000c),
+ MULTIPOINT_OFF_ACK(0x01, 0x8010),
+ MULTIPOINT_OFF(0x01, 0x0010),
+ MULTIPOINT_ON_ACK(0x01, 0x8011),
+ MULTIPOINT_ON(0x01, 0x0011),
+ MULTIPOINT_PAIR_SECOND_FINISH(0x01, 0x0023),
+ MULTIPOINT_START_PAIR_ACK(0x01, 0x8022),
+ MULTIPOINT_START_PAIR_REQ(0x01, 0x001d),
+ PLAYBACK_STATUS_GET(0x02, 0x0005),
+ PLAYBACK_STATUS_NOTIFY(0x03, 0x0006),
+ PLAYBACK_STATUS_RET(0x02, 0x8005),
+ MULTIPOINT_GET(0x02, 0x0010),
+ MULTIPOINT_RET(0x02, 0x8010),
+ VOLUME_ACK(0x01, 0x800a),
+ VOLUME_GET(0x02, 0x0007),
+ VOLUME_NOTIFY(0x03, 0x0005),
+ VOLUME_RET(0x02, 0x8007),
+ VOLUME_SET(0x01, 0x000a),
+ ;
+
+ companion object {
+ fun getCommand(group: Int, code: Int): ShokzCommand? = entries.find {
+ it.group == group && it.code == code
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzMessage.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzMessage.kt
new file mode 100644
index 0000000000..68b4c9c12a
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzMessage.kt
@@ -0,0 +1,24 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
+
+data class ShokzMessage(
+ val command: ShokzCommand,
+ val args: ByteArray = byteArrayOf()
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ShokzMessage
+
+ if (command != other.command) return false
+ if (!args.contentEquals(other.args)) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = command.hashCode()
+ result = 31 * result + args.contentHashCode()
+ return result
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupport.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupport.kt
new file mode 100644
index 0000000000..eb46d4a24c
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupport.kt
@@ -0,0 +1,626 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
+
+import android.os.Handler
+import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences
+import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo
+import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice
+import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneBTBRDeviceSupport
+import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder
+import nodomain.freeyourgadget.gadgetbridge.util.CheckSums
+import nodomain.freeyourgadget.gadgetbridge.util.kotlin.stringUntilNullTerminator
+import org.slf4j.LoggerFactory
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.LinkedList
+import java.util.Queue
+import java.util.UUID
+import kotlin.math.floor
+
+class ShokzSupport : AbstractHeadphoneBTBRDeviceSupport(LOG, MAX_MTU) {
+ private val packetBuffer: ByteBuffer = ByteBuffer.allocate(MAX_MTU).order(ByteOrder.LITTLE_ENDIAN)
+
+ private val messageQueue: Queue = LinkedList()
+ private var pendingMessage: ShokzMessage? = null
+ private var timeoutRetries = 0
+ private val timeoutHandler = Handler()
+
+ init {
+ addSupportedService(UUID_SERVICE_SHOKZ)
+ }
+
+ override fun useAutoConnect(): Boolean {
+ return true
+ }
+
+ override fun initializeDevice(builder: TransactionBuilder): TransactionBuilder {
+ packetBuffer.clear()
+ timeoutHandler.removeCallbacksAndMessages(null)
+ messageQueue.clear()
+ pendingMessage = null
+ timeoutRetries = 0
+
+ builder.setDeviceState(GBDevice.State.INITIALIZING)
+
+ // Send the fw version request directly, but queue everything else, otherwise the device does not respond to all
+ builder.write(*encodeCommand(ShokzCommand.FIRMWARE_GET))
+
+ pendingMessage = ShokzMessage(ShokzCommand.FIRMWARE_GET)
+ timeoutHandler.postDelayed({ onCommandTimeout() }, 2000L)
+ queueCommand(ShokzCommand.MEDIA_SOURCE_GET)
+ queueCommand(ShokzCommand.BATTERY_GET)
+ queueCommand(ShokzCommand.EQUALIZER_GET)
+ queueCommand(ShokzCommand.PLAYBACK_STATUS_GET)
+ queueCommand(ShokzCommand.VOLUME_GET)
+ queueCommand(ShokzCommand.MP3_PLAYBACK_MODE_GET)
+ queueCommand(ShokzCommand.CONTROLS_GET)
+ queueCommand(ShokzCommand.LANGUAGE_GET)
+
+ return builder
+ }
+
+ override fun dispose() {
+ synchronized(ConnectionMonitor) {
+ timeoutHandler.removeCallbacksAndMessages(null)
+ super.dispose()
+ }
+ }
+
+ override fun onSocketRead(data: ByteArray) {
+ packetBuffer.put(data)
+ packetBuffer.flip()
+
+ while (packetBuffer.hasRemaining()) {
+ packetBuffer.mark()
+
+ if (packetBuffer.remaining() < 10) {
+ // not enough bytes for min packet
+ packetBuffer.reset()
+ break
+ }
+
+ val preamble = packetBuffer.getShort()
+ if (preamble != PACKET_PREAMBLE) {
+ LOG.warn("Unexpected byte 0x{} is not preamble, skipping 2b", preamble.toHexString())
+ continue
+ }
+
+ val packetLength = packetBuffer.getShort()
+ if (packetBuffer.remaining() < 4 + packetLength) {
+ // not enough bytes for packetIdx + packet
+ packetBuffer.reset()
+ break
+ }
+
+ packetBuffer.get().toInt() // always 1?
+ packetBuffer.get().toInt() // isAck?
+ packetBuffer.getShort() // always 0?
+ val payloadLength = packetBuffer.getShort().toInt() and 0xffff
+
+ if (packetBuffer.remaining() < 2 + payloadLength) {
+ // not enough bytes for payload + crc
+ packetBuffer.reset()
+ break
+ }
+
+ val crc = packetBuffer.getShort().toInt() and 0xffff
+ val payload = ByteArray(payloadLength)
+ packetBuffer.get(payload)
+
+ val expectedCrc = CheckSums.crc16_maxim(payload, 0, payload.size)
+ if (crc != expectedCrc) {
+ LOG.warn("Invalid CRC: got 0x{}, expected 0x{}", crc.toHexString(), expectedCrc.toHexString())
+ continue
+ }
+
+ var sendNext: Boolean
+ try {
+ sendNext = handlePayload(payload)
+ } catch (e: Exception) {
+ LOG.error("Failed to handle payload", e)
+ sendNext = true
+ }
+
+ if (sendNext) {
+ sendNextCommand()
+ }
+ }
+
+ packetBuffer.compact()
+ }
+
+ override fun onSendConfiguration(config: String) {
+ when (config) {
+ DeviceSettingsPreferenceConst.PREF_LANGUAGE -> setLanguage()
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_MP3 -> setEqualizer(config)
+
+ DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE -> setMediaSource()
+ DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE -> setMediaPlaybackMode()
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION,
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN -> setControls()
+
+ else -> super.onSendConfiguration(config)
+ }
+ }
+
+ private fun setLanguage() {
+ val localeString: String = devicePrefs.getString("language", "en")
+ val language = ShokzLanguage.fromLocale(localeString) ?: run {
+ LOG.warn("Unknown language {}, falling back to english", localeString)
+ ShokzLanguage.ENGLISH
+ }
+ LOG.info("Setting language to {}", language)
+
+ queueCommand(
+ ShokzCommand.LANGUAGE_SET,
+ byteArrayOf(language.code.toByte(), 0x00, 0x00, 0x00)
+ )
+ }
+
+ private fun setEqualizer(config: String) {
+ val value: String = devicePrefs.getString(config, "standard")
+ val equalizer = ShokzEqualizer.fromPreference(value) ?: run {
+ LOG.warn("Unknown equalizer {}, falling back to standard", value)
+ ShokzEqualizer.STANDARD
+ }
+
+ LOG.info("Setting equalizer to {}", equalizer)
+
+ val args = when (equalizer) {
+ ShokzEqualizer.STANDARD,
+ ShokzEqualizer.SWIMMING -> {
+ byteArrayOf(
+ equalizer.code.toByte(), 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00
+ )
+ }
+
+ ShokzEqualizer.VOCAL -> {
+ byteArrayOf(
+ equalizer.code.toByte(), 0xfc.toByte(), 0x00, 0x03,
+ 0x02, 0x02, 0x00, 0x00
+ )
+ }
+ }
+ queueCommand(ShokzCommand.EQUALIZER_SET, args)
+ }
+
+ private fun setMediaSource() {
+ val value: String = devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE, "bluetooth")
+ val mediaSource = ShokzMediaSource.fromPreference(value) ?: run {
+ LOG.warn("Unknown media source {}, falling back to bluetooth", value)
+ ShokzMediaSource.BLUETOOTH
+ }
+
+ LOG.info("Setting media source to {}", mediaSource)
+
+ queueCommand(
+ ShokzCommand.MEDIA_SOURCE_SET,
+ byteArrayOf(mediaSource.code.toByte(), 0x00, 0x00, 0x00)
+ )
+ }
+
+ private fun setMediaPlaybackMode() {
+ val value: String = devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE, "normal")
+ val playbackMode = ShokzMp3PlaybackMode.fromPreference(value) ?: run {
+ LOG.warn("Unknown playback mode {}, falling back to normal", value)
+ ShokzMp3PlaybackMode.NORMAL
+ }
+
+ LOG.info("Setting playback mode to {}", playbackMode)
+
+ queueCommand(
+ ShokzCommand.MP3_PLAYBACK_MODE_SET,
+ byteArrayOf(playbackMode.code.toByte(), 0x00, 0x00, 0x00)
+ )
+ }
+
+ private fun setControls() {
+ val valueLongPress: String = devicePrefs.getString(
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION,
+ "standard"
+ )
+ val valueVolumeUpDown: String = devicePrefs.getString(
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN,
+ "standard"
+ )
+
+ val controls = when {
+ valueLongPress == "assistant" && valueVolumeUpDown == "media_source" -> ShokzControls.ASSISTANT__MEDIA_SOURCE
+ valueLongPress == "media_source" && valueVolumeUpDown == "assistant" -> ShokzControls.MEDIA_SOURCE__ASSISTANT
+ valueLongPress == "media_source" && valueVolumeUpDown == "media_source" -> ShokzControls.MEDIA_SOURCE__MEDIA_SOURCE
+ valueLongPress == "assistant" && valueVolumeUpDown == "assistant" -> ShokzControls.ASSISTANT__ASSISTANT
+ else -> {
+ LOG.warn(
+ "Unknown controls {}/{}, falling back to {}",
+ valueLongPress, valueVolumeUpDown,
+ ShokzControls.ASSISTANT__MEDIA_SOURCE
+ )
+ ShokzControls.ASSISTANT__MEDIA_SOURCE
+ }
+ }
+
+ LOG.info("Setting controls to {}", controls)
+
+ queueCommand(
+ ShokzCommand.CONTROLS_SET,
+ byteArrayOf(controls.code.toByte(), 0x00, 0x00, 0x00)
+ )
+ }
+
+ private fun handlePayload(payload: ByteArray): Boolean {
+ if (LOG.isTraceEnabled) {
+ LOG.trace("Processing payload: {}", payload.toHexString())
+ }
+
+ val buf = ByteBuffer.wrap(payload).order(ByteOrder.LITTLE_ENDIAN)
+ val unk1 = buf.getInt() // 0x01
+ if (unk1 != 0x01) {
+ LOG.warn("Unexpected unk1 0x{}", unk1.toHexString())
+ return true
+ }
+ val len = buf.getInt()
+ if (len != buf.remaining()) {
+ LOG.warn("Unexpected number of bytes in payload: got 0x{}, expected 0x{}", buf.remaining(), len)
+ return true
+ }
+ val unk2 = buf.getInt() // 0x02
+ if (unk2 != 0x02) {
+ LOG.warn("Unexpected unk2 0x{}", unk2.toHexString())
+ return true
+ }
+ val unk3 = buf.getInt() // 0x04
+ if (unk3 != 0x04) {
+ LOG.warn("Unexpected unk3 0x{}", unk3.toHexString())
+ return true
+ }
+ val group = buf.getInt()
+ val unk4 = buf.getInt() // 0x03
+ if (unk4 != 0x03) {
+ LOG.warn("Unexpected unk4 0x{}", unk4.toHexString())
+ return true
+ }
+ val unk5 = buf.getInt() // 0x04
+ if (unk5 != 0x04) {
+ LOG.warn("Unexpected unk5 0x{}", unk5.toHexString())
+ return true
+ }
+ val code = buf.getInt()
+ val unk6 = buf.getInt() // 0x04
+ if (unk6 != 0x04) {
+ LOG.warn("Unexpected unk6 0x{}", unk6.toHexString())
+ return true
+ }
+ val argsLength = buf.getInt()
+ val args = ByteArray(argsLength)
+ buf.get(args)
+
+ val command = ShokzCommand.getCommand(group, code)
+ if (command == null) {
+ LOG.warn("Unknown command: group 0x{}, code 0x{}", group.toHexString(), code.toHexString())
+ return true
+ }
+
+ handleCommand(command, args)
+
+ return pendingMessage?.let { msg ->
+ (msg.command.code or MASK_RESPONSE) == command.code
+ } ?: false
+ }
+
+ private fun handleCommand(command: ShokzCommand, args: ByteArray) {
+ val buf = ByteBuffer.wrap(args).order(ByteOrder.LITTLE_ENDIAN)
+
+ LOG.debug("Handling command {} args={}", command, args.toHexString())
+
+ when (command) {
+ ShokzCommand.FIRMWARE_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val firmware = buf.stringUntilNullTerminator()
+ if (firmware == null) {
+ LOG.warn("Failed to get firmware from payload")
+ return
+ }
+ LOG.info("Got firmware: {}", firmware)
+
+ val versionInfoEvent = GBDeviceEventVersionInfo()
+ versionInfoEvent.fwVersion = firmware
+ evaluateGBDeviceEvent(versionInfoEvent)
+
+ gbDevice.setUpdateState(GBDevice.State.INITIALIZED, context)
+ }
+
+ ShokzCommand.MEDIA_SOURCE_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val code = buf.get().toInt() and 0xff
+ val mode = ShokzMediaSource.fromCode(code)
+ if (mode == null) {
+ LOG.warn("Unknown mode code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got mode: {}", mode)
+
+ evaluateGBDeviceEvent(
+ GBDeviceEventUpdatePreferences(
+ DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
+ mode.name.lowercase()
+ )
+ )
+ }
+
+ ShokzCommand.BATTERY_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val batteryPercentage = ((buf.get().toInt() and 0xff) + 1) * 10
+
+ LOG.info("Got battery: {}%", batteryPercentage)
+
+ val batteryInfoEvent = GBDeviceEventBatteryInfo()
+ batteryInfoEvent.level = batteryPercentage
+ evaluateGBDeviceEvent(batteryInfoEvent)
+ }
+
+ ShokzCommand.EQUALIZER_RET, ShokzCommand.EQUALIZER_ACK -> {
+ if (command == ShokzCommand.EQUALIZER_RET) {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ }
+
+ val code = buf.get().toInt() and 0xff
+ val equalizer = ShokzEqualizer.fromCode(code)
+ if (equalizer == null) {
+ LOG.warn("Unknown equalizer code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got equalizer: {}", equalizer)
+
+ val mediaSourceValue: String =
+ devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE, "bluetooth")
+ val mediaSource = ShokzMediaSource.fromPreference(mediaSourceValue) ?: run {
+ LOG.warn("Unknown media source {}, falling back to bluetooth", mediaSourceValue)
+ ShokzMediaSource.BLUETOOTH
+ }
+
+ devicePrefs.getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, "en")
+ evaluateGBDeviceEvent(
+ GBDeviceEventUpdatePreferences(
+ when (mediaSource) {
+ ShokzMediaSource.BLUETOOTH -> DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH
+ ShokzMediaSource.MP3 -> DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_MP3
+ },
+ equalizer.name.lowercase()
+ )
+ )
+ }
+
+ ShokzCommand.PLAYBACK_STATUS_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val code = buf.get().toInt() and 0xff
+ val playbackStatus = ShokzPlaybackStatus.fromCode(code)
+ if (playbackStatus == null) {
+ LOG.warn("Unknown playback status code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got playback status: {}", playbackStatus)
+
+ // TODO handle
+ }
+
+ ShokzCommand.VOLUME_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val percentage = floor(((buf.get().toInt() and 0xff) / 16) * 100 + 0.5)
+
+ LOG.info("Got volume: {}", percentage)
+
+ // TODO handle
+ }
+
+ ShokzCommand.MP3_PLAYBACK_MODE_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val code = buf.get().toInt() and 0xff
+ val playbackMode = ShokzMp3PlaybackMode.fromCode(code)
+ if (playbackMode == null) {
+ LOG.warn("Unknown mp3 playback mode code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got mp3 playback mode: {}", playbackMode)
+
+ evaluateGBDeviceEvent(
+ GBDeviceEventUpdatePreferences(
+ DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE,
+ playbackMode.name.lowercase()
+ )
+ )
+ }
+
+ ShokzCommand.CONTROLS_RET -> {
+ val zero = buf.get().toInt()
+ if (zero != 0) {
+ LOG.warn("Unexpected non-zero byte 0x{} for {}", zero.toHexString(), command)
+ return
+ }
+ val code = buf.get().toInt() and 0xff
+ val controls = ShokzControls.fromCode(code)
+ if (controls == null) {
+ LOG.warn("Unknown controls code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got controls: {}", controls)
+
+ val eventUpdatePreferences = GBDeviceEventUpdatePreferences()
+
+ eventUpdatePreferences.withPreference(
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_LONG_PRESS_MULTI_FUNCTION,
+ when (controls) {
+ ShokzControls.ASSISTANT__MEDIA_SOURCE, ShokzControls.ASSISTANT__ASSISTANT -> "assistant"
+ ShokzControls.MEDIA_SOURCE__ASSISTANT, ShokzControls.MEDIA_SOURCE__MEDIA_SOURCE -> "media_source"
+ }
+ ).withPreference(
+ DeviceSettingsPreferenceConst.PREF_SHOKZ_CONTROLS_SIMULTANEOUS_VOLUME_UP_DOWN,
+ when (controls) {
+ ShokzControls.ASSISTANT__MEDIA_SOURCE, ShokzControls.MEDIA_SOURCE__MEDIA_SOURCE -> "media_source"
+ ShokzControls.MEDIA_SOURCE__ASSISTANT, ShokzControls.ASSISTANT__ASSISTANT -> "assistant"
+ }
+ )
+
+ evaluateGBDeviceEvent(eventUpdatePreferences)
+ }
+
+ ShokzCommand.LANGUAGE_RET -> {
+ buf.getShort() // 0
+ val code = buf.get().toInt() and 0xff
+ val language = ShokzLanguage.fromCode(code)
+ if (language == null) {
+ LOG.warn("Unknown language code 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got language: {}", language)
+
+ evaluateGBDeviceEvent(
+ GBDeviceEventUpdatePreferences(
+ DeviceSettingsPreferenceConst.PREF_LANGUAGE,
+ language.locale
+ )
+ )
+ }
+
+ ShokzCommand.MEDIA_SOURCE_NOTIFY -> {
+ val code = buf.get().toInt() and 0xff
+ val mediaSource = ShokzMediaSource.fromCode(code)
+ if (mediaSource == null) {
+ LOG.warn("Unknown media source 0x{}", code.toHexString())
+ return
+ }
+
+ LOG.info("Got media source change: {}", mediaSource)
+
+ evaluateGBDeviceEvent(
+ GBDeviceEventUpdatePreferences(
+ DeviceSettingsPreferenceConst.PREF_MEDIA_SOURCE,
+ mediaSource.name.lowercase()
+ )
+ )
+ }
+
+ else -> LOG.warn("Unhandled command {}, args={}", command, args.toHexString())
+ }
+ }
+
+ private fun queueCommand(
+ command: ShokzCommand,
+ args: ByteArray = byteArrayOf()
+ ) {
+ messageQueue.add(ShokzMessage(command, args))
+
+ if (pendingMessage == null) {
+ sendNextCommand()
+ }
+ }
+
+ private fun onCommandTimeout() {
+ if (timeoutRetries++ < 3) {
+ LOG.warn("Timed out waiting for response, retrying attempt {}", timeoutRetries)
+ pendingMessage?.let { msg ->
+ sendMessage(msg)
+ return
+ }
+ }
+ LOG.warn("Timed out waiting for response, giving up")
+ sendNextCommand()
+ }
+
+ private fun sendNextCommand() {
+ timeoutHandler.removeCallbacksAndMessages(null)
+ timeoutRetries = 0
+
+ pendingMessage = messageQueue.poll()
+ pendingMessage?.let { msg ->
+ LOG.debug("Sending next command in queue: {}", msg.command)
+ sendMessage(msg)
+ return
+ }
+ LOG.debug("No more commands in the queue")
+ }
+
+ private fun sendMessage(message: ShokzMessage) {
+ val builder = createTransactionBuilder(message.command.name.lowercase())
+ builder.write(*encodeCommand(message.command, message.args))
+ builder.queue()
+
+ timeoutHandler.postDelayed({ onCommandTimeout() }, 2000L)
+ }
+
+ companion object {
+ private val LOG = LoggerFactory.getLogger(ShokzSupport::class.java)
+ private const val MAX_MTU: Int = 2048
+ private const val PACKET_PREAMBLE: Short = 0x5aa5.toShort() // LE
+ private const val MASK_RESPONSE: Int = 0x8000
+
+ // This does not show up in the discovered services?
+ private val UUID_SERVICE_SHOKZ: UUID = UUID.fromString("0000fef0-0000-1000-8000-00805f9b34fb")
+
+ /// Each command seems to have the following format (all u32 LE)
+ /// 0x01 length 0x02 0x04 group 0x03 0x04 command 0x04 args_length args
+ fun encodeCommand(
+ command: ShokzCommand,
+ args: ByteArray = byteArrayOf()
+ ): ByteArray {
+ val buf = ByteBuffer.allocate(52 + args.size).order(ByteOrder.LITTLE_ENDIAN)
+ buf.putShort(PACKET_PREAMBLE)
+ buf.putShort((buf.limit() - 8).toShort())
+ buf.putInt(0x01)
+ buf.putShort((buf.limit() - 12).toShort())
+ // CRC will be added later
+ buf.position(buf.position() + 2)
+ buf.putInt(0x01)
+ buf.putInt(buf.limit() - 20)
+ buf.putInt(0x02)
+ buf.putInt(0x04)
+ buf.putInt(command.group)
+ buf.putInt(0x03)
+ buf.putInt(0x04)
+ buf.putInt(command.code)
+ buf.putInt(0x04)
+ buf.putInt(args.size)
+ buf.put(args)
+
+ buf.putShort(10, CheckSums.crc16_maxim(buf.array(), 12, buf.limit() - 12).toShort())
+
+ return buf.array()
+ }
+ }
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java
index b54260f810..f8382c6cc1 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/xiaomi/XiaomiSppSupport.java
@@ -46,7 +46,7 @@ import nodomain.freeyourgadget.gadgetbridge.util.GB;
public class XiaomiSppSupport extends XiaomiConnectionSupport {
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSppSupport.class);
- AbstractBTBRDeviceSupport commsSupport = new AbstractBTBRDeviceSupport(LOG) {
+ AbstractBTBRDeviceSupport commsSupport = new AbstractBTBRDeviceSupport(LOG, 1024) {
@Override
public boolean useAutoConnect() {
return mXiaomiSupport.useAutoConnect();
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java
index d6e42705c2..db2fe27ccc 100644
--- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/CheckSums.java
@@ -59,7 +59,7 @@ public class CheckSums {
public static int getCRC16(byte[] seq) {
return getCRC16(seq, 0xFFFF);
}
-
+
public static int getCRC16(byte[] seq, int crc) {
for (byte b : seq) {
crc = ((crc >>> 8) | (crc << 8)) & 0xffff;
@@ -71,7 +71,7 @@ public class CheckSums {
crc &= 0xffff;
return crc;
}
-
+
public static int getCRC16ansi(byte[] seq) {
int crc = 0xffff;
int polynomial = 0xA001;
@@ -190,4 +190,24 @@ public class CheckSums {
md.update(str.getBytes(StandardCharsets.UTF_8));
return GB.hexdump(md.digest()).toLowerCase(Locale.ROOT);
}
+
+ public static int crc16_maxim(final byte[] data, final int offset, final int length) {
+ // Reflected polynomial 0x8005 -> 0xA001 when shifting right (LSB first)
+ final int poly = 0xA001;
+ int crc = 0x0000;
+
+ for (int i = offset; i < offset + length; i++) {
+ final byte b = data[i];
+ crc ^= (b & 0xFF);
+ for (int j = 0; j < 8; j++) {
+ if ((crc & 0x0001) != 0) {
+ crc = (crc >>> 1) ^ poly;
+ } else {
+ crc >>>= 1;
+ }
+ }
+ }
+
+ return crc ^ 0xFFFF;
+ }
}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteArrayExtensions.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteArrayExtensions.kt
new file mode 100644
index 0000000000..dcf8fe8a56
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteArrayExtensions.kt
@@ -0,0 +1,13 @@
+package nodomain.freeyourgadget.gadgetbridge.util.kotlin
+
+fun ByteArray.startsWith(prefix: ByteArray): Boolean {
+ if (prefix.size > this.size) {
+ return false
+ }
+ for (i in prefix.indices) {
+ if (this[i] != prefix[i]) {
+ return false
+ }
+ }
+ return true
+}
diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteBufferExtensions.kt b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteBufferExtensions.kt
new file mode 100644
index 0000000000..32b14cacff
--- /dev/null
+++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/kotlin/ByteBufferExtensions.kt
@@ -0,0 +1,8 @@
+package nodomain.freeyourgadget.gadgetbridge.util.kotlin
+
+import nodomain.freeyourgadget.gadgetbridge.util.StringUtils
+import java.nio.ByteBuffer
+
+fun ByteBuffer.stringUntilNullTerminator(): String? {
+ return StringUtils.untilNullTerminator(this)
+}
diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml
index e3566efc36..7303ac01ad 100644
--- a/app/src/main/res/values/arrays.xml
+++ b/app/src/main/res/values/arrays.xml
@@ -2708,6 +2708,7 @@
- @string/bahasa_melayu
- @string/persian
+ - @string/chinese
- @string/english
- @string/korean
- @string/japanese
@@ -2773,6 +2774,7 @@
- ms_MY
- fa_IR
+ - zh
- en
- ko
- ja
@@ -4119,6 +4121,53 @@
- custom_2
+
+ - @string/menuitem_bluetooth
+ - @string/mp3
+
+
+ - bluetooth
+ - mp3
+
+
+
+ - @string/equalizer_preset_standard
+ - @string/sony_equalizer_preset_vocal
+
+
+ - standard
+ - vocal
+
+
+
+ - @string/equalizer_preset_standard
+ - @string/Swimming
+
+
+ - standard
+ - swimming
+
+
+
+ - @string/media_playback_mode_normal
+ - @string/media_playback_mode_shuffle
+ - @string/media_playback_mode_repeat
+
+
+ - normal
+ - shuffle
+ - repeat
+
+
+
+ - @string/moondrop_touch_action_assistant
+ - @string/media_source_switching_bluetooth_mp3
+
+
+ - assistant
+ - media_source
+
+
- @string/sony_automatic_power_off_off
- @string/sony_automatic_power_off_5_min
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 631b3e9f25..8d6e8138b1 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1208,6 +1208,7 @@
Manual
Chinese (Simplified)
Chinese (Traditional)
+ Chinese
English
English (Australia)
English (Canada)
@@ -4205,4 +4206,15 @@
Help
Could not open URL
Authentication settings
+ Shokz OpenSwim Pro
+ Standard
+ Playback mode
+ Normal
+ Shuffle
+ Repeat
+ Media source
+ MP3
+ Switch media source (Bluetooth/MP3)
+ Long-press the multi-function button
+ Long-press volume up and down simultaneously
diff --git a/app/src/main/res/xml/devicesettings_media_source.xml b/app/src/main/res/xml/devicesettings_media_source.xml
new file mode 100644
index 0000000000..58dc37a8ba
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_media_source.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_playback_mode.xml b/app/src/main/res/xml/devicesettings_playback_mode.xml
new file mode 100644
index 0000000000..2882610f7c
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_playback_mode.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_shokz_controls.xml b/app/src/main/res/xml/devicesettings_shokz_controls.xml
new file mode 100644
index 0000000000..46e6814b27
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_shokz_controls.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/devicesettings_shokz_equalizer.xml b/app/src/main/res/xml/devicesettings_shokz_equalizer.xml
new file mode 100644
index 0000000000..3160d3828b
--- /dev/null
+++ b/app/src/main/res/xml/devicesettings_shokz_equalizer.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupportTest.kt b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupportTest.kt
new file mode 100644
index 0000000000..10cf16a3d8
--- /dev/null
+++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/shokz/ShokzSupportTest.kt
@@ -0,0 +1,14 @@
+package nodomain.freeyourgadget.gadgetbridge.service.devices.shokz
+
+import org.junit.Assert.*
+import org.junit.Test
+
+class ShokzSupportTest {
+ @Test
+ fun testEncodeCommand() {
+ assertEquals(
+ "a55a2c000100000028005ab701000000200000000200000004000000020000000300000004000000020000000400000000000000",
+ ShokzSupport.encodeCommand(ShokzCommand.FIRMWARE_GET).toHexString()
+ )
+ }
+}