From ac8d1ed6a0a8ad82a61bb77dc254c1ea2a5b1709 Mon Sep 17 00:00:00 2001 From: ahormann Date: Tue, 7 May 2024 22:39:13 +0000 Subject: [PATCH] New Device Soundcore Liberty 3 Pro (#3753) Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3753 Co-authored-by: ahormann Co-committed-by: ahormann --- .../DeviceSettingsPreferenceConst.java | 19 + .../DeviceSpecificSettingsFragment.java | 21 + .../DeviceSpecificSettingsScreen.java | 1 + .../SoundcoreLiberty3ProCoordinator.java | 83 ++++ .../gadgetbridge/model/DeviceType.java | 2 + .../SoundcoreLiberty3ProDeviceSupport.java | 33 ++ .../soundcore/SoundcoreLibertyIOThread.java | 49 +++ .../soundcore/SoundcoreLibertyProtocol.java | 377 ++++++++++++++++++ .../service/devices/soundcore/TapAction.java | 19 + .../devices/soundcore/TapFunction.java | 21 + app/src/main/res/values/arrays.xml | 21 + app/src/main/res/values/strings.xml | 7 + .../xml/devicesettings_root_touch_options.xml | 9 + .../devicesettings_soundcore_headphones.xml | 76 ++++ ...devicesettings_soundcore_touch_options.xml | 107 +++++ 15 files changed, 845 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/SoundcoreLiberty3ProCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLiberty3ProDeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyIOThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyProtocol.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapAction.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapFunction.java create mode 100644 app/src/main/res/xml/devicesettings_root_touch_options.xml create mode 100644 app/src/main/res/xml/devicesettings_soundcore_headphones.xml create mode 100644 app/src/main/res/xml/devicesettings_soundcore_touch_options.xml diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java index aa1c26074..50a0d9284 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSettingsPreferenceConst.java @@ -343,6 +343,25 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_SONY_PROTOCOL_VERSION = "pref_protocol_version"; public static final String PREF_SONY_ACTUAL_PROTOCOL_VERSION = "pref_actual_protocol_version"; public static final String PREF_SONY_AMBIENT_SOUND_CONTROL = "pref_sony_ambient_sound_control"; + public static final String PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL = "pref_soundcore_ambient_sound_control"; + public static final String PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING = "pref_adaptive_noise_cancelling"; + public static final String PREF_SOUNDCORE_WIND_NOISE_REDUCTION= "pref_soundcore_wind_noise_reduction"; + public static final String PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE = "pref_soundcore_transparency_vocal_mode"; + public static final String PREF_SOUNDCORE_WEARING_DETECTION = "pref_soundcore_wearing_detection"; + public static final String PREF_SOUNDCORE_WEARING_TONE = "pref_soundcore_wearing_tone"; + public static final String PREF_SOUNDCORE_TOUCH_TONE = "pref_soundcore_touch_tone"; + public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED = "pref_soundcore_control_single_tap_disabled"; + public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED = "pref_soundcore_control_double_tap_disabled"; + public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED = "pref_soundcore_control_triple_tap_disabled"; + public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED = "pref_soundcore_control_long_press_disabled"; + public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT = "pref_soundcore_control_single_tap_action_left"; + public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT = "pref_soundcore_control_double_tap_action_left"; + public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT = "pref_soundcore_control_triple_tap_action_left"; + public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT = "pref_soundcore_control_long_press_action_left"; + public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT = "pref_soundcore_control_single_tap_action_right"; + public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT = "pref_soundcore_control_double_tap_action_right"; + public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT = "pref_soundcore_control_triple_tap_action_right"; + public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT = "pref_soundcore_control_long_press_action_right"; public static final String PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE = "pref_sony_ambient_sound_control_button_mode"; public static final String PREF_SONY_FOCUS_VOICE = "pref_sony_focus_voice"; public static final String PREF_SONY_AMBIENT_SOUND_LEVEL = "pref_sony_ambient_sound_level"; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java index a8e2aec73..5615318d7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsFragment.java @@ -563,6 +563,27 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES); addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL); addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP); + + addPreferenceHandlerFor(PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL); + addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION); + addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE); + addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING); + addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE); + addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE); + addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT); + addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT); + addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE); addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java index 39df4ce33..1f01c9dae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/activities/devicesettings/DeviceSpecificSettingsScreen.java @@ -34,6 +34,7 @@ public enum DeviceSpecificSettingsScreen { DATE_TIME("pref_screen_date_time", R.xml.devicesettings_root_date_time), WORKOUT("pref_screen_workout", R.xml.devicesettings_root_workout), HEALTH("pref_screen_health", R.xml.devicesettings_root_health), + TOUCH_OPTIONS("pref_screen_touch_options", R.xml.devicesettings_root_touch_options), ; private final String key; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/SoundcoreLiberty3ProCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/SoundcoreLiberty3ProCoordinator.java new file mode 100644 index 000000000..096db6a3e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/SoundcoreLiberty3ProCoordinator.java @@ -0,0 +1,83 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.soundcore; + +import androidx.annotation.NonNull; + +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcoreLiberty3ProDeviceSupport; + +public class SoundcoreLiberty3ProCoordinator extends AbstractDeviceCoordinator { + @Override + public int getDeviceNameResource() { + return R.string.devicetype_soundcore_liberty3_pro; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_galaxy_buds; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_galaxy_buds_disabled; + } + + @Override + public String getManufacturer() { + return "Anker"; + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("Soundcore Liberty 3 Pro"); + } + + @Override + public int getBondingStyle(){ + return BONDING_STYLE_NONE; + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + } + + + @Override + public int getBatteryCount() { + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig() { + BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_buds_pro_case, R.string.battery_case); + BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_l, R.string.left_earbud); + BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_nothing_ear_r, R.string.right_earbud); + return new BatteryConfig[]{battery1, battery2, battery3}; + } + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings(); + deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS); + deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_sony_headphones_ambient_sound_control_button_modes); + deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_soundcore_touch_options); + deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_headphones); + return deviceSpecificSettings; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return SoundcoreLiberty3ProDeviceSupport.class; + } +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index a65e963b4..883dbd015 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -168,6 +168,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; @@ -348,6 +349,7 @@ public enum DeviceType { SONY_LINKBUDS_S(SonyLinkBudsSCoordinator.class), SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class), SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class), + SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class), BOSE_QC35(QC35Coordinator.class), HONORBAND3(HonorBand3Coordinator.class), HONORBAND4(HonorBand4Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLiberty3ProDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLiberty3ProDeviceSupport.java new file mode 100644 index 000000000..8a8d04ed0 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLiberty3ProDeviceSupport.java @@ -0,0 +1,33 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; + +import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class SoundcoreLiberty3ProDeviceSupport extends AbstractSerialDeviceSupport { + + @Override + public boolean connect() { + getDeviceIOThread().start(); + return true; + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new SoundcoreLibertyProtocol(getDevice()); + } + + @Override + protected synchronized GBDeviceIoThread createDeviceIOThread() { + return new SoundcoreLibertyIOThread(getDevice(), getContext(), + (SoundcoreLibertyProtocol) getDeviceProtocol(), + SoundcoreLiberty3ProDeviceSupport.this, getBluetoothAdapter()); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyIOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyIOThread.java new file mode 100644 index 000000000..4d1da7e81 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyIOThread.java @@ -0,0 +1,49 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.os.ParcelUuid; + +import androidx.annotation.NonNull; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; + +public class SoundcoreLibertyIOThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyIOThread.class); + private final SoundcoreLibertyProtocol mSoundcoreProtocol; + + public SoundcoreLibertyIOThread(GBDevice gbDevice, Context context, SoundcoreLibertyProtocol deviceProtocol, SoundcoreLiberty3ProDeviceSupport deviceSupport, BluetoothAdapter btAdapter) { + super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter); + mSoundcoreProtocol = deviceProtocol; + } + + @Override + protected void initialize() { + write(mSoundcoreProtocol.encodeDeviceInfoRequest()); + setUpdateState(GBDevice.State.INITIALIZED); + } + + @NonNull + protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { + return mSoundcoreProtocol.UUID_DEVICE_CTRL; + } + + @Override + protected byte[] parseIncoming(InputStream inStream) throws IOException { + byte[] buffer = new byte[1048576]; //HUGE read + int bytes = inStream.read(buffer); + LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes)); + return Arrays.copyOf(buffer, bytes); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyProtocol.java new file mode 100644 index 000000000..d1906d9b9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcoreLibertyProtocol.java @@ -0,0 +1,377 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +import android.content.SharedPreferences; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +public class SoundcoreLibertyProtocol extends GBDeviceProtocol { + + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyProtocol.class); + + private static final int battery_case = 0; + private static final int battery_earphone_left = 1; + private static final int battery_earphone_right = 2; + + final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3952"); + + protected SoundcoreLibertyProtocol(GBDevice device) { + super(device); + } + + private GBDeviceEventBatteryInfo buildBatteryInfo(int batteryIndex, int level) { + GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo(); + info.batteryIndex = batteryIndex; + info.level = level; + return info; + } + + private GBDeviceEventVersionInfo buildVersionInfo(String firmware1, String firmware2, String serialNumber) { + GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo(); + info.hwVersion = serialNumber; + info.fwVersion = firmware1; + info.fwVersion2 = firmware2; + return info; + } + + private String readString(byte[] data, int position, int size) { + if (position + size > data.length) throw new IllegalStateException(); + return new String(data, position, size, StandardCharsets.UTF_8); + } + @Override + public GBDeviceEvent[] decodeResponse(byte[] responseData) { + // Byte 0-4: Header + // Byte 5-6: Command (Audio-Mode) + // Byte 7: Size of data + // Byte 8-(x-1): Data + // Byte x: Checksum + if (responseData.length == 0) return null; + + List devEvts = new ArrayList<>(); + + byte[] command = Arrays.copyOfRange(responseData, 5, 7); + byte[] data = Arrays.copyOfRange(responseData, 8, responseData.length-1); + + if (Arrays.equals(command, new byte[]{0x01, 0x01})) { + // a lot of other data is in here, anything interesting? + String firmware1 = readString(data, 7, 5); + String firmware2 = readString(data, 12, 5); + String serialNumber = readString(data, 17, 16); + devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber)); + } else if (Arrays.equals(command, new byte[]{0x01, (byte) 0x8d})) { + LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData)); + } else if (Arrays.equals(command, new byte[]{0x05, (byte) 0x82})) { + LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData)); + } else if (Arrays.equals(command, new byte[]{0x05, 0x01})) { + LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData)); + } else if (Arrays.equals(command, new byte[]{0x06, 0x01})) { //Sound Mode Update + decodeAudioMode(data); + } else if (Arrays.equals(command, new byte[]{0x01, 0x03})) { // Battery Update + int batteryLeft = data[1] * 20; + int batteryRight = data[2] * 20; + int batteryCase = data[3] * 20; + + devEvts.add(buildBatteryInfo(battery_case, batteryCase)); + devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeft)); + devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRight)); + } else { + // see https://github.com/gmallios/SoundcoreManager/blob/master/soundcore-lib/src/models/packet_kind.rs + // for a mapping for other soundcore devices (similar protocol?) + LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData)); + } + return devEvts.toArray(new GBDeviceEvent[devEvts.size()]); + } + + private void decodeAudioMode(byte[] payload) { + SharedPreferences prefs = getDevicePrefs().getPreferences(); + SharedPreferences.Editor editor = prefs.edit(); + String soundmode = "off"; + int anc_strength = 0; + + if (payload[1] == 0x00) { + soundmode = "noise_cancelling"; + } else if (payload[1] == 0x01) { + soundmode = "ambient_sound"; + } else if (payload[1] == 0x02) { + soundmode = "off"; + } + + if (payload[2] == 0x10) { + anc_strength = 0; + } else if (payload[2] == 0x20) { + anc_strength = 1; + } else if (payload[2] == 0x30) { + anc_strength = 2; + } + + boolean vocal_mode = (payload[3] == 0x01); + boolean adaptive_anc = (payload[4] == 0x01); + boolean windnoiseReduction = (payload[5] == 0x01); + + editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode); + editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength); + editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode); + editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc); + editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, windnoiseReduction); + editor.apply(); + } + + @Override + public byte[] encodeSendConfiguration(String config) { + Prefs prefs = getDevicePrefs(); + String pref_string; + + switch (config) { + // Ambient Sound Modes + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL: + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION: + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE: + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING: + case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL: + return encodeAudioMode(); + + // Control + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED: + return encodeControlTouchLockMessage(TapAction.SINGLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED, false)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED: + return encodeControlTouchLockMessage(TapAction.DOUBLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED, false)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED: + return encodeControlTouchLockMessage(TapAction.TRIPLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED, false)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED: + return encodeControlTouchLockMessage(TapAction.LONG_PRESS, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED, false)); + + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT, ""); + return encodeControlFunctionMessage(TapAction.SINGLE_TAP, false, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT, ""); + return encodeControlFunctionMessage(TapAction.SINGLE_TAP, true, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT, ""); + return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, false, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT, ""); + return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, true, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT, ""); + return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, false, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT, ""); + return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, true, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT, ""); + return encodeControlFunctionMessage(TapAction.LONG_PRESS, false, TapFunction.valueOf(pref_string)); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT: + pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT, ""); + return encodeControlFunctionMessage(TapAction.LONG_PRESS, true, TapFunction.valueOf(pref_string)); + + case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE: + AmbientSoundControlButtonMode modes = AmbientSoundControlButtonMode.fromPreferences(prefs.getPreferences()); + switch (modes) { + case NC_AS_OFF: + return encodeControlAmbientModeMessage(true, true, true); + case NC_AS: + return encodeControlAmbientModeMessage(true, true, false); + case NC_OFF: + return encodeControlAmbientModeMessage(true, false, true); + case AS_OFF: + return encodeControlAmbientModeMessage(false, true, true); + } + + // Miscellaneous Settings + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION: + boolean wearingDetection = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION, false); + return encodeMessage((byte) 0x01, (byte) 0x81, new byte[]{0x00, encodeBoolean(wearingDetection)}); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE: + boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false); + return encodeMessage((byte) 0x01, (byte) 0x8c, new byte[]{0x00, encodeBoolean(wearingTone)}); + case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE: + boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false); + return encodeMessage((byte) 0x01, (byte) 0x83, new byte[]{0x00, encodeBoolean(touchTone)}); + default: + LOG.debug("Unsupported CONFIG: " + config); + } + + return super.encodeSendConfiguration(config); + } + + byte[] encodeDeviceInfoRequest() { + byte[] payload = new byte[]{0x00}; + return encodeMessage((byte) 0x01, (byte) 0x01, payload); + } + + byte[] encodeMysteryDataRequest1() { + byte[] payload = new byte[]{0x00, 0x00}; + return encodeMessage((byte) 0x01, (byte) 0x8d, payload); + } + byte[] encodeMysteryDataRequest2() { + byte[] payload = new byte[]{0x00}; + return encodeMessage((byte) 0x05, (byte) 0x01, payload); + } + byte[] encodeMysteryDataRequest3() { + byte[] payload = new byte[]{0x00, 0x00}; + return encodeMessage((byte) 0x05, (byte) 0x82, payload); + } + + /** + * Encodes the following settings to a payload to set the audio-mode on the headphones: + * PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active + * PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise + * PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode + * PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent + * PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise + * @return The payload + */ + private byte[] encodeAudioMode() { + Prefs prefs = getDevicePrefs(); + + byte anc_mode; + switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) { + case "noise_cancelling": + anc_mode = 0x00; + break; + case "ambient_sound": + anc_mode = 0x01; + break; + case "off": + anc_mode = 0x02; + break; + default: + LOG.error("Invalid Audio Mode selected"); + return null; + } + + byte anc_strength; + switch (prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0)) { + case 0: + anc_strength = 0x10; + break; + case 1: + anc_strength = 0x20; + break; + case 2: + anc_strength = 0x30; + break; + default: + LOG.error("Invalid ANC Strength selected"); + return null; + } + + byte adaptive_anc = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, true)); + byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false)); + byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false)); + + byte[] payload = new byte[]{0x00, anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01}; + return encodeMessage((byte) 0x06, (byte) 0x81, payload); + } + + /** + * Enables or disables a tap-action + * @param action The byte that encodes the action (single/double/triple or long tap) + * @param disabled If the action should be enabled or disabled + * @return + */ + private byte[] encodeControlTouchLockMessage(TapAction action, boolean disabled) { + boolean enabled = !disabled; + byte enabled_byte; + byte[] payload; + switch (action) { + case SINGLE_TAP: + case TRIPLE_TAP: + enabled_byte = encodeBoolean(enabled); + break; + case DOUBLE_TAP: + case LONG_PRESS: + enabled_byte = enabled?(byte) 0x11: (byte) 0x10; + break; + default: + LOG.error("Invalid Tap action"); + return null; + } + payload = new byte[]{0x00, 0x00, action.getCode(), enabled_byte}; + return encodeMessage((byte) 0x04, (byte) 0x83, payload); + } + + /** + * Assigns a function (eg play/pause) to an action (eg single tap on right bud) + * @param action The byte that encodes the action (single/double/triple or long tap) + * @param right If the right or left earbud is meant + * @param function The byte that encodes the triggered function (eg play/pause) + * @return The encoded message + */ + private byte[] encodeControlFunctionMessage(TapAction action, boolean right, TapFunction function) { + byte function_byte; + switch (action) { + case SINGLE_TAP: + case DOUBLE_TAP: + function_byte = (byte) (16*6 + function.getCode()); + break; + case TRIPLE_TAP: + function_byte = (byte) (16*4 + function.getCode()); + break; + case LONG_PRESS: + function_byte = (byte) (16*5 + function.getCode()); + break; + default: + LOG.error("Invalid Tap action"); + return null; + } + byte[] payload = new byte[] {0x00, encodeBoolean(right), action.getCode(), function_byte}; + return encodeMessage((byte) 0x04, (byte) 0x81, payload); + } + + /** + * Encodes between which Audio Modes a tap should switch, if it is set to switch the Audio Mode. + * Zb ANC -> -> Transparency -> Normal -> ANC -> .... + */ + private byte[] encodeControlAmbientModeMessage(boolean anc, boolean transparency, boolean normal) { + // Original app does not allow only one true flag. Unsure if Earbuds accept this state. + byte ambientModes = (byte) (4 * (normal?1:0) + 2 * (transparency?1:0) + (anc?1:0)); + return encodeMessage((byte) 0x06, (byte) 0x82, new byte[] {0x00, ambientModes}); + } + + private byte encodeBoolean(boolean bool) { + if (bool) return 0x01; + else return 0x00; + } + + private byte[] encodeMessage(byte command1, byte command2, byte[] payload) { + int size = 8 + payload.length + 1; + ByteBuffer msgBuf = ByteBuffer.allocate(size); + msgBuf.order(ByteOrder.BIG_ENDIAN); + msgBuf.put(new byte[] {0x08, (byte) 0xee, 0x00, 0x00, 0x00}); // header + msgBuf.put(command1); + msgBuf.put(command2); + msgBuf.put((byte) size); + + msgBuf.put(payload); + + byte checksum = -10; + checksum += command1 + command2 + size; + for (int b : payload) { + checksum += b; + } + msgBuf.put(checksum); + + return msgBuf.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapAction.java new file mode 100644 index 000000000..7e75170f9 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapAction.java @@ -0,0 +1,19 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; + +enum TapAction { + SINGLE_TAP((byte) 0x02), + DOUBLE_TAP((byte) 0x00), + TRIPLE_TAP((byte) 0x05), + LONG_PRESS((byte) 0x01) + ; + + private final byte code; + + TapAction(final byte code) { + this.code = code; + } + + public byte getCode() { + return code; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapFunction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapFunction.java new file mode 100644 index 000000000..abdaa9534 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/TapFunction.java @@ -0,0 +1,21 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; +enum TapFunction { + VOLUME_DOWN(1), + VOLUME_UP(0), + MEDIA_NEXT( 3), + MEDIA_PREV(2), + PLAYPAUSE(6), + VOICE_ASSISTANT(5), + AMBIENT_SOUND_CONTROL(4) + ; + + private final int code; + + TapFunction(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } +} \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index aa0fcb056..ec81c7c4e 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3441,6 +3441,27 @@ as_off + + @string/pref_media_volumedown + @string/pref_media_volumeup + @string/pref_media_next + @string/pref_media_previous + @string/pref_media_playpause + @string/pref_title_touch_voice_assistant + @string/sony_button_mode_ambient_sound_control + + + + VOLUME_DOWN + VOLUME_UP + MEDIA_NEXT + MEDIA_PREV + PLAYPAUSE + VOICE_ASSISTANT + AMBIENT_SOUND_CONTROL + + + de.dennisguse.opentracks de.dennisguse.opentracks.playStore diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 5185cc9cc..60b23d8bc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -520,6 +520,10 @@ Satellite Search Crown Vibration Alert Tone + Touch Tone + Plays a tone when the earbud is touched + Wearing Tone + Plays a tone when the earbud is inserted Cover to Mute Vibrate for Alert Text to Speech @@ -1533,6 +1537,7 @@ Sony WI-SP600N Sony LinkBuds Sony LinkBuds S + Soundcore Liberty 3 Pro Binary sensor Honor Band 3 Honor Band 4 @@ -2284,6 +2289,8 @@ Wide area tap Increase volume automatically when ambient sound is loud Adaptive volume control + Adaptive ANC + Set the strength of the ANC automatically depending on the ambient sound level Speak-to-chat Turn off noise cancelling automatically when you start talking. Voice Detection Sensitivity diff --git a/app/src/main/res/xml/devicesettings_root_touch_options.xml b/app/src/main/res/xml/devicesettings_root_touch_options.xml new file mode 100644 index 000000000..de5a20468 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_root_touch_options.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/xml/devicesettings_soundcore_headphones.xml b/app/src/main/res/xml/devicesettings_soundcore_headphones.xml new file mode 100644 index 000000000..8c5ba11b2 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_soundcore_headphones.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/xml/devicesettings_soundcore_touch_options.xml b/app/src/main/res/xml/devicesettings_soundcore_touch_options.xml new file mode 100644 index 000000000..5b8cb966b --- /dev/null +++ b/app/src/main/res/xml/devicesettings_soundcore_touch_options.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + +