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>sevostyanova</w>
|
||||||
<w>shahrabani</w>
|
<w>shahrabani</w>
|
||||||
<w>shimokawa</w>
|
<w>shimokawa</w>
|
||||||
|
<w>shokz</w>
|
||||||
<w>skype</w>
|
<w>skype</w>
|
||||||
<w>slezak</w>
|
<w>slezak</w>
|
||||||
<w>sophanimus</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_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_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_ENABLED = "override_features_enabled";
|
||||||
public static final String PREF_OVERRIDE_FEATURES_LIST = "override_features_list";
|
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 androidx.annotation.XmlRes;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -47,6 +48,9 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
private final List<Integer> rootScreens = new ArrayList<>();
|
private final List<Integer> rootScreens = new ArrayList<>();
|
||||||
private final Map<String, List<Integer>> subScreens = new LinkedHashMap<>();
|
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() {
|
public DeviceSpecificSettings() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,6 +119,7 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
Objects.requireNonNull(subScreens.get(e.getKey())).add(screen);
|
Objects.requireNonNull(subScreens.get(e.getKey())).add(screen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
connectedPreferences.addAll(deviceSpecificSettings.connectedPreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Integer> getRootScreens() {
|
public List<Integer> getRootScreens() {
|
||||||
@@ -126,6 +131,14 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
return subScreens.get(key);
|
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() {
|
public List<Integer> getAllNonRootScreens() {
|
||||||
final List<Integer> allScreens = new ArrayList<>();
|
final List<Integer> allScreens = new ArrayList<>();
|
||||||
for (final List<Integer> screens : subScreens.values()) {
|
for (final List<Integer> screens : subScreens.values()) {
|
||||||
@@ -143,7 +156,7 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
return allScreens;
|
return allScreens;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Creator<DeviceSpecificSettings> CREATOR = new Creator<DeviceSpecificSettings>() {
|
public static final Creator<DeviceSpecificSettings> CREATOR = new Creator<>() {
|
||||||
@Override
|
@Override
|
||||||
public DeviceSpecificSettings createFromParcel(final Parcel in) {
|
public DeviceSpecificSettings createFromParcel(final Parcel in) {
|
||||||
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||||
@@ -161,6 +174,7 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
}
|
}
|
||||||
deviceSpecificSettings.addSubScreen(key, screens);
|
deviceSpecificSettings.addSubScreen(key, screens);
|
||||||
}
|
}
|
||||||
|
in.readStringList(deviceSpecificSettings.connectedPreferences);
|
||||||
return deviceSpecificSettings;
|
return deviceSpecificSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,5 +203,6 @@ public class DeviceSpecificSettings implements Parcelable {
|
|||||||
dest.writeInt(s);
|
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";
|
static final String FRAGMENT_TAG = "DEVICE_SPECIFIC_SETTINGS_FRAGMENT";
|
||||||
|
|
||||||
|
private DeviceSpecificSettings deviceSpecificSettings;
|
||||||
private DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer;
|
private DeviceSpecificSettingsCustomizer deviceSpecificSettingsCustomizer;
|
||||||
|
|
||||||
private GBDevice device;
|
private GBDevice device;
|
||||||
@@ -142,6 +143,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||||||
LOG.debug("{} changed, notifying customizer", changedDevice);
|
LOG.debug("{} changed, notifying customizer", changedDevice);
|
||||||
deviceSpecificSettingsCustomizer.onDeviceChanged(DeviceSpecificSettingsFragment.this);
|
deviceSpecificSettingsCustomizer.onDeviceChanged(DeviceSpecificSettingsFragment.this);
|
||||||
}
|
}
|
||||||
|
reloadEnabledPreferences();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -174,7 +176,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
String settingsFileSuffix = arguments.getString("settingsFileSuffix", null);
|
String settingsFileSuffix = arguments.getString("settingsFileSuffix", null);
|
||||||
DeviceSpecificSettings deviceSpecificSettings = arguments.getParcelable("deviceSpecificSettings");
|
this.deviceSpecificSettings = arguments.getParcelable("deviceSpecificSettings");
|
||||||
this.deviceSpecificSettingsCustomizer = arguments.getParcelable("deviceSpecificSettingsCustomizer");
|
this.deviceSpecificSettingsCustomizer = arguments.getParcelable("deviceSpecificSettingsCustomizer");
|
||||||
this.device = arguments.getParcelable("device");
|
this.device = arguments.getParcelable("device");
|
||||||
|
|
||||||
@@ -241,6 +243,19 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
|
|||||||
}
|
}
|
||||||
|
|
||||||
setChangeListener(rootKey);
|
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) {
|
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_ADAPTIVE_VOLUME_CONTROL);
|
||||||
addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP);
|
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_AMBIENT_SOUND_CONTROL);
|
||||||
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
|
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
|
||||||
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
|
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
|
||||||
|
|||||||
+2
@@ -16,6 +16,7 @@
|
|||||||
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
package nodomain.freeyourgadget.gadgetbridge.activities.devicesettings;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.XmlRes;
|
import androidx.annotation.XmlRes;
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.R;
|
import nodomain.freeyourgadget.gadgetbridge.R;
|
||||||
@@ -50,6 +51,7 @@ public enum DeviceSpecificSettingsScreen {
|
|||||||
this.xml = xml;
|
this.xml = xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getKey() {
|
public String getKey() {
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ public abstract class GBDeviceEvent {
|
|||||||
/**
|
/**
|
||||||
* Helper method to run specific actions configured in the device preferences, upon wear state
|
* Helper method to run specific actions configured in the device preferences, upon wear state
|
||||||
* or awake/asleep events.
|
* or awake/asleep events.
|
||||||
*
|
|
||||||
* @param actions
|
|
||||||
* @param message
|
|
||||||
*/
|
*/
|
||||||
protected void handleDeviceAction(final Context context, final GBDevice device, Set<String> actions, String message, String broadcastPackage) {
|
protected void handleDeviceAction(final Context context, final GBDevice device, Set<String> actions, String message, String broadcastPackage) {
|
||||||
if (actions.isEmpty()) {
|
if (actions.isEmpty()) {
|
||||||
|
|||||||
+19
-3
@@ -19,6 +19,8 @@ package nodomain.freeyourgadget.gadgetbridge.deviceevents;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -34,6 +36,18 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
|
|||||||
|
|
||||||
public final Map<String, Object> preferences;
|
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() {
|
public GBDeviceEventUpdatePreferences() {
|
||||||
this.preferences = new HashMap<>();
|
this.preferences = new HashMap<>();
|
||||||
}
|
}
|
||||||
@@ -73,10 +87,11 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
|
|||||||
private void update(final SharedPreferences prefs) {
|
private void update(final SharedPreferences prefs) {
|
||||||
final SharedPreferences.Editor editor = prefs.edit();
|
final SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
|
||||||
for (String key : preferences.keySet()) {
|
for (Map.Entry<String, Object> e : preferences.entrySet()) {
|
||||||
final Object value = preferences.get(key);
|
final String key = e.getKey();
|
||||||
|
final Object value = e.getValue();
|
||||||
|
|
||||||
LOG.debug("Updating {} = {}", key, value);
|
LOG.trace("Updating {} = {}", key, value);
|
||||||
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
editor.remove(key);
|
editor.remove(key);
|
||||||
@@ -93,6 +108,7 @@ public class GBDeviceEventUpdatePreferences extends GBDeviceEvent {
|
|||||||
} else if (value instanceof Long) {
|
} else if (value instanceof Long) {
|
||||||
editor.putLong(key, (Long) value);
|
editor.putLong(key, (Long) value);
|
||||||
} else if (value instanceof Set) {
|
} else if (value instanceof Set) {
|
||||||
|
//noinspection unchecked,rawtypes
|
||||||
editor.putStringSet(key, (Set) value);
|
editor.putStringSet(key, (Set) value);
|
||||||
} else {
|
} else {
|
||||||
LOG.warn("Unknown preference value type {} for {}", value.getClass(), key);
|
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.Roidmi1Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator;
|
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.smaq2oss.SMAQ2OSSCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soflow.SoFlowCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soflow.SoFlowCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyLinkBudsCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyLinkBudsCoordinator;
|
||||||
@@ -662,6 +663,7 @@ public enum DeviceType {
|
|||||||
SONY_WF_C700N(SonyWFC700NCoordinator.class),
|
SONY_WF_C700N(SonyWFC700NCoordinator.class),
|
||||||
SONY_WF_C710N(SonyWFC710NCoordinator.class),
|
SONY_WF_C710N(SonyWFC710NCoordinator.class),
|
||||||
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
PIXEL_BUDS_A(PixelBudsACoordinator.class),
|
||||||
|
SHOKZ_OPENSWIM_PRO(ShokzOpenSwimProCoordinator.class),
|
||||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
||||||
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
|
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
|
||||||
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.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/>. */
|
along with this program. If not, see <https://www.gnu.org/licenses/>. */
|
||||||
package nodomain.freeyourgadget.gadgetbridge.service.btbr;
|
package nodomain.freeyourgadget.gadgetbridge.service.btbr;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.bluetooth.BluetoothDevice;
|
||||||
|
import android.os.ParcelUuid;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -24,6 +28,7 @@ import java.util.UUID;
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
import nodomain.freeyourgadget.gadgetbridge.Logging;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.service.AbstractDeviceSupport;
|
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.
|
* 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 BtBRQueue mQueue;
|
||||||
private UUID mSupportedService = null;
|
private UUID mSupportedService = null;
|
||||||
private int mBufferSize = 1024;
|
private final int mBufferSize;
|
||||||
private final Logger logger;
|
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.logger = logger;
|
||||||
|
this.mBufferSize = bufferSize;
|
||||||
if (logger == null) {
|
if (logger == null) {
|
||||||
throw new IllegalArgumentException("logger must not be null");
|
throw new IllegalArgumentException("logger must not be null");
|
||||||
}
|
}
|
||||||
@@ -57,6 +66,21 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
|
|||||||
synchronized (ConnectionMonitor) {
|
synchronized (ConnectionMonitor) {
|
||||||
final UUID supportedService = getSupportedService();
|
final UUID supportedService = getSupportedService();
|
||||||
if (supportedService == null) {
|
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");
|
throw new NullPointerException("No supported service UUID specified");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,10 +160,6 @@ public abstract class AbstractBTBRDeviceSupport extends AbstractDeviceSupport im
|
|||||||
return mSupportedService;
|
return mSupportedService;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void setBufferSize(int bufferSize) {
|
|
||||||
mBufferSize = bufferSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected int getBufferSize() {
|
protected int getBufferSize() {
|
||||||
return mBufferSize;
|
return mBufferSize;
|
||||||
}
|
}
|
||||||
|
|||||||
-2
@@ -26,8 +26,6 @@ public interface SocketCallback {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Read data from InputStream of BluetoothSocket
|
* Read data from InputStream of BluetoothSocket
|
||||||
*
|
|
||||||
* @param data
|
|
||||||
*/
|
*/
|
||||||
void onSocketRead(byte[] 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.
|
* Invokes a write operation on a given socket.
|
||||||
* The result status will be made available asynchronously through the
|
* The result status will be made available asynchronously through the
|
||||||
* {@link SocketCallback}
|
* {@link nodomain.freeyourgadget.gadgetbridge.service.btbr.SocketCallback}
|
||||||
*/
|
*/
|
||||||
public class WriteAction extends BtBRAction {
|
public class WriteAction extends BtBRAction {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class);
|
private static final Logger LOG = LoggerFactory.getLogger(WriteAction.class);
|
||||||
@@ -59,7 +59,7 @@ public class WriteAction extends BtBRAction {
|
|||||||
|
|
||||||
protected boolean writeValue(byte[] value) {
|
protected boolean writeValue(byte[] value) {
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug("writing to socket: " + Logging.formatBytes(value));
|
LOG.debug("writing to socket: {}", Logging.formatBytes(value));
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mOutputStream.write(value);
|
mOutputStream.write(value);
|
||||||
|
|||||||
+5
@@ -485,6 +485,11 @@ public class BleNamesResolver {
|
|||||||
mServices.put("0000185a-0000-1000-8000-00805f9b34fb", "Industrial Measurement Device");
|
mServices.put("0000185a-0000-1000-8000-00805f9b34fb", "Industrial Measurement Device");
|
||||||
mServices.put("0000185b-0000-1000-8000-00805f9b34fb", "Ranging");
|
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("0000fdab-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Proximity Unlock Service)");
|
||||||
mServices.put("0000fe95-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Wear Service)");
|
mServices.put("0000fe95-0000-1000-8000-00805f9b34fb", "(Propr: Xiaomi Wear Service)");
|
||||||
mServices.put("0000fee0-0000-3512-2118-0009af100700", "(Propr: Xiaomi MiLi 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();
|
private final BroadcastReceiver commandReceiver = new AAWirelessCommandReceiver();
|
||||||
|
|
||||||
public AAWirelessSupport() {
|
public AAWirelessSupport() {
|
||||||
super(LOG);
|
super(LOG, MAX_MTU);
|
||||||
addSupportedService(UUID_SERVICE_AAWIRELESS);
|
addSupportedService(UUID_SERVICE_AAWIRELESS);
|
||||||
setBufferSize(MAX_MTU);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
+1
-2
@@ -101,9 +101,8 @@ public class ZeppOsBtbrSupport extends AbstractBTBRDeviceSupport implements Zepp
|
|||||||
private long lastWrite = -1;
|
private long lastWrite = -1;
|
||||||
|
|
||||||
public ZeppOsBtbrSupport() {
|
public ZeppOsBtbrSupport() {
|
||||||
super(LOG);
|
super(LOG, MAX_MTU);
|
||||||
addSupportedService(HuamiService.UUID_BT_SERIAL_SERVICE);
|
addSupportedService(HuamiService.UUID_BT_SERIAL_SERVICE);
|
||||||
setBufferSize(MAX_MTU);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
-6
@@ -17,17 +17,11 @@
|
|||||||
|
|
||||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.huawei;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
|
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
import nodomain.freeyourgadget.gadgetbridge.devices.huawei.HuaweiConstants;
|
||||||
|
|
||||||
public class HonorBRSupport extends HuaweiBRSupport {
|
public class HonorBRSupport extends HuaweiBRSupport {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(HonorBRSupport.class);
|
|
||||||
public HonorBRSupport() {
|
public HonorBRSupport() {
|
||||||
super();
|
super();
|
||||||
addSupportedService(HuaweiConstants.UUID_SERVICE_HONOR_SDP);
|
addSupportedService(HuaweiConstants.UUID_SERVICE_HONOR_SDP);
|
||||||
setBufferSize(1032);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-2
@@ -49,9 +49,8 @@ public class HuaweiBRSupport extends AbstractBTBRDeviceSupport {
|
|||||||
private final HuaweiSupportProvider supportProvider;
|
private final HuaweiSupportProvider supportProvider;
|
||||||
|
|
||||||
public HuaweiBRSupport() {
|
public HuaweiBRSupport() {
|
||||||
super(LOG);
|
super(LOG, 1032);
|
||||||
addSupportedService(HuaweiConstants.UUID_SERVICE_HUAWEI_SDP);
|
addSupportedService(HuaweiConstants.UUID_SERVICE_HUAWEI_SDP);
|
||||||
setBufferSize(1032);
|
|
||||||
supportProvider = new HuaweiSupportProvider(this);
|
supportProvider = new HuaweiSupportProvider(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
-1
@@ -56,7 +56,6 @@ public class HuaweiFreebudsSupport extends HuaweiBRSupport implements HeadphoneH
|
|||||||
public HuaweiFreebudsSupport() {
|
public HuaweiFreebudsSupport() {
|
||||||
super();
|
super();
|
||||||
addSupportedService(UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"));
|
addSupportedService(UUID.fromString("00001101-0000-1000-8000-00805f9b34fb"));
|
||||||
setBufferSize(1032);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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 {
|
public class XiaomiSppSupport extends XiaomiConnectionSupport {
|
||||||
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSppSupport.class);
|
private static final Logger LOG = LoggerFactory.getLogger(XiaomiSppSupport.class);
|
||||||
|
|
||||||
AbstractBTBRDeviceSupport commsSupport = new AbstractBTBRDeviceSupport(LOG) {
|
AbstractBTBRDeviceSupport commsSupport = new AbstractBTBRDeviceSupport(LOG, 1024) {
|
||||||
@Override
|
@Override
|
||||||
public boolean useAutoConnect() {
|
public boolean useAutoConnect() {
|
||||||
return mXiaomiSupport.useAutoConnect();
|
return mXiaomiSupport.useAutoConnect();
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ public class CheckSums {
|
|||||||
public static int getCRC16(byte[] seq) {
|
public static int getCRC16(byte[] seq) {
|
||||||
return getCRC16(seq, 0xFFFF);
|
return getCRC16(seq, 0xFFFF);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getCRC16(byte[] seq, int crc) {
|
public static int getCRC16(byte[] seq, int crc) {
|
||||||
for (byte b : seq) {
|
for (byte b : seq) {
|
||||||
crc = ((crc >>> 8) | (crc << 8)) & 0xffff;
|
crc = ((crc >>> 8) | (crc << 8)) & 0xffff;
|
||||||
@@ -71,7 +71,7 @@ public class CheckSums {
|
|||||||
crc &= 0xffff;
|
crc &= 0xffff;
|
||||||
return crc;
|
return crc;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static int getCRC16ansi(byte[] seq) {
|
public static int getCRC16ansi(byte[] seq) {
|
||||||
int crc = 0xffff;
|
int crc = 0xffff;
|
||||||
int polynomial = 0xA001;
|
int polynomial = 0xA001;
|
||||||
@@ -190,4 +190,24 @@ public class CheckSums {
|
|||||||
md.update(str.getBytes(StandardCharsets.UTF_8));
|
md.update(str.getBytes(StandardCharsets.UTF_8));
|
||||||
return GB.hexdump(md.digest()).toLowerCase(Locale.ROOT);
|
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="ms_MY">@string/bahasa_melayu</item>
|
||||||
<item name="fa_IR">@string/persian</item>
|
<item name="fa_IR">@string/persian</item>
|
||||||
<!-- simplified versions, without a country -->
|
<!-- simplified versions, without a country -->
|
||||||
|
<item name="zh">@string/chinese</item>
|
||||||
<item name="en">@string/english</item>
|
<item name="en">@string/english</item>
|
||||||
<item name="ko">@string/korean</item>
|
<item name="ko">@string/korean</item>
|
||||||
<item name="ja">@string/japanese</item>
|
<item name="ja">@string/japanese</item>
|
||||||
@@ -2773,6 +2774,7 @@
|
|||||||
<item>ms_MY</item>
|
<item>ms_MY</item>
|
||||||
<item>fa_IR</item>
|
<item>fa_IR</item>
|
||||||
<!-- simplified versions, without a country -->
|
<!-- simplified versions, without a country -->
|
||||||
|
<item>zh</item>
|
||||||
<item>en</item>
|
<item>en</item>
|
||||||
<item>ko</item>
|
<item>ko</item>
|
||||||
<item>ja</item>
|
<item>ja</item>
|
||||||
@@ -4119,6 +4121,53 @@
|
|||||||
<item>custom_2</item>
|
<item>custom_2</item>
|
||||||
</string-array>
|
</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">
|
<string-array name="sony_automatic_power_off_names">
|
||||||
<item>@string/sony_automatic_power_off_off</item>
|
<item>@string/sony_automatic_power_off_off</item>
|
||||||
<item>@string/sony_automatic_power_off_5_min</item>
|
<item>@string/sony_automatic_power_off_5_min</item>
|
||||||
|
|||||||
@@ -1208,6 +1208,7 @@
|
|||||||
<string name="manual">Manual</string>
|
<string name="manual">Manual</string>
|
||||||
<string name="simplified_chinese">Chinese (Simplified)</string>
|
<string name="simplified_chinese">Chinese (Simplified)</string>
|
||||||
<string name="traditional_chinese">Chinese (Traditional)</string>
|
<string name="traditional_chinese">Chinese (Traditional)</string>
|
||||||
|
<string name="chinese">Chinese</string>
|
||||||
<string name="english">English</string>
|
<string name="english">English</string>
|
||||||
<string name="english_au">English (Australia)</string>
|
<string name="english_au">English (Australia)</string>
|
||||||
<string name="english_ca">English (Canada)</string>
|
<string name="english_ca">English (Canada)</string>
|
||||||
@@ -4205,4 +4206,15 @@
|
|||||||
<string name="help">Help</string>
|
<string name="help">Help</string>
|
||||||
<string name="cannot_open_help_url">Could not open URL</string>
|
<string name="cannot_open_help_url">Could not open URL</string>
|
||||||
<string name="auth_settings">Authentication settings</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>
|
</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