From be3f8375f9c3adebde30f28da0d44dccdd3e9372 Mon Sep 17 00:00:00 2001 From: Severin von Wnuck-Lipinski Date: Wed, 7 Aug 2024 21:38:31 +0200 Subject: [PATCH] Add support for Soundcore Motion 300 --- .../DeviceSettingsPreferenceConst.java | 27 ++ .../DeviceSpecificSettingsFragment.java | 25 ++ .../SoundcoreMotion300Coordinator.java | 93 +++++ .../SoundcoreMotion300SettingsCustomizer.java | 113 +++++ .../gadgetbridge/model/DeviceType.java | 2 + .../devices/soundcore/SoundcorePacket.java | 112 +++++ .../SoundcoreMotion300DeviceSupport.java | 43 ++ .../motion300/SoundcoreMotion300IOThread.java | 75 ++++ .../motion300/SoundcoreMotion300Protocol.java | 385 ++++++++++++++++++ app/src/main/res/values/arrays.xml | 186 +++++++++ app/src/main/res/values/strings.xml | 36 ++ .../devicesettings_soundcore_motion300.xml | 23 ++ ...vicesettings_soundcore_motion300_audio.xml | 201 +++++++++ 13 files changed, 1321 insertions(+) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java create mode 100644 app/src/main/res/xml/devicesettings_soundcore_motion300.xml create mode 100644 app/src/main/res/xml/devicesettings_soundcore_motion300_audio.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 da2f49302..965c358b7 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 @@ -372,6 +372,33 @@ public class DeviceSettingsPreferenceConst { 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_SOUNDCORE_VOICE_PROMPTS = "pref_soundcore_voice_prompts"; + public static final String PREF_SOUNDCORE_BUTTON_BRIGHTNESS = "pref_soundcore_button_brightness"; + public static final String PREF_SOUNDCORE_AUTO_POWER_OFF = "pref_soundcore_auto_power_off"; + public static final String PREF_SOUNDCORE_LDAC_MODE = "pref_soundcore_ldac_mode"; + public static final String PREF_SOUNDCORE_ADAPTIVE_DIRECTION = "pref_soundcore_adaptive_direction"; + public static final String PREF_SOUNDCORE_EQUALIZER_PRESET = "pref_soundcore_equalizer_preset"; + public static final String PREF_SOUNDCORE_EQUALIZER_CUSTOM = "pref_soundcore_equalizer_custom"; + public static final String PREF_SOUNDCORE_EQUALIZER_DIRECTION = "pref_soundcore_equalizer_direction"; + public static final String PREF_SOUNDCORE_EQUALIZER_RESET = "pref_soundcore_equalizer_reset"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ = "pref_soundcore_equalizer_band1_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE = "pref_soundcore_equalizer_band1_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ = "pref_soundcore_equalizer_band2_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE = "pref_soundcore_equalizer_band2_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ = "pref_soundcore_equalizer_band3_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE = "pref_soundcore_equalizer_band3_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ = "pref_soundcore_equalizer_band4_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE = "pref_soundcore_equalizer_band4_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ = "pref_soundcore_equalizer_band5_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE = "pref_soundcore_equalizer_band5_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ = "pref_soundcore_equalizer_band6_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE = "pref_soundcore_equalizer_band6_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ = "pref_soundcore_equalizer_band7_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE = "pref_soundcore_equalizer_band7_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ = "pref_soundcore_equalizer_band8_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE = "pref_soundcore_equalizer_band8_value"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ = "pref_soundcore_equalizer_band9_freq"; + public static final String PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE = "pref_soundcore_equalizer_band9_value"; 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 2b6810132..04a7523ef 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 @@ -715,6 +715,31 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i 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_SOUNDCORE_VOICE_PROMPTS); + addPreferenceHandlerFor(PREF_SOUNDCORE_BUTTON_BRIGHTNESS); + addPreferenceHandlerFor(PREF_SOUNDCORE_AUTO_POWER_OFF); + addPreferenceHandlerFor(PREF_SOUNDCORE_LDAC_MODE); + addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_DIRECTION); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_PRESET); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_DIRECTION); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ); + addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE); addPreferenceHandlerFor(PREF_MOONDROP_EQUALIZER_PRESET); addPreferenceHandlerFor(PREF_MOONDROP_TOUCH_PLAY_PAUSE_EARBUD); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java new file mode 100644 index 000000000..1299e86d1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300Coordinator.java @@ -0,0 +1,93 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300; + +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.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300.SoundcoreMotion300DeviceSupport; + +public class SoundcoreMotion300Coordinator extends AbstractBLClassicDeviceCoordinator { + @Override + public String getManufacturer() { + return "Anker"; + } + + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("soundcore Motion 300"); + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_soundcore_motion300; + } + + @Override + public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { + return new SoundcoreMotion300SettingsCustomizer(); + } + + @Override + public int getBatteryCount() { + return 1; + } + + @Override + public boolean supportsPowerOff() { + return true; + } + + @Override + protected void deleteDevice( + @NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) + throws GBException {} + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings settings = new DeviceSpecificSettings(); + + settings.addRootScreen(R.xml.devicesettings_soundcore_motion300); + settings.addRootScreen(DeviceSpecificSettingsScreen.AUDIO); + settings.addSubScreen( + DeviceSpecificSettingsScreen.AUDIO, + R.xml.devicesettings_soundcore_motion300_audio); + settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); + settings.addSubScreen( + DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, + R.xml.devicesettings_headphones); + + return settings; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return SoundcoreMotion300DeviceSupport.class; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java new file mode 100644 index 000000000..251fae92c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/motion300/SoundcoreMotion300SettingsCustomizer.java @@ -0,0 +1,113 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300; + +import android.os.Parcel; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.SeekBarPreference; + +import java.util.Collections; +import java.util.Set; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300.SoundcoreMotion300Protocol; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*; + +public class SoundcoreMotion300SettingsCustomizer implements DeviceSpecificSettingsCustomizer { + public static final Creator CREATOR = new Creator() { + @Override + public SoundcoreMotion300SettingsCustomizer createFromParcel(final Parcel in) { + return new SoundcoreMotion300SettingsCustomizer(); + } + + @Override + public SoundcoreMotion300SettingsCustomizer[] newArray(final int size) { + return new SoundcoreMotion300SettingsCustomizer[size]; + } + }; + + @Override + public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { + if (!preference.getKey().equals(PREF_SOUNDCORE_EQUALIZER_PRESET)) + return; + + String preset = ((ListPreference)preference).getEntry().toString(); + Preference pref = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_CUSTOM); + boolean customEnabled = preset.equals(handler.getContext().getString(R.string.custom)); + + if (pref != null) + pref.setEnabled(customEnabled); + } + + @Override + public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs) { + ListPreference equalizerDirection = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_DIRECTION); + + if (equalizerDirection != null) { + boolean enabled = prefs.getBoolean(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, true); + + equalizerDirection.setVisible(enabled); + } + + Preference equalizerReset = handler.findPreference(PREF_SOUNDCORE_EQUALIZER_RESET); + + if (equalizerReset != null) + equalizerReset.setOnPreferenceClickListener(pref -> resetEqualizer(handler)); + } + + private boolean resetEqualizer(final DeviceSpecificSettingsHandler handler) { + // Reset all bands to default settings + for (int i = 0; i < SoundcoreMotion300Protocol.EQUALIZER_PREFS_FREQ.length; i++) { + ListPreference prefFreq = handler.findPreference(SoundcoreMotion300Protocol.EQUALIZER_PREFS_FREQ[i]); + SeekBarPreference prefValue = handler.findPreference(SoundcoreMotion300Protocol.EQUALIZER_PREFS_VALUE[i]); + + // Neutral configuration + prefValue.setValue(60); + + // Default configuration: 80 Hz, 150 Hz, 300 Hz, 600 Hz, 1.2 kHz, 2.5 kHz, 5 kHz, 9 kHz, 13 kHz + if (i < 7) + prefFreq.setValue("7"); + else if (i == 7) + prefFreq.setValue("6"); + else if (i == 8) + prefFreq.setValue("1"); + } + + // Send updated equalizer configuration + handler.notifyPreferenceChanged(PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ); + + return true; + } + + @Override + public Set getPreferenceKeysWithSummary() { + return Collections.emptySet(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(final Parcel dest, final int flags) {} +} 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 ba21eefa4..6c858eb46 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -217,6 +217,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator; @@ -436,6 +437,7 @@ public enum DeviceType { SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class), SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class), SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class), + SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class), MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class), BOSE_QC35(QC35Coordinator.class), HONORBAND3(HonorBand3Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java new file mode 100644 index 000000000..22c5089e3 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/SoundcorePacket.java @@ -0,0 +1,112 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +public class SoundcorePacket { + private static final Logger LOG = LoggerFactory.getLogger(SoundcorePacket.class); + + private static final int HEADER_LENGTH = 10; + private static final short START_OF_PACKET_HOST = (short)0xee08; + private static final short START_OF_PACKET_DEVICE = (short)0xff09; + private static final byte DIRECTION_HOST = (byte)0x00; + private static final byte DIRECTION_DEVICE = (byte)0x01; + + private short command; + private byte[] payload; + + public SoundcorePacket(short command) { + this(command, new byte[] {}); + } + + public SoundcorePacket(short command, byte[] payload) { + LOG.debug("Packet: command={}", String.format("0x%04x", command)); + + this.command = command; + this.payload = payload; + } + + public short getCommand() { + return command; + } + + public byte[] getPayload() { + return payload; + } + + public static SoundcorePacket decode(ByteBuffer buf) { + if (buf.remaining() < HEADER_LENGTH) + return null; + + buf.order(ByteOrder.LITTLE_ENDIAN); + + if (buf.getShort() != START_OF_PACKET_DEVICE) { + LOG.error("Invalid start of packet: {}", hexdump(buf.array())); + return null; + } + + // Skip two zero bytes + buf.getShort(); + + if (buf.get() != DIRECTION_DEVICE) { + LOG.error("Invalid direction: {}", hexdump(buf.array())); + return null; + } + + short command = buf.getShort(); + short length = buf.getShort(); + + if (length < HEADER_LENGTH) { + LOG.error("Invalid length: {}", hexdump(buf.array())); + return null; + } + + // Skip checksum byte at the end + byte[] payload = new byte[length - HEADER_LENGTH]; + buf.get(payload); + + return new SoundcorePacket(command, payload); + } + + public byte[] encode() { + ByteBuffer buf = ByteBuffer.allocate(HEADER_LENGTH + payload.length); + + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putShort(START_OF_PACKET_HOST); + buf.putShort((short)0x0000); + buf.put(DIRECTION_HOST); + buf.putShort(command); + buf.putShort((short)(HEADER_LENGTH + payload.length)); + buf.put(payload); + + byte checksum = 0; + + for (byte val : buf.array()) + checksum += val; + + buf.put(checksum); + + return buf.array(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java new file mode 100644 index 000000000..487c14009 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300DeviceSupport.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300; + +import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; + +public class SoundcoreMotion300DeviceSupport extends AbstractHeadphoneDeviceSupport { + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + protected GBDeviceProtocol createDeviceProtocol() { + return new SoundcoreMotion300Protocol(getDevice()); + } + + @Override + protected GBDeviceIoThread createDeviceIOThread() { + return new SoundcoreMotion300IOThread( + getDevice(), + getContext(), + (SoundcoreMotion300Protocol)getDeviceProtocol(), + SoundcoreMotion300DeviceSupport.this, + getBluetoothAdapter()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java new file mode 100644 index 000000000..8c4894530 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300IOThread.java @@ -0,0 +1,75 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300; + +import android.bluetooth.BluetoothAdapter; +import android.content.Context; +import android.os.Handler; +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; + +import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump; + +public class SoundcoreMotion300IOThread extends BtClassicIoThread { + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreMotion300IOThread.class); + + private final SoundcoreMotion300Protocol protocol; + private final Handler handler = new Handler(); + + public SoundcoreMotion300IOThread( + GBDevice device, + Context context, + SoundcoreMotion300Protocol protocol, + SoundcoreMotion300DeviceSupport support, + BluetoothAdapter adapter) { + super(device, context, protocol, support, adapter); + this.protocol = protocol; + } + + @Override + protected void initialize() { + setUpdateState(GBDevice.State.INITIALIZING); + + // Device requires a little delay to respond to commands + handler.postDelayed(() -> write(protocol.encodeGetDeviceInfo()), 500); + } + + @NonNull + protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { + return UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3135"); + } + + @Override + protected byte[] parseIncoming(InputStream stream) throws IOException { + byte[] buffer = new byte[1048576]; + int bytes = stream.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/motion300/SoundcoreMotion300Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java new file mode 100644 index 000000000..5f218ec01 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/motion300/SoundcoreMotion300Protocol.java @@ -0,0 +1,385 @@ +/* Copyright (C) 2024 Severin von Wnuck-Lipinski + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.motion300; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket; +import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; +import nodomain.freeyourgadget.gadgetbridge.util.Prefs; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.*; + +public class SoundcoreMotion300Protocol extends GBDeviceProtocol { + private static final Logger LOG = LoggerFactory.getLogger(SoundcoreMotion300Protocol.class); + + // Some of these commands are not used right now, they serve as documentation + private static final short CMD_GET_DEVICE_INFO = (short)0x0101; + private static final short CMD_GET_LDAC_MODE = (short)0x7f01; + private static final short CMD_GET_BUTTON_BRIGHTNESS = (short)0x9310; + private static final short CMD_GET_EQUALIZER = (short)0x8902; + private static final short CMD_GET_CURRENT_DIRECTION = (short)0x8c02; + + private static final short CMD_SET_VOICE_PROMPTS = (short)0x9001; + private static final short CMD_SET_BUTTON_BRIGHTNESS = (short)0x9210; + private static final short CMD_SET_AUTO_POWER_OFF = (short)0x8601; + private static final short CMD_SET_LDAC_MODE = (short)0xff01; + private static final short CMD_SET_ADAPTIVE_DIRECTION = (short)0x8a02; + private static final short CMD_SET_EQUALIZER_PRESET = (short)0x8b02; + private static final short CMD_SET_EQUALIZER_CUSTOM = (short)0x8d02; + + private static final short CMD_NOTIFY_BATTERY_INFO = (short)0x0301; + private static final short CMD_NOTIFY_CHARGING_INFO = (short)0x0401; + private static final short CMD_NOTIFY_VOLUME_INFO = (short)0x0901; + private static final short CMD_NOTIFY_PLAYBACK_INFO = (short)0x2101; + private static final short CMD_NOTIFY_BASS_MODE = (short)0x8e02; + + private static final short CMD_POWER_OFF = (short)0x8901; + + public static final String[] EQUALIZER_PREFS_FREQ = new String[] { + PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ, + PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ + }; + + public static final String[] EQUALIZER_PREFS_VALUE = new String[] { + PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE, + PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE + }; + + private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); + + protected SoundcoreMotion300Protocol(GBDevice device) { + super(device); + } + + @Override + public GBDeviceEvent[] decodeResponse(byte[] data) { + ByteBuffer buf = ByteBuffer.wrap(data); + SoundcorePacket packet = SoundcorePacket.decode(buf); + + if (packet == null) + return null; + + short cmd = packet.getCommand(); + byte[] payload = packet.getPayload(); + + switch (cmd) { + case CMD_GET_DEVICE_INFO: + return handlePacketDeviceInfo(payload); + case CMD_GET_LDAC_MODE: + return handlePacketLdacMode(payload); + case CMD_GET_BUTTON_BRIGHTNESS: + return handlePacketButtonBrightness(payload); + case CMD_GET_EQUALIZER: + return handlePacketEqualizer(payload); + case CMD_SET_ADAPTIVE_DIRECTION: + return handlePacketAdaptiveDirection(payload); + case CMD_NOTIFY_BATTERY_INFO: + return handlePacketBatteryInfo(payload); + case CMD_NOTIFY_CHARGING_INFO: + return handlePacketChargingInfo(payload); + } + + return null; + } + + private GBDeviceEvent[] handlePacketDeviceInfo(byte[] payload) { + if (payload.length != 29) + return null; + + ByteBuffer buf = ByteBuffer.wrap(payload); + byte volume = buf.get(); + byte batteryLevel = buf.get(); + byte batteryCharging = buf.get(); + byte currentlyPlaying = buf.get(); + byte voicePrompts = buf.get(); + byte autoPowerOffEnabled = buf.get(); + byte autoPowerOffDuration = buf.get(); + byte[] firmwareBytes = new byte[5]; + byte[] serialBytes = new byte[17]; + + buf.get(firmwareBytes); + buf.get(serialBytes); + + String fwVersion = new String(firmwareBytes, StandardCharsets.UTF_8); + String serialNumber = new String(serialBytes, StandardCharsets.UTF_8); + + GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); + versionInfo.fwVersion = fwVersion; + batteryInfo.state = batteryCharging == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING; + batteryInfo.level = batteryLevel * 20; + + LOG.debug( + "Device info: volume={}, battery={}, charging={}, playing={}, prompts={}, autoOff={}, autoOffDuration={}", + volume, batteryLevel, batteryCharging, currentlyPlaying, voicePrompts, autoPowerOffEnabled, autoPowerOffDuration + ); + + return new GBDeviceEvent[] { + versionInfo, + batteryInfo, + new GBDeviceEventUpdateDeviceInfo("SERIAL: ", serialNumber), + new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_VOICE_PROMPTS, voicePrompts != (byte)0x00), + new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_AUTO_POWER_OFF, String.valueOf(autoPowerOffDuration + 1)), + new GBDeviceEventSendBytes(encodeRequest(CMD_GET_LDAC_MODE)), + }; + } + + private GBDeviceEvent[] handlePacketLdacMode(byte[] payload) { + if (payload.length != 1) + return null; + + return new GBDeviceEvent[] { + new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_LDAC_MODE, payload[0] != (byte)0x00), + new GBDeviceEventSendBytes(encodeRequest(CMD_GET_BUTTON_BRIGHTNESS)), + }; + } + + private GBDeviceEvent[] handlePacketButtonBrightness(byte[] payload) { + if (payload.length != 1) + return null; + + return new GBDeviceEvent[] { + new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_BUTTON_BRIGHTNESS, String.valueOf(payload[0])), + new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)), + }; + } + + private GBDeviceEvent[] handlePacketEqualizer(byte[] payload) { + if (payload.length != 57) + return null; + + // Get direction chosen in custom equalizer preferences + Prefs prefs = getDevicePrefs(); + int equalizerDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0")); + + // Equalizer preset might be larger than 127 -> convert to unsigned int + ByteBuffer buf = ByteBuffer.wrap(payload); + byte adaptiveDirection = buf.get(); + byte currentDirection = buf.get(); + int equalizerPreset = Byte.toUnsignedInt(buf.get()); + byte[] equalizer = new byte[18]; + + // Choose one of the three custom equalizer configurations + buf.position(buf.position() + equalizerDirection * equalizer.length); + buf.get(equalizer); + + LOG.debug( + "Equalizer: adaptiveDirection={}, direction={}, preset={}", + adaptiveDirection, currentDirection, equalizerPreset + ); + + Map newPrefs = equalizerToPrefs(equalizer); + newPrefs.put(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, adaptiveDirection != (byte)0x00); + newPrefs.put(PREF_SOUNDCORE_EQUALIZER_PRESET, String.valueOf(equalizerPreset)); + + return new GBDeviceEvent[] { + new GBDeviceEventUpdatePreferences(newPrefs), + new GBDeviceEventUpdateDeviceState(GBDevice.State.INITIALIZED), + }; + } + + private GBDeviceEvent[] handlePacketAdaptiveDirection(byte[] payload) { + if (payload.length != 0) + return null; + + Prefs prefs = getDevicePrefs(); + + // Ignore if adaptive direction is enabled + if (prefs.getBoolean(PREF_SOUNDCORE_ADAPTIVE_DIRECTION, true)) + return null; + + // Set equalizer direction to "standing" and get current configuration + return new GBDeviceEvent[] { + new GBDeviceEventUpdatePreferences(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0"), + new GBDeviceEventSendBytes(encodeRequest(CMD_GET_EQUALIZER)), + }; + } + + private GBDeviceEvent[] handlePacketBatteryInfo(byte[] payload) { + if (payload.length != 1) + return null; + + batteryInfo.level = payload[0] * 20; + + return new GBDeviceEvent[] { batteryInfo }; + } + + private GBDeviceEvent[] handlePacketChargingInfo(byte[] payload) { + if (payload.length != 1) + return null; + + batteryInfo.state = payload[0] == (byte)0x00 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_CHARGING; + + return new GBDeviceEvent[] { batteryInfo }; + } + + @Override + public byte[] encodeSendConfiguration(String config) { + Prefs prefs = getDevicePrefs(); + + switch (config) { + case PREF_SOUNDCORE_VOICE_PROMPTS: + return encodeSetBoolean(prefs, PREF_SOUNDCORE_VOICE_PROMPTS, CMD_SET_VOICE_PROMPTS); + case PREF_SOUNDCORE_BUTTON_BRIGHTNESS: + return encodeSetByte(prefs, PREF_SOUNDCORE_BUTTON_BRIGHTNESS, CMD_SET_BUTTON_BRIGHTNESS); + case PREF_SOUNDCORE_AUTO_POWER_OFF: + return encodeSetAutoPowerOff(prefs); + case PREF_SOUNDCORE_LDAC_MODE: + return encodeSetBoolean(prefs, PREF_SOUNDCORE_LDAC_MODE, CMD_SET_LDAC_MODE); + case PREF_SOUNDCORE_ADAPTIVE_DIRECTION: + return encodeSetBoolean(prefs, PREF_SOUNDCORE_ADAPTIVE_DIRECTION, CMD_SET_ADAPTIVE_DIRECTION); + case PREF_SOUNDCORE_EQUALIZER_PRESET: + return encodeSetByte(prefs, PREF_SOUNDCORE_EQUALIZER_PRESET, CMD_SET_EQUALIZER_PRESET); + case PREF_SOUNDCORE_EQUALIZER_DIRECTION: + return encodeRequest(CMD_GET_EQUALIZER); + case PREF_SOUNDCORE_EQUALIZER_BAND1_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND1_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND2_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND2_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND3_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND3_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND4_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND4_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND5_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND5_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND6_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND6_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND7_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND7_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND8_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND8_VALUE: + case PREF_SOUNDCORE_EQUALIZER_BAND9_FREQ: + case PREF_SOUNDCORE_EQUALIZER_BAND9_VALUE: + return encodeSetEqualizerCustom(prefs); + } + + return super.encodeSendConfiguration(config); + } + + private byte[] encodeRequest(short cmd) { + return new SoundcorePacket(cmd).encode(); + } + + public byte[] encodeGetDeviceInfo() { + return encodeRequest(CMD_GET_DEVICE_INFO); + } + + @Override + public byte[] encodePowerOff() { + return encodeRequest(CMD_POWER_OFF); + } + + private byte[] encodeSetBoolean(Prefs prefs, String pref, short cmd) { + boolean enabled = prefs.getBoolean(pref, true); + byte[] payload = new byte[] { enabled ? (byte)0x01 : (byte)0x00 }; + + return new SoundcorePacket(cmd, payload).encode(); + } + + private byte[] encodeSetByte(Prefs prefs, String pref, short cmd) { + byte value = (byte)Integer.parseInt(prefs.getString(pref, "0")); + byte[] payload = new byte[] { value }; + + return new SoundcorePacket(cmd, payload).encode(); + } + + private byte[] encodeSetAutoPowerOff(Prefs prefs) { + byte duration = (byte)Integer.parseInt(prefs.getString(PREF_SOUNDCORE_AUTO_POWER_OFF, "2")); + byte[] payload; + + if (duration > 0) + payload = new byte[] { (byte)0x01, (byte)(duration - 1) }; + else + payload = new byte[] { (byte)0x00, (byte)0x00 }; + + return new SoundcorePacket(CMD_SET_AUTO_POWER_OFF, payload).encode(); + } + + private byte[] encodeSetEqualizerCustom(Prefs prefs) { + ByteBuffer buf = ByteBuffer.allocate(21); + int eqDirection = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_EQUALIZER_DIRECTION, "0")); + byte[] equalizer = equalizerFromPrefs(prefs); + + // Bit combination of the equalizer directions that should be changed + buf.put((byte)(1 << eqDirection)); + buf.put((byte)0x01); + buf.put((byte)0xff); + buf.put(equalizer); + + return new SoundcorePacket(CMD_SET_EQUALIZER_CUSTOM, buf.array()).encode(); + } + + private Map equalizerToPrefs(byte[] equalizer) { + Map prefs = new HashMap<>(); + + for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) { + // Equalizer values range from 60 to 180, with 120 as "neutral" + byte freq = equalizer[i * 2 + 1]; + int value = Byte.toUnsignedInt(equalizer[i * 2]); + + // Map equalizer value to preference range (0 to 120) + prefs.put(EQUALIZER_PREFS_FREQ[i], freq); + prefs.put(EQUALIZER_PREFS_VALUE[i], value - 60); + } + + return prefs; + } + + private byte[] equalizerFromPrefs(Prefs prefs) { + byte[] equalizer = new byte[EQUALIZER_PREFS_FREQ.length * 2]; + + for (int i = 0; i < EQUALIZER_PREFS_FREQ.length; i++) { + int freq = Integer.parseInt(prefs.getString(EQUALIZER_PREFS_FREQ[i], "0")); + int value = prefs.getInt(EQUALIZER_PREFS_VALUE[i], 60); + + // Map equalizer values from 0 - 120 back to 60 - 180 + equalizer[i * 2 + 1] = ((byte)freq); + equalizer[i * 2] = (byte)(value + 60); + } + + return equalizer; + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index e5e22cba1..113426168 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3485,6 +3485,192 @@ AMBIENT_SOUND_CONTROL + + @string/off + @string/soundcore_button_brightness_low + @string/soundcore_button_brightness_medium + @string/soundcore_button_brightness_high + + + + 0 + 20 + 70 + 100 + + + + @string/pref_button_action_disabled + @string/minutes_5 + @string/minutes_10 + @string/minutes_20 + @string/minutes_60 + + + + 0 + 1 + 2 + 3 + 4 + + + + @string/soundcore_equalizer_preset_signature + @string/soundcore_equalizer_preset_xtra_bass + @string/soundcore_equalizer_preset_voice + @string/soundcore_equalizer_preset_balanced + @string/custom + + + + 0 + 1 + 2 + 3 + 254 + + + + @string/soundcore_equalizer_direction_standing + @string/soundcore_equalizer_direction_lying + @string/soundcore_equalizer_direction_hanging + + + + 0 + 1 + 2 + + + + 48 Hz + 51 Hz + 55 Hz + 59 Hz + 63 Hz + 68 Hz + 72 Hz + 80 Hz + 83 Hz + 89 Hz + + + + 95 Hz + 102 Hz + 109 Hz + 117 Hz + 125 Hz + 134 Hz + 144 Hz + 150 Hz + 165 Hz + 177 Hz + + + + 190 Hz + 203 Hz + 218 Hz + 233 Hz + 250 Hz + 268 Hz + 287 Hz + 300 Hz + 330 Hz + 353 Hz + + + + 379 Hz + 406 Hz + 435 Hz + 467 Hz + 500 Hz + 536 Hz + 574 Hz + 600 Hz + 659 Hz + 706 Hz + + + + 759 Hz + 813 Hz + 871 Hz + 933 Hz + 1.0 kHz + 1.07 kHz + 1.1 kHz + 1.2 kHz + 1.3 kHz + 1.4 kHz + + + + 1.5 kHz + 1.6 kHz + 1.75 kHz + 1.86 kHz + 2.0 kHz + 2.15 kHz + 2.3 kHz + 2.5 kHz + 2.6 kHz + 2.8 kHz + + + + 3.0 kHz + 3.2 kHz + 3.5 kHz + 3.7 kHz + 4.0 kHz + 4.3 kHz + 4.6 kHz + 5.0 kHz + 5.3 kHz + 5.6 kHz + + + + 6.0 kHz + 6.5 kHz + 7.0 kHz + 7.5 kHz + 8.0 kHz + 8.5 kHz + 9.0 kHz + 9.8 kHz + 10.5 kHz + 11.3 kHz + + + + 12.1 kHz + 13.0 kHz + 14.0 kHz + 15.0 kHz + 16.0 kHz + 17.1 kHz + 18.3 kHz + 20.0 kHz + + + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + 8 + 9 + + @string/moondrop_equalizer_preset_reference @string/moondrop_equalizer_preset_basshead diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82dcd54aa..4c9e2fc98 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -925,7 +925,9 @@ 1 minute 5 minutes 10 minutes + 20 minutes 30 minutes + 60 minutes Live activity Steps today, target: %1$s Lack of steps: %1$d @@ -1621,6 +1623,7 @@ Sony LinkBuds Sony LinkBuds S Soundcore Liberty 3 Pro + Soundcore Motion 300 Moondrop Space Travel Binary sensor Honor Band 3 @@ -2442,6 +2445,39 @@ Switch Active Noise Cancelling Mode Long Press (1s) Long Press (3s) + Voice Prompts + Button Brightness + Low + Medium + High + LDAC Mode + Enabling LDAC will decrease the battery life and might lead to connection instability + Adaptive Direction + Adjust equalizer preset automatically based on device direction + Preset + soundcore Signature + Xtra Bass + Voice + Balanced + Customize… + Configure parametric equalizer settings + Device Direction + Standing + Lying + Hanging + Reset to default + Set all equalizer bands back to default settings + Band 1 + Band 2 + Band 3 + Band 4 + Band 5 + Band 6 + Band 7 + Band 8 + Band 9 + Frequency + Value Protocol Version Auto Brightness Adjust screen brightness according to ambient light diff --git a/app/src/main/res/xml/devicesettings_soundcore_motion300.xml b/app/src/main/res/xml/devicesettings_soundcore_motion300.xml new file mode 100644 index 000000000..130bb9ce4 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_soundcore_motion300.xml @@ -0,0 +1,23 @@ + + + + + + diff --git a/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml b/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml new file mode 100644 index 000000000..aebba11b4 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_soundcore_motion300_audio.xml @@ -0,0 +1,201 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +