mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
Shokz OpenSwim Pro: Initial support
This commit is contained in:
Generated
+1
@@ -147,6 +147,7 @@
|
||||
<w>sevostyanova</w>
|
||||
<w>shahrabani</w>
|
||||
<w>shimokawa</w>
|
||||
<w>shokz</w>
|
||||
<w>skype</w>
|
||||
<w>slezak</w>
|
||||
<w>sophanimus</w>
|
||||
|
||||
+7
@@ -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";
|
||||
|
||||
|
||||
+16
-1
@@ -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<Integer> rootScreens = new ArrayList<>();
|
||||
private final Map<String, List<Integer>> subScreens = new LinkedHashMap<>();
|
||||
|
||||
/// Settings that should only be available while connected
|
||||
private final List<String> 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<Integer> 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<String> getConnectedPreferences() {
|
||||
return connectedPreferences;
|
||||
}
|
||||
|
||||
public List<Integer> getAllNonRootScreens() {
|
||||
final List<Integer> allScreens = new ArrayList<>();
|
||||
for (final List<Integer> screens : subScreens.values()) {
|
||||
@@ -143,7 +156,7 @@ public class DeviceSpecificSettings implements Parcelable {
|
||||
return allScreens;
|
||||
}
|
||||
|
||||
public static final Creator<DeviceSpecificSettings> CREATOR = new Creator<DeviceSpecificSettings>() {
|
||||
public static final Creator<DeviceSpecificSettings> 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);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-1
@@ -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);
|
||||
|
||||
+2
@@ -16,6 +16,7 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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<String> actions, String message, String broadcastPackage) {
|
||||
if (actions.isEmpty()) {
|
||||
|
||||
+19
-3
@@ -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<String, Object> preferences;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
final StringBuilder sb = new StringBuilder(super.toString());
|
||||
sb.append("preferences: ");
|
||||
for (Map.Entry<String, Object> 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<String, Object> 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);
|
||||
|
||||
+70
@@ -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<out DeviceSupport?> {
|
||||
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<out String>? {
|
||||
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()
|
||||
}
|
||||
}
|
||||
+14
@@ -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
|
||||
}
|
||||
}
|
||||
+75
@@ -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 <https://www.gnu.org/licenses/>. */
|
||||
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<Preference>(DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_BLUETOOTH)?.isVisible =
|
||||
mediaSource == ShokzMediaSource.BLUETOOTH
|
||||
handler.findPreference<Preference>(DeviceSettingsPreferenceConst.PREF_SHOKZ_EQUALIZER_MP3)?.isVisible =
|
||||
mediaSource == ShokzMediaSource.MP3
|
||||
handler.findPreference<Preference>(DeviceSettingsPreferenceConst.PREF_MEDIA_PLAYBACK_MODE)?.isVisible =
|
||||
mediaSource == ShokzMediaSource.MP3
|
||||
}
|
||||
|
||||
override fun getPreferenceKeysWithSummary(): Set<String> {
|
||||
return setOf()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val LOG = LoggerFactory.getLogger(ShokzSettingsCustomizer::class.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),
|
||||
|
||||
+41
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-6
@@ -16,6 +16,10 @@
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||
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;
|
||||
}
|
||||
|
||||
-2
@@ -26,8 +26,6 @@ public interface SocketCallback {
|
||||
|
||||
/**
|
||||
* Read data from InputStream of BluetoothSocket
|
||||
*
|
||||
* @param data
|
||||
*/
|
||||
void onSocketRead(byte[] data);
|
||||
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+5
@@ -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)");
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
+1
-2
@@ -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
|
||||
|
||||
-6
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-2
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
-1
@@ -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
|
||||
|
||||
+74
@@ -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 }
|
||||
}
|
||||
}
|
||||
+75
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
@@ -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
|
||||
}
|
||||
}
|
||||
+626
@@ -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<ShokzMessage> = 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+13
@@ -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
|
||||
}
|
||||
+8
@@ -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)
|
||||
}
|
||||
@@ -2708,6 +2708,7 @@
|
||||
<item name="ms_MY">@string/bahasa_melayu</item>
|
||||
<item name="fa_IR">@string/persian</item>
|
||||
<!-- simplified versions, without a country -->
|
||||
<item name="zh">@string/chinese</item>
|
||||
<item name="en">@string/english</item>
|
||||
<item name="ko">@string/korean</item>
|
||||
<item name="ja">@string/japanese</item>
|
||||
@@ -2773,6 +2774,7 @@
|
||||
<item>ms_MY</item>
|
||||
<item>fa_IR</item>
|
||||
<!-- simplified versions, without a country -->
|
||||
<item>zh</item>
|
||||
<item>en</item>
|
||||
<item>ko</item>
|
||||
<item>ja</item>
|
||||
@@ -4119,6 +4121,53 @@
|
||||
<item>custom_2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="media_source_names">
|
||||
<item>@string/menuitem_bluetooth</item>
|
||||
<item>@string/mp3</item>
|
||||
</string-array>
|
||||
<string-array name="media_source_values">
|
||||
<item>bluetooth</item>
|
||||
<item>mp3</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="shokz_equalizer_bluetooth_names">
|
||||
<item>@string/equalizer_preset_standard</item>
|
||||
<item>@string/sony_equalizer_preset_vocal</item>
|
||||
</string-array>
|
||||
<string-array name="shokz_equalizer_bluetooth_values">
|
||||
<item>standard</item>
|
||||
<item>vocal</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="shokz_equalizer_mp3_names">
|
||||
<item>@string/equalizer_preset_standard</item>
|
||||
<item>@string/Swimming</item>
|
||||
</string-array>
|
||||
<string-array name="shokz_equalizer_mp3_values">
|
||||
<item>standard</item>
|
||||
<item>swimming</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="media_playback_mode_names">
|
||||
<item>@string/media_playback_mode_normal</item>
|
||||
<item>@string/media_playback_mode_shuffle</item>
|
||||
<item>@string/media_playback_mode_repeat</item>
|
||||
</string-array>
|
||||
<string-array name="media_playback_mode_values">
|
||||
<item>normal</item>
|
||||
<item>shuffle</item>
|
||||
<item>repeat</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="shokz_controls_names">
|
||||
<item>@string/moondrop_touch_action_assistant</item>
|
||||
<item>@string/media_source_switching_bluetooth_mp3</item>
|
||||
</string-array>
|
||||
<string-array name="shokz_controls_values">
|
||||
<item>assistant</item>
|
||||
<item>media_source</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="sony_automatic_power_off_names">
|
||||
<item>@string/sony_automatic_power_off_off</item>
|
||||
<item>@string/sony_automatic_power_off_5_min</item>
|
||||
|
||||
@@ -1208,6 +1208,7 @@
|
||||
<string name="manual">Manual</string>
|
||||
<string name="simplified_chinese">Chinese (Simplified)</string>
|
||||
<string name="traditional_chinese">Chinese (Traditional)</string>
|
||||
<string name="chinese">Chinese</string>
|
||||
<string name="english">English</string>
|
||||
<string name="english_au">English (Australia)</string>
|
||||
<string name="english_ca">English (Canada)</string>
|
||||
@@ -4205,4 +4206,15 @@
|
||||
<string name="help">Help</string>
|
||||
<string name="cannot_open_help_url">Could not open URL</string>
|
||||
<string name="auth_settings">Authentication settings</string>
|
||||
<string name="devicetype_shokz_openswim_pro">Shokz OpenSwim Pro</string>
|
||||
<string name="equalizer_preset_standard">Standard</string>
|
||||
<string name="media_playback_mode">Playback mode</string>
|
||||
<string name="media_playback_mode_normal">Normal</string>
|
||||
<string name="media_playback_mode_shuffle">Shuffle</string>
|
||||
<string name="media_playback_mode_repeat">Repeat</string>
|
||||
<string name="media_source">Media source</string>
|
||||
<string name="mp3">MP3</string>
|
||||
<string name="media_source_switching_bluetooth_mp3">Switch media source (Bluetooth/MP3)</string>
|
||||
<string name="shokz_controls_long_press_multi_function">Long-press the multi-function button</string>
|
||||
<string name="shokz_controls_simultaneous_volume_up_down">Long-press volume up and down simultaneously</string>
|
||||
</resources>
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="off"
|
||||
android:entries="@array/media_source_names"
|
||||
android:entryValues="@array/media_source_values"
|
||||
android:icon="@drawable/ic_music_note"
|
||||
android:key="pref_media_source"
|
||||
android:title="@string/media_source"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="normal"
|
||||
android:entries="@array/media_playback_mode_names"
|
||||
android:entryValues="@array/media_playback_mode_values"
|
||||
android:icon="@drawable/ic_play"
|
||||
android:key="pref_media_playback_mode"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
android:title="@string/media_playback_mode" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue=""
|
||||
android:entries="@array/shokz_controls_names"
|
||||
android:entryValues="@array/shokz_controls_values"
|
||||
android:icon="@drawable/ic_touch"
|
||||
android:key="shokz_controls_long_press_multi_function"
|
||||
android:title="@string/shokz_controls_long_press_multi_function"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue=""
|
||||
android:entries="@array/shokz_controls_names"
|
||||
android:entryValues="@array/shokz_controls_values"
|
||||
android:icon="@drawable/ic_arrows_up_down"
|
||||
android:key="shokz_controls_simultaneous_volume_up_down"
|
||||
android:title="@string/shokz_controls_simultaneous_volume_up_down"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
@@ -0,0 +1,23 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="standard"
|
||||
android:entries="@array/shokz_equalizer_bluetooth_names"
|
||||
android:entryValues="@array/shokz_equalizer_bluetooth_values"
|
||||
android:icon="@drawable/ic_equalizer"
|
||||
android:key="pref_shokz_equalizer_bluetooth"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
android:title="@string/sony_equalizer" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="standard"
|
||||
android:entries="@array/shokz_equalizer_mp3_names"
|
||||
android:entryValues="@array/shokz_equalizer_mp3_values"
|
||||
android:icon="@drawable/ic_equalizer"
|
||||
android:key="pref_shokz_equalizer_mp3"
|
||||
app:useSimpleSummaryProvider="true"
|
||||
android:title="@string/sony_equalizer" />
|
||||
|
||||
</androidx.preference.PreferenceScreen>
|
||||
+14
@@ -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()
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user