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 72eb79718..cda7bf042 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 @@ -112,34 +112,20 @@ public class DeviceSettingsPreferenceConst { public static final String PREF_GALAXY_BUDS_LIVE_ANC = "pref_galaxy_buds_live_anc"; public static final String PREF_GALAXY_BUDS_PRESSURE_RELIEF = "pref_galaxy_buds_live_pressure_relief"; + public static final String PREF_SONY_AUDIO_CODEC = "pref_sony_audio_codec"; public static final String PREF_SONY_AMBIENT_SOUND_CONTROL = "pref_sony_ambient_sound_control"; 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"; public static final String PREF_SONY_SOUND_POSITION = "pref_sony_sound_position"; public static final String PREF_SONY_SURROUND_MODE = "pref_sony_surround_mode"; public static final String PREF_SONY_EQUALIZER_MODE = "pref_sony_equalizer_mode"; - public static final String PREF_SONY_DSEE_HX = "pref_sony_dsee_hx"; - public static final String PREF_SONY_EQUALIZER_PRESET_MANUAL = "pref_sony_equalizer_preset_manual"; - public static final String PREF_SONY_EQUALIZER_PRESET_CUSTOM_1 = "pref_sony_equalizer_preset_custom_1"; - public static final String PREF_SONY_EQUALIZER_PRESET_CUSTOM_2 = "pref_sony_equalizer_preset_custom_2"; - public static final String PREF_SONY_EQUALIZER_MANUAL_BAND_400 = "pref_sony_equalizer_manual_band_400"; - public static final String PREF_SONY_EQUALIZER_MANUAL_BAND_1000 = "pref_sony_equalizer_manual_band_1000"; - public static final String PREF_SONY_EQUALIZER_MANUAL_BAND_2500 = "pref_sony_equalizer_manual_band_2500"; - public static final String PREF_SONY_EQUALIZER_MANUAL_BAND_6300 = "pref_sony_equalizer_manual_band_6300"; - public static final String PREF_SONY_EQUALIZER_MANUAL_BAND_16000 = "pref_sony_equalizer_manual_band_16000"; - public static final String PREF_SONY_EQUALIZER_MANUAL_CLEAR_BASS = "pref_sony_equalizer_manual_clear_bass"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_BAND_400 = "pref_sony_equalizer_custom_1_band_400"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_BAND_1000 = "pref_sony_equalizer_custom_1_band_1000"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_BAND_2500 = "pref_sony_equalizer_custom_1_band_2500"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_BAND_6300 = "pref_sony_equalizer_custom_1_band_6300"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_BAND_16000 = "pref_sony_equalizer_custom_1_band_16000"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_1_CLEAR_BASS = "pref_sony_equalizer_custom_1_clear_bass"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_BAND_400 = "pref_sony_equalizer_custom_2_band_400"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_BAND_1000 = "pref_sony_equalizer_custom_2_band_1000"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_BAND_2500 = "pref_sony_equalizer_custom_2_band_2500"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_BAND_6300 = "pref_sony_equalizer_custom_2_band_6300"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_BAND_16000 = "pref_sony_equalizer_custom_2_band_16000"; - public static final String PREF_SONY_EQUALIZER_CUSTOM_2_CLEAR_BASS = "pref_sony_equalizer_custom_2_clear_bass"; + public static final String PREF_SONY_AUDIO_UPSAMPLING = "pref_sony_audio_upsampling"; + public static final String PREF_SONY_EQUALIZER_BAND_400 = "pref_sony_equalizer_band_400"; + public static final String PREF_SONY_EQUALIZER_BAND_1000 = "pref_sony_equalizer_band_1000"; + public static final String PREF_SONY_EQUALIZER_BAND_2500 = "pref_sony_equalizer_band_2500"; + public static final String PREF_SONY_EQUALIZER_BAND_6300 = "pref_sony_equalizer_band_6300"; + public static final String PREF_SONY_EQUALIZER_BAND_16000 = "pref_sony_equalizer_band_16000"; + public static final String PREF_SONY_EQUALIZER_BASS = "pref_sony_equalizer_bass"; public static final String PREF_SONY_TOUCH_SENSOR = "pref_sony_touch_sensor"; public static final String PREF_SONY_AUTOMATIC_POWER_OFF = "pref_sony_automatic_power_off"; public static final String PREF_SONY_NOTIFICATION_VOICE_GUIDE = "pref_sony_notification_voice_guide"; 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 23bfed87a..63aab472b 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 @@ -51,7 +51,6 @@ import nodomain.freeyourgadget.gadgetbridge.devices.DeviceManager; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst; import nodomain.freeyourgadget.gadgetbridge.devices.makibeshr3.MakibesHR3Constants; import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandConst; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.XTimePreference; @@ -124,7 +123,6 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONYSWR12_STAMINA; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDS; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TIMEFORMAT; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_TRANSLITERATION_ENABLED; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE; @@ -136,25 +134,13 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_SURROUND_MODE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_400; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_1000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_2500; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_6300; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_16000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_CLEAR_BASS; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_400; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_1000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_2500; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_6300; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_16000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_CLEAR_BASS; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_400; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_1000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_2500; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_6300; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_16000; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_CLEAR_BASS; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_DSEE_HX; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_400; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_1000; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_2500; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_6300; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_16000; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BASS; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_TOUCH_SENSOR; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AUTOMATIC_POWER_OFF; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_NOTIFICATION_VOICE_GUIDE; @@ -566,25 +552,13 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat imp addPreferenceHandlerFor(PREF_SONY_SOUND_POSITION); addPreferenceHandlerFor(PREF_SONY_SURROUND_MODE); addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MODE); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_BAND_400); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_BAND_1000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_BAND_2500); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_BAND_6300); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_BAND_16000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MANUAL_CLEAR_BASS); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_BAND_400); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_BAND_1000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_BAND_2500); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_BAND_6300); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_BAND_16000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_1_CLEAR_BASS); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_BAND_400); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_BAND_1000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_BAND_2500); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_BAND_6300); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_BAND_16000); - addPreferenceHandlerFor(PREF_SONY_EQUALIZER_CUSTOM_2_CLEAR_BASS); - addPreferenceHandlerFor(PREF_SONY_DSEE_HX); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BAND_400); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BAND_1000); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BAND_2500); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BAND_6300); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BAND_16000); + addPreferenceHandlerFor(PREF_SONY_EQUALIZER_BASS); + addPreferenceHandlerFor(PREF_SONY_AUDIO_UPSAMPLING); addPreferenceHandlerFor(PREF_SONY_TOUCH_SENSOR); addPreferenceHandlerFor(PREF_SONY_AUTOMATIC_POWER_OFF); addPreferenceHandlerFor(PREF_SONY_NOTIFICATION_VOICE_GUIDE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AmbientSoundControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AmbientSoundControl.java deleted file mode 100644 index 915fe5764..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AmbientSoundControl.java +++ /dev/null @@ -1,24 +0,0 @@ -/* Copyright (C) 2021 José Rebelo - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones; - -public enum AmbientSoundControl { - OFF, - NOISE_CANCELLING, - WIND_NOISE_REDUCTION, - AMBIENT_SOUND -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerCustomBands.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerCustomBands.java deleted file mode 100644 index d739cd2f8..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerCustomBands.java +++ /dev/null @@ -1,51 +0,0 @@ -/* Copyright (C) 2021 José Rebelo - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones; - -import java.util.List; - -public class EqualizerCustomBands { - private List bands; - private int clearBass; - - public EqualizerCustomBands(List bands, int clearBass) { - if (bands.size() != 5) { - throw new IllegalArgumentException("Equalizer needs exactly 5 bands"); - } - - for (Integer band : bands) { - if (band < -10 || band > 10) { - throw new IllegalArgumentException(String.format("Bands should be between -10 and 10, got %d", band)); - } - } - - if (clearBass < -10 || clearBass > 10) { - throw new IllegalArgumentException(String.format("Clear Bass value shoulud be between -10 and 10, got %d", clearBass)); - } - - this.bands = bands; - this.clearBass = clearBass; - } - - public List getBands() { - return bands; - } - - public int getClearBass() { - return clearBass; - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java index 90f5f9a85..1834e462e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java @@ -41,7 +41,7 @@ public abstract class SonyHeadphonesCoordinator extends AbstractDeviceCoordinato @Override public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { - return new SonySettingsCustomizer(); + return new SonyHeadphonesSettingsCustomizer(); } @Override diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesSettingsCustomizer.java new file mode 100644 index 000000000..7cb85c9af --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesSettingsCustomizer.java @@ -0,0 +1,56 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones; + +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL; +import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE; + +import androidx.preference.ListPreference; +import androidx.preference.Preference; + +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl; + +public class SonyHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer { + @Override + public void customizeSettings(final DeviceSpecificSettingsHandler handler) { + // Only enable the focus on voice check and voice level slider if the ambient sound control mode is ambient sound + + final ListPreference ambientSoundControl = handler.findPreference(PREF_SONY_AMBIENT_SOUND_CONTROL); + if (ambientSoundControl != null) { + final Preference focusOnVoice = handler.findPreference(PREF_SONY_FOCUS_VOICE); + final Preference ambientSoundLevel = handler.findPreference(PREF_SONY_AMBIENT_SOUND_LEVEL); + + final Preference.OnPreferenceChangeListener ambientSoundControlPrefListener = new Preference.OnPreferenceChangeListener() { + public boolean onPreferenceChange(Preference preference, Object newVal) { + boolean isAmbientSoundEnabled = AmbientSoundControl.Mode.AMBIENT_SOUND.name().toLowerCase(Locale.getDefault()).equals(newVal); + focusOnVoice.setEnabled(isAmbientSoundEnabled); + ambientSoundLevel.setEnabled(isAmbientSoundEnabled); + + return true; + } + }; + + ambientSoundControlPrefListener.onPreferenceChange(ambientSoundControl, ambientSoundControl.getValue()); + handler.addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL, ambientSoundControlPrefListener); + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonySettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonySettingsCustomizer.java deleted file mode 100644 index 03bf5919b..000000000 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonySettingsCustomizer.java +++ /dev/null @@ -1,118 +0,0 @@ -/* Copyright (C) 2021 José Rebelo - - This file is part of Gadgetbridge. - - Gadgetbridge is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Gadgetbridge is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones; - -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_PRESET_CUSTOM_1; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_PRESET_CUSTOM_2; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_PRESET_MANUAL; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION; -import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SONY_SURROUND_MODE; - -import androidx.preference.ListPreference; -import androidx.preference.Preference; - -import java.util.Locale; - -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; - -public class SonySettingsCustomizer implements DeviceSpecificSettingsCustomizer { - @Override - public void customizeSettings(final DeviceSpecificSettingsHandler handler) { - // Only enable the focus on voice check and voice level slider if the ambient sound control mode is ambient sound - - final ListPreference ambientSoundControl = handler.findPreference(PREF_SONY_AMBIENT_SOUND_CONTROL); - if (ambientSoundControl != null) { - final Preference focusOnVoice = handler.findPreference(PREF_SONY_FOCUS_VOICE); - final Preference ambientSoundLevel = handler.findPreference(PREF_SONY_AMBIENT_SOUND_LEVEL); - - final Preference.OnPreferenceChangeListener ambientSoundControlPrefListener = new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newVal) { - boolean isAmbientSoundEnabled = AmbientSoundControl.AMBIENT_SOUND.name().toLowerCase(Locale.ROOT).equals(newVal); - focusOnVoice.setEnabled(isAmbientSoundEnabled); - ambientSoundLevel.setEnabled(isAmbientSoundEnabled); - - return true; - } - }; - - ambientSoundControlPrefListener.onPreferenceChange(ambientSoundControl, ambientSoundControl.getValue()); - handler.addPreferenceHandlerFor(PREF_SONY_AMBIENT_SOUND_CONTROL, ambientSoundControlPrefListener); - } - - // Make the sound position and surround mode settings mutually exclusive - - final ListPreference soundPositionPref = handler.findPreference(PREF_SONY_SOUND_POSITION); - final ListPreference surroundModePref = handler.findPreference(PREF_SONY_SURROUND_MODE); - - if (soundPositionPref != null && surroundModePref != null) { - final Preference.OnPreferenceChangeListener soundPositionPrefListener = new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newVal) { - SoundPosition soundPosition = SoundPosition.valueOf(newVal.toString().toUpperCase(Locale.ROOT)); - surroundModePref.setEnabled(SoundPosition.OFF.equals(soundPosition)); - - return true; - } - }; - - final Preference.OnPreferenceChangeListener surroundModePrefListener = new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newVal) { - SurroundMode surroundMode = SurroundMode.valueOf(newVal.toString().toUpperCase(Locale.ROOT)); - soundPositionPref.setEnabled(SurroundMode.OFF.equals(surroundMode)); - - return true; - } - }; - - soundPositionPrefListener.onPreferenceChange(soundPositionPref, soundPositionPref.getValue()); - surroundModePrefListener.onPreferenceChange(surroundModePref, surroundModePref.getValue()); - handler.addPreferenceHandlerFor(PREF_SONY_SOUND_POSITION, soundPositionPrefListener); - handler.addPreferenceHandlerFor(PREF_SONY_SURROUND_MODE, surroundModePrefListener); - } - - // Only enable the equalizer preset if the corresponding mode is selected - - final ListPreference equalizerModePref = handler.findPreference(PREF_SONY_EQUALIZER_MODE); - - if (equalizerModePref != null) { - handler.addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MODE); - - final Preference presetManual = handler.findPreference(PREF_SONY_EQUALIZER_PRESET_MANUAL); - final Preference presetCustom1 = handler.findPreference(PREF_SONY_EQUALIZER_PRESET_CUSTOM_1); - final Preference presetCustom2 = handler.findPreference(PREF_SONY_EQUALIZER_PRESET_CUSTOM_2); - - final Preference.OnPreferenceChangeListener equalizerModePrefListener = new Preference.OnPreferenceChangeListener() { - public boolean onPreferenceChange(Preference preference, Object newVal) { - final EqualizerPreset equalizerPreset = EqualizerPreset.valueOf(newVal.toString().toUpperCase(Locale.ROOT)); - - presetManual.setEnabled(EqualizerPreset.MANUAL.equals(equalizerPreset)); - presetCustom1.setEnabled(EqualizerPreset.CUSTOM_1.equals(equalizerPreset)); - presetCustom2.setEnabled(EqualizerPreset.CUSTOM_2.equals(equalizerPreset)); - - return true; - } - }; - - equalizerModePrefListener.onPreferenceChange(equalizerModePref, equalizerModePref.getValue()); - handler.addPreferenceHandlerFor(PREF_SONY_EQUALIZER_MODE, equalizerModePrefListener); - } - } -} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java similarity index 75% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Coordinator.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java index 71fb6de01..bfd9a6d4c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java @@ -14,7 +14,7 @@ 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.sony.headphones.wh1000xm3; +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators; import androidx.annotation.NonNull; @@ -24,13 +24,14 @@ import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; -public class SonyWh1000Xm3Coordinator extends SonyHeadphonesCoordinator { +public class SonyWH1000XM3Coordinator extends SonyHeadphonesCoordinator { @NonNull @Override public DeviceType getSupportedType(GBDeviceCandidate candidate) { if (candidate.getName().contains("WH-1000XM3")) { return DeviceType.SONY_WH_1000XM3; } + return DeviceType.UNKNOWN; } @@ -44,9 +45,15 @@ public class SonyWh1000Xm3Coordinator extends SonyHeadphonesCoordinator { return new int[]{ R.xml.devicesettings_sony_warning_wh1000xm3, R.xml.devicesettings_sony_headphones_ambient_sound_control, + R.xml.devicesettings_header_other, R.xml.devicesettings_sony_headphones_equalizer, - R.xml.devicesettings_sony_headphones_other, - R.xml.devicesettings_sony_wh_1000xm3 + R.xml.devicesettings_sony_headphones_sound_position, + R.xml.devicesettings_sony_headphones_surround_mode, + R.xml.devicesettings_sony_headphones_audio_upsampling, + R.xml.devicesettings_header_system, + R.xml.devicesettings_sony_headphones_touch_sensor_single, + R.xml.devicesettings_automatic_power_off, + R.xml.devicesettings_sony_headphones_notifications_voice_guide }; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AmbientSoundControl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AmbientSoundControl.java new file mode 100644 index 000000000..ed4a6a067 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AmbientSoundControl.java @@ -0,0 +1,87 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public class AmbientSoundControl { + public enum Mode { + OFF, + NOISE_CANCELLING, + WIND_NOISE_REDUCTION, + AMBIENT_SOUND + } + + private final Mode mode; + private final boolean focusOnVoice; + private final int ambientSound; + + public AmbientSoundControl(Mode mode, boolean focusOnVoice, int ambientSound) { + if (ambientSound < 0 || ambientSound > 20) { + throw new IllegalArgumentException(String.format("Level must be between 0 and 20 (was %d)", ambientSound)); + } + + this.mode = mode; + this.focusOnVoice = focusOnVoice; + this.ambientSound = ambientSound; + } + + public String toString() { + return String.format(Locale.getDefault(), "AmbientSoundControl{mode=%s, focusOnVoice=%s, ambientSound=%d}", mode, focusOnVoice, ambientSound); + } + + public Mode getMode() { + return mode; + } + + public boolean isFocusOnVoice() { + return focusOnVoice; + } + + public int getAmbientSound() { + return ambientSound; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL, mode.name().toLowerCase(Locale.getDefault())); + + if (AmbientSoundControl.Mode.AMBIENT_SOUND.equals(mode)) { + // Only use the ambient sound levels and focus on voice if we're on ambient sound mode, + // to prevent overriding the user settings + put(DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE, focusOnVoice); + // Level is offset by 1 because we can't configure the SeekBarPreference min level on the current api level + put(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, ambientSound - 1); + } + }}; + } + + public static AmbientSoundControl fromPreferences(final SharedPreferences prefs) { + final String soundControl = prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL, "noise_cancelling"); + final boolean focusVoice = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE, false); + // Level is offset by 1 because we can't configure the SeekBarPreference min level on the current api level + final int ambientSound = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0) + 1; + + return new AmbientSoundControl(AmbientSoundControl.Mode.valueOf(soundControl.toUpperCase()), focusVoice, ambientSound); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AudioUpsampling.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AudioUpsampling.java new file mode 100644 index 000000000..4f4f1811c --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AudioUpsampling.java @@ -0,0 +1,46 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public class AudioUpsampling { + private final boolean enabled; + + public AudioUpsampling(final boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING, enabled); + }}; + } + + public static AudioUpsampling fromPreferences(final SharedPreferences prefs) { + return new AudioUpsampling(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING, false)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AutomaticPowerOff.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AutomaticPowerOff.java similarity index 61% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AutomaticPowerOff.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AutomaticPowerOff.java index 2c9c3fc20..325f4fe03 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/AutomaticPowerOff.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/AutomaticPowerOff.java @@ -14,7 +14,15 @@ 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.sony.headphones; +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; public enum AutomaticPowerOff { OFF(new byte[]{(byte) 0x11, (byte) 0x00}), @@ -24,9 +32,23 @@ public enum AutomaticPowerOff { AFTER_3_HOUR(new byte[]{(byte) 0x03, (byte) 0x03}), WHEN_TAKEN_OFF(new byte[]{(byte) 0x10, (byte) 0x00}); - public final byte[] code; + private final byte[] code; AutomaticPowerOff(final byte[] code) { this.code = code; } + + public byte[] getCode() { + return this.code; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_AUTOMATIC_POWER_OFF, name().toLowerCase(Locale.getDefault())); + }}; + } + + public static AutomaticPowerOff fromPreferences(final SharedPreferences prefs) { + return AutomaticPowerOff.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_AUTOMATIC_POWER_OFF, "off").toUpperCase()); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerCustomBands.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerCustomBands.java new file mode 100644 index 000000000..ee61e1464 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerCustomBands.java @@ -0,0 +1,85 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public class EqualizerCustomBands { + private List bands; + private int bass; + + public EqualizerCustomBands(final List bands, final int bass) { + if (bands.size() != 5) { + throw new IllegalArgumentException("Equalizer needs exactly 5 bands"); + } + + for (final Integer band : bands) { + if (band < -10 || band > 10) { + throw new IllegalArgumentException(String.format("Bands should be between -10 and 10, got %d", band)); + } + } + + if (bass < -10 || bass > 10) { + throw new IllegalArgumentException(String.format("Clear Bass value shoulud be between -10 and 10, got %d", bass)); + } + + this.bands = bands; + this.bass = bass; + } + + public List getBands() { + return bands; + } + + public int getBass() { + return bass; + } + + public String toString() { + return String.format("EqualizerCustomBands{clearBass=%d, bands=%s}", bass, bands); + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_400, bands.get(0) + 10); + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_1000, bands.get(1) + 10); + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_2500, bands.get(2) + 10); + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_6300, bands.get(3) + 10); + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_16000, bands.get(4) + 10); + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BASS, bass + 10); + ; + }}; + } + + public static EqualizerCustomBands fromPreferences(final SharedPreferences prefs) { + int band1 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_400, 10) - 10; + int band2 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_1000, 10) - 10; + int band3 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_2500, 10) - 10; + int band4 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_6300, 10) - 10; + int band5 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_16000, 10) - 10; + int bass = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BASS, 10) - 10; + + return new EqualizerCustomBands(Arrays.asList(band1, band2, band3, band4, band5), bass); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerPreset.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerPreset.java new file mode 100644 index 000000000..b0ac4883e --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/EqualizerPreset.java @@ -0,0 +1,60 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public enum EqualizerPreset { + OFF((byte) 0x00), + BRIGHT((byte) 0x10), + EXCITED((byte) 0x11), + MELLOW((byte) 0x12), + RELAXED((byte) 0x13), + VOCAL((byte) 0x14), + TREBLE_BOOST((byte) 0x15), + BASS_BOOST((byte) 0x16), + SPEECH((byte) 0x17), + MANUAL((byte) 0xa0), + CUSTOM_1((byte) 0xa1), + CUSTOM_2((byte) 0xa2); + + private final byte code; + + EqualizerPreset(final byte code) { + this.code = code; + } + + public byte getCode() { + return this.code; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE, name().toLowerCase(Locale.getDefault())); + }}; + } + + public static EqualizerPreset fromPreferences(final SharedPreferences prefs) { + return EqualizerPreset.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE, "off").toUpperCase()); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SoundPosition.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SoundPosition.java similarity index 58% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SoundPosition.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SoundPosition.java index eca7063aa..15b2bf4fd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SoundPosition.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SoundPosition.java @@ -14,7 +14,15 @@ 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.sony.headphones; +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; public enum SoundPosition { OFF((byte) 0x00), @@ -24,9 +32,23 @@ public enum SoundPosition { REAR_LEFT((byte) 0x11), REAR_RIGHT((byte) 0x12); - public final byte code; + private final byte code; SoundPosition(final byte code) { this.code = code; } + + public byte getCode() { + return this.code; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION, name().toLowerCase(Locale.getDefault())); + }}; + } + + public static SoundPosition fromPreferences(final SharedPreferences prefs) { + return SoundPosition.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION, "off").toUpperCase()); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SurroundMode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SurroundMode.java similarity index 57% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SurroundMode.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SurroundMode.java index 0953c0e1c..a62fecea8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SurroundMode.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/SurroundMode.java @@ -14,7 +14,15 @@ 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.sony.headphones; +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Locale; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; public enum SurroundMode { OFF((byte) 0x00), @@ -23,9 +31,23 @@ public enum SurroundMode { OUTDOOR_STAGE((byte) 0x01), CONCERT_HALL((byte) 0x03); - public final byte code; + private final byte code; SurroundMode(final byte code) { this.code = code; } + + public byte getCode() { + return this.code; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_SURROUND_MODE, name().toLowerCase(Locale.getDefault())); + }}; + } + + public static SurroundMode fromPreferences(final SharedPreferences prefs) { + return SurroundMode.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION, "off").toUpperCase()); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerPreset.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/TouchSensor.java similarity index 50% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerPreset.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/TouchSensor.java index 94aa0a1df..3923edf3b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/EqualizerPreset.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/TouchSensor.java @@ -14,25 +14,33 @@ 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.sony.headphones; +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; -public enum EqualizerPreset { - OFF(new byte[] {(byte) 0x00, (byte) 0x00}), - BRIGHT(new byte[] {(byte) 0x10, (byte) 0x00}), - EXCITED(new byte[] {(byte) 0x11, (byte) 0x00}), - MELLOW(new byte[] {(byte) 0x12, (byte) 0x00}), - RELAXED(new byte[] {(byte) 0x13, (byte) 0x00}), - VOCAL(new byte[] {(byte) 0x14, (byte) 0x00}), - TREBLE_BOOST(new byte[] {(byte) 0x15, (byte) 0x00}), - BASS_BOOST(new byte[] {(byte) 0x16, (byte) 0x00}), - SPEECH(new byte[] {(byte) 0x17, (byte) 0x00}), - MANUAL(new byte[] {(byte) 0xa0, (byte) 0x00}), - CUSTOM_1(new byte[] {(byte) 0xa1, (byte) 0x00}), - CUSTOM_2(new byte[] {(byte) 0xa2, (byte) 0x00}); +import android.content.SharedPreferences; - public final byte[] code; +import java.util.HashMap; +import java.util.Map; - EqualizerPreset(final byte[] code) { - this.code = code; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public class TouchSensor { + private final boolean enabled; + + public TouchSensor(final boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_TOUCH_SENSOR, enabled); + }}; + } + + public static TouchSensor fromPreferences(final SharedPreferences prefs) { + return new TouchSensor(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_TOUCH_SENSOR, true)); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/VoiceNotifications.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/VoiceNotifications.java new file mode 100644 index 000000000..8fbbc9a65 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/prefs/VoiceNotifications.java @@ -0,0 +1,46 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs; + +import android.content.SharedPreferences; + +import java.util.HashMap; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; + +public class VoiceNotifications { + private final boolean enabled; + + public VoiceNotifications(final boolean enabled) { + this.enabled = enabled; + } + + public boolean isEnabled() { + return enabled; + } + + public Map toPreferences() { + return new HashMap() {{ + put(DeviceSettingsPreferenceConst.PREF_SONY_NOTIFICATION_VOICE_GUIDE, enabled); + }}; + } + + public static VoiceNotifications fromPreferences(final SharedPreferences prefs) { + return new VoiceNotifications(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_NOTIFICATION_VOICE_GUIDE, true)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java index 6db2d9832..10dad1269 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/btclassic/BtClassicIoThread.java @@ -82,7 +82,7 @@ public abstract class BtClassicIoThread extends GBDeviceIoThread { LOG.error("mOutStream is null"); return; } - LOG.debug("writing:" + GB.hexdump(bytes, 0, bytes.length)); + LOG.debug("writing: {}", GB.hexdump(bytes, 0, bytes.length)); try { mOutStream.write(bytes); mOutStream.flush(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java index 952e733d3..db80e8639 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java @@ -18,6 +18,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones; import android.bluetooth.BluetoothAdapter; import android.content.Context; +import android.os.Handler; import android.os.ParcelUuid; import androidx.annotation.NonNull; @@ -32,38 +33,80 @@ import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Message; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class SonyHeadphonesIoThread extends BtClassicIoThread { private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesIoThread.class); + private final SonyHeadphonesProtocol mProtocol; + + // Track whether we got the first reply + private boolean firstReply = false; + private final Handler handler = new Handler(); + private int initRetries = 0; + + /** + * Sometimes the headphones will ignore the first init request, so we retry a few times + * TODO: Implement this in a more elegant way. Ideally, we should retry every command for which we didn't get an ACK. + */ + private final Runnable initSendRunnable = new Runnable() { + public void run() { + // If we still haven't got any reply, re-send the init + if (!firstReply) { + if (initRetries++ < 2) { + LOG.warn("Init retry {}", initRetries); + + write(mProtocol.encodeInit()); + scheduleInitRetry(); + } else { + LOG.error("Failed to start headphones init after {} tries", initRetries); + quit(); + } + } + } + }; + public SonyHeadphonesIoThread(GBDevice gbDevice, Context context, SonyHeadphonesProtocol protocol, SonyHeadphonesSupport support, BluetoothAdapter btAdapter) { super(gbDevice, context, protocol, support, btAdapter); + mProtocol = protocol; + } + + @Override + protected void initialize() { + write(mProtocol.encodeInit()); + scheduleInitRetry(); + setUpdateState(GBDevice.State.INITIALIZING); + } + + @Override + public synchronized void write(byte[] bytes) { + // Log the human-readable message, for debugging + LOG.info("Writing {}", Message.fromBytes(bytes)); + + super.write(bytes); } @Override protected byte[] parseIncoming(InputStream inputStream) throws IOException { - ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); - byte[] incoming = new byte[1]; + final ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); + final byte[] incoming = new byte[1]; - while (true) { + do { inputStream.read(incoming); - if (incoming[0] == SonyHeadphonesProtocol.PACKET_HEADER) { + if (incoming[0] == Message.MESSAGE_HEADER) { msgStream.reset(); - continue; - } - - if (incoming[0] == SonyHeadphonesProtocol.PACKET_TRAILER) { - break; } msgStream.write(incoming); - } + } while (incoming[0] != Message.MESSAGE_TRAILER); - byte[] msgArray = msgStream.toByteArray(); - LOG.debug("Received: " + GB.hexdump(msgArray, 0, msgArray.length)); - return msgArray; + firstReply = true; + + LOG.trace("Raw message: {}", GB.hexdump(msgStream.toByteArray())); + + return msgStream.toByteArray(); } @NonNull @@ -71,4 +114,10 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { return UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA"); } + + private void scheduleInitRetry() { + LOG.info("Scheduling init retry"); + + handler.postDelayed(initSendRunnable, 1250); + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java index e203fd537..4ec87f4db 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java @@ -21,386 +21,233 @@ import android.content.SharedPreferences; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.ByteOrder; -import java.util.Arrays; +import java.util.ArrayList; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventSendBytes; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.AmbientSoundControl; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.AutomaticPowerOff; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.EqualizerCustomBands; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.EqualizerPreset; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SoundPosition; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SurroundMode; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceState; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AudioUpsampling; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AutomaticPowerOff; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerPreset; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SoundPosition; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SurroundMode; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.TouchSensor; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Message; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.AbstractSonyProtocolImpl; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.SonyProtocolImplV1; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; -import nodomain.freeyourgadget.gadgetbridge.util.GB; -public abstract class SonyHeadphonesProtocol extends GBDeviceProtocol { +public class SonyHeadphonesProtocol extends GBDeviceProtocol { private static final Logger LOG = LoggerFactory.getLogger(SonyHeadphonesProtocol.class); - /** - * Packet format: - *

- * - PACKET_HEADER - * - Command type? - almost always 0x0c or 0x0e? - * - Sequence Number - needs to be updated with the one sent in the ACK responses - * - 4-byte big endian int with number of bytes that will follow - * - N bytes of data - * - Checksum (1-byte sum, excluding header) - * - PACKET_TRAILER - *

- * Data between PACKET_HEADER and PACKET_TRAILER is escaped with PACKET_ESCAPE, and the - * following byte masked with PACKET_ESCAPE_MASK. - */ - - public static final byte PACKET_HEADER = 0x3e; - public static final byte PACKET_TRAILER = 0x3c; - public static final byte PACKET_ESCAPE = 0x3d; - public static final byte PACKET_ESCAPE_MASK = (byte) 0b11101111; - - private static final byte MSG_TYPE_ACK = 0x01; - - private static final byte CMD_SOUND_SURROUND = 0x01; - private static final byte CMD_SOUND_POSITION = 0x02; - private byte sequenceNumber = 0; + // Request queue, sent every ACK, as we can't send them all at once + // Initially, it should contain all the init requests + private final Queue requestQueue = new LinkedList<>(); + private int pendingAcks = 0; + + private AbstractSonyProtocolImpl protocolImpl = null; + public SonyHeadphonesProtocol(GBDevice device) { super(device); } @Override public GBDeviceEvent[] decodeResponse(byte[] res) { - byte[] message = unescape(res); - String hexdump = GB.hexdump(message, 0, message.length); - - LOG.debug("Received {}", hexdump); - - byte messageChecksum = message[message.length - 1]; - byte expectedChecksum = calcChecksum(message, true); - - if (messageChecksum != expectedChecksum) { - LOG.error("Invalid checksum for {}", hexdump); + final Message message = Message.fromBytes(res); + if (message == null) { return null; } - int payloadLength = ((message[2] << 24) & 0xFF000000) | ((message[3] << 16) & 0xFF0000) | ((message[4] << 8) & 0xFF00) | (message[5] & 0xFF); - if (payloadLength != message.length - 7) { - LOG.error("Unexpected payload length {}, expected {}", message.length - 7, payloadLength); + LOG.info("Received {}", message); + + final MessageType messageType = message.getType(); + + if (messageType == MessageType.ACK) { + if (sequenceNumber == message.getSequenceNumber()) { + LOG.warn("Unexpected ACK sequence number {}", message.getSequenceNumber()); + return null; + } + + sequenceNumber = message.getSequenceNumber(); + + return new GBDeviceEvent[]{handleAck()}; + } + + if (message.getPayload().length == 0) { + LOG.warn("Empty message: {}", message); return null; } - if (message[0] == MSG_TYPE_ACK) { - LOG.info("Received ACK: {}", hexdump); - sequenceNumber = message[1]; - return new GBDeviceEvent[]{new GBDeviceEventSendBytes(encodeAck())}; + final List events = new ArrayList<>(); + + if (protocolImpl == null) { + // Check if we got an init response, which should indicate the protocol version + if (MessageType.COMMAND_1.equals(messageType) && message.getPayload()[0] == 0x01) { + // Init reply, set the protocol version + if (message.getPayload().length == 4) { + protocolImpl = new SonyProtocolImplV1(getDevice()); + } else if (message.getPayload().length == 6) { + LOG.warn("Sony Headphones protocol v2 is not yet supported"); + return null; + } + + return null; + } } - LOG.warn("Unknown message: {}", hexdump); + if (protocolImpl == null) { + LOG.error("No protocol implementation, ignoring message"); + return null; + } - return null; + try { + switch (messageType) { + case COMMAND_1: + case COMMAND_2: + events.add(new GBDeviceEventSendBytes(encodeAck(message.getSequenceNumber()))); + events.addAll(protocolImpl.handlePayload(messageType, message.getPayload())); + break; + default: + LOG.warn("Unknown message type for {}", message); + return null; + } + } catch (final Exception e) { + // Don't crash the app if we somehow fail to handle the payload + LOG.error("Error handling payload", e); + } + + return events.toArray(new GBDeviceEvent[0]); } @Override public byte[] encodeSendConfiguration(String config) { + if (protocolImpl == null) { + LOG.error("No protocol implementation, ignoring config {}", config); + return super.encodeSendConfiguration(config); + } + final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress()); - EqualizerPreset equalizerPreset = EqualizerPreset.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE, "off").toUpperCase()); + + final Request configRequest; switch (config) { case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL: case DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE: case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL: - String soundControl = prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL, "noise_cancelling"); - boolean focusVoice = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_FOCUS_VOICE, false); - int level = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0); - return encodeSoundControl(AmbientSoundControl.valueOf(soundControl.toUpperCase()), focusVoice, level); - + configRequest = protocolImpl.setAmbientSoundControl(AmbientSoundControl.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION: - return encodeSoundPosition( - SoundPosition.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_SOUND_POSITION, "off").toUpperCase()) - ); - + configRequest = protocolImpl.setSoundPosition(SoundPosition.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_SURROUND_MODE: - return encodeSurroundMode( - SurroundMode.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_SURROUND_MODE, "off").toUpperCase()) - ); - + configRequest = protocolImpl.setSurroundMode(SurroundMode.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE: - return encodeEqualizerPreset(equalizerPreset); - - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_400: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_1000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_2500: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_6300: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_16000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_CLEAR_BASS: - int m_band1 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_400, 10) - 10; - int m_band2 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_1000, 10) - 10; - int m_band3 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_2500, 10) - 10; - int m_band4 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_6300, 10) - 10; - int m_band5 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_BAND_16000, 10) - 10; - int m_bass = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MANUAL_CLEAR_BASS, 10) - 10; - - return encodeEqualizerCustomBands(new EqualizerCustomBands(Arrays.asList(m_band1, m_band2, m_band3, m_band4, m_band5), m_bass)); - - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_400: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_1000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_2500: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_6300: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_16000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_CLEAR_BASS: - int c1_band1 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_400, 10) - 10; - int c1_band2 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_1000, 10) - 10; - int c1_band3 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_2500, 10) - 10; - int c1_band4 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_6300, 10) - 10; - int c1_band5 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_BAND_16000, 10) - 10; - int c1_bass = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_1_CLEAR_BASS, 10) - 10; - - return encodeEqualizerCustomBands(new EqualizerCustomBands(Arrays.asList(c1_band1, c1_band2, c1_band3, c1_band4, c1_band5), c1_bass)); - - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_400: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_1000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_2500: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_6300: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_16000: - case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_CLEAR_BASS: - int c2_band1 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_400, 10) - 10; - int c2_band2 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_1000, 10) - 10; - int c2_band3 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_2500, 10) - 10; - int c2_band4 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_6300, 10) - 10; - int c2_band5 = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_BAND_16000, 10) - 10; - int c2_bass = prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_CUSTOM_2_CLEAR_BASS, 10) - 10; - - return encodeEqualizerCustomBands(new EqualizerCustomBands(Arrays.asList(c2_band1, c2_band2, c2_band3, c2_band4, c2_band5), c2_bass)); - - case DeviceSettingsPreferenceConst.PREF_SONY_DSEE_HX: - return encodeDSEEHX(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_DSEE_HX, false)); - + configRequest = protocolImpl.setEqualizerPreset(EqualizerPreset.fromPreferences(prefs)); + break; + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_400: + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_1000: + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_2500: + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_6300: + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BAND_16000: + case DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_BASS: + configRequest = protocolImpl.setEqualizerCustomBands(EqualizerCustomBands.fromPreferences(prefs)); + break; + case DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING: + configRequest = protocolImpl.setAudioUpsampling(AudioUpsampling.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_TOUCH_SENSOR: - return encodeTouchSensor(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_TOUCH_SENSOR, true)); - + configRequest = protocolImpl.setTouchSensor(TouchSensor.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_AUTOMATIC_POWER_OFF: - return encodeAutomaticPowerOff( - AutomaticPowerOff.valueOf(prefs.getString(DeviceSettingsPreferenceConst.PREF_SONY_AUTOMATIC_POWER_OFF, "off").toUpperCase()) - ); - + configRequest = protocolImpl.setAutomaticPowerOff(AutomaticPowerOff.fromPreferences(prefs)); + break; case DeviceSettingsPreferenceConst.PREF_SONY_NOTIFICATION_VOICE_GUIDE: - return encodeVoiceNotifications(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SONY_NOTIFICATION_VOICE_GUIDE, true)); + configRequest = protocolImpl.setVoiceNotifications(VoiceNotifications.fromPreferences(prefs)); + break; default: LOG.warn("Unknown config '{}'", config); + return super.encodeSendConfiguration(config); } - return super.encodeSendConfiguration(config); + pendingAcks++; + + return configRequest.encode(sequenceNumber); } - public byte[] encodeAck() { - return encodeMessage( - MSG_TYPE_ACK, - new byte[]{} - ); - } + @Override + public byte[] encodeTestNewFunction() { + //return Request.fromHex(MessageType.COMMAND_1, "c40100").encode(sequenceNumber); - public byte[] encodeTriggerNoiseCancellingOptimizer() { - // This successfully triggers the noise cancelling optimizer. However, we don't get the - // optimization progress messages. - - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0x84, (byte) 0x01, (byte) 0x00, (byte) 0x01} - ); - } - - private byte[] encodeSoundControl(AmbientSoundControl ambientSoundControl, boolean focusOnVoice, int ambientSound) { - if (ambientSound < 0 || ambientSound > 19) { - throw new IllegalArgumentException("Level must be between 0 and 19"); + if (protocolImpl != null) { + return protocolImpl.startNoiseCancellingOptimizer().encode(sequenceNumber); } - ByteBuffer buf = ByteBuffer.allocate(14); - buf.order(ByteOrder.BIG_ENDIAN); - - buf.put((byte) 0x0c); - buf.put(sequenceNumber); - buf.putInt(8); - - buf.put((byte) 0x68); - buf.put((byte) 0x02); - if (AmbientSoundControl.OFF.equals(ambientSoundControl)) { - buf.put((byte) 0x00); - } else { - buf.put((byte) 0x11); - } - buf.put((byte) 0x01); - - switch (ambientSoundControl) { - case NOISE_CANCELLING: - buf.put((byte) 2); - break; - case WIND_NOISE_REDUCTION: - buf.put((byte) 1); - break; - case OFF: - case AMBIENT_SOUND: - default: - buf.put((byte) 0); - break; - } - - buf.put((byte) 0x01); - buf.put((byte) (focusOnVoice ? 0x01 : 0x00)); - buf.put((byte) (ambientSound + 1)); - - return encodeMessage(buf.array()); + return null; } - private byte[] encodeSoundPosition(SoundPosition position) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0x48, CMD_SOUND_POSITION, position.code} - ); + public byte[] encodeAck(byte sequenceNumber) { + return new Message(MessageType.ACK, (byte) (1 - sequenceNumber), new byte[0]).encode(); } - private byte[] encodeSurroundMode(SurroundMode mode) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0x48, CMD_SOUND_SURROUND, mode.code} - ); - } - - private byte[] encodeEqualizerPreset(EqualizerPreset preset) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0x58, (byte) 0x01, preset.code[0], preset.code[1]} - ); - } - - private byte[] encodeEqualizerCustomBands(EqualizerCustomBands equalizer) { - final ByteBuffer buf = ByteBuffer.allocate(10); - - buf.put((byte) 0x58); - buf.put((byte) 0x01); - buf.put((byte) 0xff); - buf.put((byte) 0x06); - - buf.put((byte) (equalizer.getClearBass() + 10)); - for (final Integer band : equalizer.getBands()) { - buf.put((byte) (band + 10)); - } - - return encodeMessage( - (byte) 0x0c, - buf.array() - ); - } - - private byte[] encodeDSEEHX(boolean enabled) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0xe8, (byte) 0x02, (byte) 0x00, (byte) (enabled ? 0x01 : 0x00)} - ); - } - - private byte[] encodeTouchSensor(boolean enabled) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0xd8, (byte) 0xd2, (byte) 0x01, (byte) (enabled ? 0x01 : 0x00)} - ); - } - - private byte[] encodeAutomaticPowerOff(AutomaticPowerOff automaticPowerOff) { - return encodeMessage( - (byte) 0x0c, - new byte[]{(byte) 0xf8, (byte) 0x04, (byte) 0x01, automaticPowerOff.code[0], automaticPowerOff.code[1]} - ); - } - - private byte[] encodeVoiceNotifications(boolean enabled) { - return encodeMessage( - (byte) 0x0e, - new byte[]{(byte) 0x48, (byte) 0x01, (byte) 0x01, (byte) (enabled ? 0x01 : 0x00)} - ); - } - - private byte[] encodeMessage(byte type, byte[] content) { - ByteBuffer buf = ByteBuffer.allocate(content.length + 6); - buf.order(ByteOrder.BIG_ENDIAN); - - buf.put(type); - buf.put(sequenceNumber); - buf.putInt(content.length); - buf.put(content); - - return encodeMessage(buf.array()); - } - - private byte[] encodeMessage(byte[] message) { - ByteArrayOutputStream cmdStream = new ByteArrayOutputStream(message.length + 2); - - cmdStream.write(PACKET_HEADER); - - byte checksum = calcChecksum(message, false); - - try { - cmdStream.write(escape(message)); - cmdStream.write(escape(new byte[]{checksum})); - } catch (IOException e) { - LOG.error("This should never happen", e); - } - - cmdStream.write(PACKET_TRAILER); - - return cmdStream.toByteArray(); - } - - private byte[] escape(byte[] bytes) { - ByteArrayOutputStream escapedStream = new ByteArrayOutputStream(bytes.length); - - for (byte b : bytes) { - switch (b) { - case PACKET_HEADER: - case PACKET_TRAILER: - case PACKET_ESCAPE: - escapedStream.write(PACKET_ESCAPE); - escapedStream.write(b & PACKET_ESCAPE_MASK); - break; - default: - escapedStream.write(b); - break; - } - } - - return escapedStream.toByteArray(); - } - - private byte[] unescape(byte[] bytes) { - ByteArrayOutputStream unescapedStream = new ByteArrayOutputStream(bytes.length); - - for (int i = 0; i < bytes.length; i++) { - byte b = bytes[i]; - if (b == PACKET_ESCAPE) { - if (++i >= bytes.length) { - throw new IllegalArgumentException("Invalid escape character at end of array"); + public byte[] encodeInit() { + pendingAcks++; + return new Message( + MessageType.COMMAND_1, + sequenceNumber, + new byte[]{ + (byte) 0x00, + (byte) 0x00 } - unescapedStream.write(b & ~PACKET_ESCAPE_MASK); - } else { - unescapedStream.write(b); - } - } - - return unescapedStream.toByteArray(); + ).encode(); } - public byte calcChecksum(byte[] message, boolean ignoreLastByte) { - int chk = 0; - for (int i = 0; i < message.length - (ignoreLastByte ? 1 : 0); i++) { - chk += message[i] & 255; + public void enqueueRequests(final List requests) { + LOG.debug("Enqueueing {} requests", requests.size()); + + requestQueue.addAll(requests); + } + + public int getPendingAcks() { + return pendingAcks; + } + + public byte[] getFromQueue() { + return requestQueue.remove().encode(sequenceNumber); + } + + private GBDeviceEvent handleAck() { + pendingAcks--; + + if (!requestQueue.isEmpty()) { + LOG.debug("Outstanding requests in queue: {}", requestQueue.size()); + + final Request request = requestQueue.remove(); + + return new GBDeviceEventSendBytes(request.encode(sequenceNumber)); } - return (byte) chk; + + if (GBDevice.State.INITIALIZING.equals(getDevice().getState())) { + // The queue is now empty, so we have got all the information from the device + // Mark it as initialized + + return new GBDeviceEventUpdateDeviceState(GBDevice.State.INITIALIZED); + } + + return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesSupport.java index c1ad96edd..7e3470d2a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesSupport.java @@ -24,9 +24,9 @@ import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.UUID; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; -import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; -import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.wh1000xm3.SonyWh1000Xm3Protocol; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.deviceevents.SonyHeadphonesEnqueueRequestEvent; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; @@ -37,20 +37,12 @@ public class SonyHeadphonesSupport extends AbstractSerialDeviceSupport { @Override public boolean connect() { getDeviceIOThread().start(); - return true; } @Override protected GBDeviceProtocol createDeviceProtocol() { - DeviceType deviceType = getDevice().getType(); - switch (deviceType) { - case SONY_WH_1000XM3: - return new SonyWh1000Xm3Protocol(getDevice()); - default: - LOG.error("Unsupported Sony device type '{}' with key '{}", deviceType, deviceType.getKey()); - return null; - } + return new SonyHeadphonesProtocol(getDevice()); } @Override @@ -63,6 +55,25 @@ public class SonyHeadphonesSupport extends AbstractSerialDeviceSupport { return (SonyHeadphonesIoThread) super.getDeviceIOThread(); } + @Override + public void evaluateGBDeviceEvent(GBDeviceEvent deviceEvent) { + final SonyHeadphonesProtocol sonyProtocol = (SonyHeadphonesProtocol) getDeviceProtocol(); + + if (deviceEvent instanceof SonyHeadphonesEnqueueRequestEvent) { + final SonyHeadphonesEnqueueRequestEvent enqueueRequestEvent = (SonyHeadphonesEnqueueRequestEvent) deviceEvent; + sonyProtocol.enqueueRequests(enqueueRequestEvent.getRequests()); + + if (sonyProtocol.getPendingAcks() == 0) { + // There are no pending acks, send one request from the queue + // TODO: A more elegant way of scheduling these? + SonyHeadphonesIoThread deviceIOThread = getDeviceIOThread(); + deviceIOThread.write(sonyProtocol.getFromQueue()); + } + } + + super.evaluateGBDeviceEvent(deviceEvent); + } + @Override public boolean useAutoConnect() { return false; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/deviceevents/SonyHeadphonesEnqueueRequestEvent.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/deviceevents/SonyHeadphonesEnqueueRequestEvent.java new file mode 100644 index 000000000..473d7e5cb --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/deviceevents/SonyHeadphonesEnqueueRequestEvent.java @@ -0,0 +1,40 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.deviceevents; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; + +public class SonyHeadphonesEnqueueRequestEvent extends GBDeviceEvent { + private final List requests = new ArrayList<>(); + + public SonyHeadphonesEnqueueRequestEvent(final Request request) { + this.requests.add(request); + } + + public SonyHeadphonesEnqueueRequestEvent(final Collection requests) { + this.requests.addAll(requests); + } + + public List getRequests() { + return this.requests; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Message.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Message.java new file mode 100644 index 000000000..4a2b5c2af --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Message.java @@ -0,0 +1,215 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Locale; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.PayloadType; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Message { + private static final Logger LOG = LoggerFactory.getLogger(Message.class); + + /** + * Message format: + *

+ * - MESSAGE_HEADER + * - Message Type ({@link MessageType}) + * - Sequence Number - needs to be updated with the one sent in the ACK responses + * - Payload Length - 4-byte big endian int with number of bytes that will follow + * - N bytes of payload data (first being the {@link PayloadType}) + * - Checksum (1-byte sum, excluding header) + * - MESSAGE_TRAILER + *

+ * Data between MESSAGE_HEADER and MESSAGE_TRAILER is escaped with MESSAGE_ESCAPE, and the + * following byte masked with MESSAGE_ESCAPE_MASK. + */ + + public static final byte MESSAGE_HEADER = 0x3e; + public static final byte MESSAGE_TRAILER = 0x3c; + public static final byte MESSAGE_ESCAPE = 0x3d; + public static final byte MESSAGE_ESCAPE_MASK = (byte) 0b11101111; + + private final MessageType type; + private final byte sequenceNumber; + private final byte[] payload; + + public Message(final MessageType type, final byte sequenceNumber, final byte[] payload) { + this.type = type; + this.sequenceNumber = sequenceNumber; + this.payload = payload; + } + + public byte[] encode() { + final ByteBuffer buf = ByteBuffer.allocate(payload.length + 6); + buf.order(ByteOrder.BIG_ENDIAN); + + buf.put(type.getCode()); + buf.put(sequenceNumber); + buf.putInt(payload.length); + buf.put(payload); + + return encodeMessage(buf.array()); + } + + public MessageType getType() { + return this.type; + } + + public byte getSequenceNumber() { + return this.sequenceNumber; + } + + public byte[] getPayload() { + return this.payload; + } + + public String toString() { + if (payload.length > 0) { + final PayloadType payloadType = PayloadType.fromCode(type, payload[0]); + return String.format(Locale.getDefault(), "Message{Cmd=%s, Seq=%d, PayloadType=%s, Payload=%s}", type, sequenceNumber, payloadType, GB.hexdump(payload)); + } else { + return String.format(Locale.getDefault(), "Message{Cmd=%s, Seq=%d}", type, sequenceNumber); + } + } + + public static Message fromBytes(final byte[] rawBytes) { + if (rawBytes[0] != MESSAGE_HEADER) { + throw new IllegalArgumentException(String.format("Invalid header %02x", rawBytes[0])); + } + + if (rawBytes[rawBytes.length - 1] != MESSAGE_TRAILER) { + throw new IllegalArgumentException(String.format("Invalid trailer %02x", rawBytes[0])); + } + + final byte[] messageBytes = unescape(rawBytes); + final String hexdump = GB.hexdump(messageBytes, 0, messageBytes.length); + + final byte messageChecksum = messageBytes[messageBytes.length - 2]; + final byte expectedChecksum = calcChecksum(messageBytes, 1, messageBytes.length - 2); + if (messageChecksum != expectedChecksum) { + LOG.warn(String.format("Invalid checksum %02x for %s (expected %02x)", messageChecksum, hexdump, expectedChecksum)); + return null; + } + + final int payloadLength = ((messageBytes[3] << 24) & 0xFF000000) | + ((messageBytes[4] << 16) & 0xFF0000) | + ((messageBytes[5] << 8) & 0xFF00) | + (messageBytes[6] & 0xFF); + if (payloadLength != messageBytes.length - 9) { + LOG.warn("Unexpected payload length {}, expected {}", messageBytes.length - 7, payloadLength); + return null; + } + + final byte rawMessageType = messageBytes[1]; + final MessageType messageType = MessageType.fromCode(rawMessageType); + if (messageType == null) { + LOG.warn("Unknown message type {}", String.format("%02x", rawMessageType)); + return null; + } + + final byte sequenceNumber = messageBytes[2]; + final byte[] payload = new byte[payloadLength]; + System.arraycopy(messageBytes, 7, payload, 0, payloadLength); + + return new Message(messageType, sequenceNumber, payload); + } + + public static byte[] encodeMessage(byte[] message) { + final ByteArrayOutputStream cmdStream = new ByteArrayOutputStream(message.length + 2); + + cmdStream.write(MESSAGE_HEADER); + + final byte checksum = calcChecksum(message, 0, message.length); + + try { + cmdStream.write(escape(message)); + cmdStream.write(escape(new byte[]{checksum})); + } catch (final IOException e) { + LOG.error("This should never happen", e); + } + + cmdStream.write(MESSAGE_TRAILER); + + return cmdStream.toByteArray(); + } + + public static byte[] escape(byte[] bytes) { + final ByteArrayOutputStream escapedStream = new ByteArrayOutputStream(bytes.length); + + for (byte b : bytes) { + switch (b) { + case MESSAGE_HEADER: + case MESSAGE_TRAILER: + case MESSAGE_ESCAPE: + escapedStream.write(MESSAGE_ESCAPE); + escapedStream.write(b & MESSAGE_ESCAPE_MASK); + break; + default: + escapedStream.write(b); + break; + } + } + + return escapedStream.toByteArray(); + } + + public static byte[] unescape(byte[] bytes) { + final ByteArrayOutputStream unescapedStream = new ByteArrayOutputStream(bytes.length); + + for (int i = 0; i < bytes.length; i++) { + final byte b = bytes[i]; + if (b == MESSAGE_ESCAPE) { + if (++i >= bytes.length) { + throw new IllegalArgumentException("Invalid escape character at end of array"); + } + unescapedStream.write(bytes[i] | ~MESSAGE_ESCAPE_MASK); + } else { + unescapedStream.write(b); + } + } + + return unescapedStream.toByteArray(); + } + + public static byte calcChecksum(byte[] message, int start, int end) { + int chk = 0; + for (int i = start; i < end; i++) { + chk += message[i] & 255; + } + return (byte) chk; + } + + public static byte[] hexToBytes(final String payloadHex) { + final String[] parts = payloadHex.split(":"); + + final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + + for (final String b : parts) { + stream.write((byte) ((Character.digit(b.charAt(0), 16) << 4) + Character.digit(b.charAt(1), 16))); + } + + return stream.toByteArray(); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/MessageType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/MessageType.java new file mode 100644 index 000000000..906cb8341 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/MessageType.java @@ -0,0 +1,45 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol; + +public enum MessageType { + ACK(0x01), + COMMAND_1(0x0c), + COMMAND_2(0x0e), + + UNKNOWN(0xff); + + private final byte code; + + MessageType(final int code) { + this.code = (byte) code; + } + + public byte getCode() { + return this.code; + } + + public static MessageType fromCode(final byte code) { + for (final MessageType messageType : values()) { + if (messageType.code == code) { + return messageType; + } + } + + return MessageType.UNKNOWN; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Request.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Request.java new file mode 100644 index 000000000..9c1ca06f2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/Request.java @@ -0,0 +1,58 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol; + +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class Request { + private final MessageType messageType; + private final byte[] payload; + + public Request(final MessageType messageType, final byte[] payload) { + this.messageType = messageType; + this.payload = payload; + } + + public Request(final MessageType messageType, final int... payload) { + this.messageType = messageType; + this.payload = new byte[payload.length]; + + for (int i = 0; i < payload.length; i++) { + this.payload[i] = (byte) payload[i]; + } + } + + public MessageType messageType() { + return this.messageType; + } + + public byte[] payload() { + return this.payload; + } + + public final Message toMessage(final byte sequenceNumber) { + return new Message(messageType(), sequenceNumber, payload()); + } + + public final byte[] encode(final byte sequenceNumber) { + return toMessage(sequenceNumber).encode(); + } + + public static Request fromHex(final MessageType messageType, final String payload) { + return new Request(messageType, GB.hexStringToByteArray(payload)); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java new file mode 100644 index 000000000..6e53d06ca --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java @@ -0,0 +1,90 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AudioUpsampling; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AutomaticPowerOff; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerPreset; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SoundPosition; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SurroundMode; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.TouchSensor; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType; + +public abstract class AbstractSonyProtocolImpl { + private final GBDevice device; + + public AbstractSonyProtocolImpl(final GBDevice device) { + this.device = device; + } + + protected GBDevice getDevice() { + return this.device; + } + + public abstract Request getAmbientSoundControl(); + + public abstract Request setAmbientSoundControl(final AmbientSoundControl config); + + public abstract Request getAudioCodec(); + + public abstract Request getBattery(final BatteryType batteryType); + + public abstract Request getFirmwareVersion(); + + public abstract Request getAudioUpsampling(); + + public abstract Request setAudioUpsampling(final AudioUpsampling config); + + public abstract Request getAutomaticPowerOff(); + + public abstract Request setAutomaticPowerOff(final AutomaticPowerOff config); + + public abstract Request getEqualizer(); + + public abstract Request setEqualizerPreset(final EqualizerPreset config); + + public abstract Request setEqualizerCustomBands(final EqualizerCustomBands config); + + public abstract Request getSoundPosition(); + + public abstract Request setSoundPosition(final SoundPosition config); + + public abstract Request getSurroundMode(); + + public abstract Request setSurroundMode(final SurroundMode config); + + public abstract Request getTouchSensor(); + + public abstract Request setTouchSensor(final TouchSensor config); + + public abstract Request getVoiceNotifications(); + + public abstract Request setVoiceNotifications(final VoiceNotifications config); + + public abstract Request startNoiseCancellingOptimizer(); + + public abstract List handlePayload(final MessageType messageType, final byte[] payload); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/PayloadType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/PayloadType.java new file mode 100644 index 000000000..6af86dce5 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/PayloadType.java @@ -0,0 +1,111 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; + +public enum PayloadType { + INIT_REQUEST(MessageType.COMMAND_1, 0x00), + INIT_REPLY(MessageType.COMMAND_1, 0x01), + + FW_VERSION_REQUEST(MessageType.COMMAND_1, 0x04), + FW_VERSION_REPLY(MessageType.COMMAND_1, 0x05), + + INIT_2_REQUEST(MessageType.COMMAND_1, 0x06), + INIT_2_REPLY(MessageType.COMMAND_1, 0x07), + + BATTERY_LEVEL_REQUEST(MessageType.COMMAND_1, 0x10), + BATTERY_LEVEL_REPLY(MessageType.COMMAND_1, 0x11), + BATTERY_LEVEL_NOTIFY(MessageType.COMMAND_1, 0x13), + + AUDIO_CODEC_REQUEST(MessageType.COMMAND_1, 0x18), + AUDIO_CODEC_REPLY(MessageType.COMMAND_1, 0x19), + AUDIO_CODEC_NOTIFY(MessageType.COMMAND_1, 0x1b), + + SOUND_POSITION_OR_MODE_GET(MessageType.COMMAND_1, 0x46), + SOUND_POSITION_OR_MODE_RET(MessageType.COMMAND_1, 0x47), + SOUND_POSITION_OR_MODE_SET(MessageType.COMMAND_1, 0x48), + SOUND_POSITION_OR_MODE_NOTIFY(MessageType.COMMAND_1, 0x49), + + EQUALIZER_GET(MessageType.COMMAND_1, 0x56), + EQUALIZER_RET(MessageType.COMMAND_1, 0x57), + EQUALIZER_SET(MessageType.COMMAND_1, 0x58), + EQUALIZER_NOTIFY(MessageType.COMMAND_1, 0x59), + + AMBIENT_SOUND_CONTROL_GET(MessageType.COMMAND_1, 0x66), + AMBIENT_SOUND_CONTROL_RET(MessageType.COMMAND_1, 0x67), + AMBIENT_SOUND_CONTROL_SET(MessageType.COMMAND_1, 0x68), + AMBIENT_SOUND_CONTROL_NOTIFY(MessageType.COMMAND_1, 0x69), + + NOISE_CANCELLING_OPTIMIZER_START_REQUEST(MessageType.COMMAND_1, 0x84), + + TOUCH_SENSOR_GET(MessageType.COMMAND_1, 0xd6), + TOUCH_SENSOR_RET(MessageType.COMMAND_1, 0xd7), + TOUCH_SENSOR_SET(MessageType.COMMAND_1, 0xd8), + TOUCH_SENSOR_NOTIFY(MessageType.COMMAND_1, 0xd9), + + AUDIO_UPSAMPLING_GET(MessageType.COMMAND_1, 0xe6), + AUDIO_UPSAMPLING_RET(MessageType.COMMAND_1, 0xe7), + AUDIO_UPSAMPLING_SET(MessageType.COMMAND_1, 0xe8), + AUDIO_UPSAMPLING_NOTIFY(MessageType.COMMAND_1, 0xe9), + + AUTOMATIC_POWER_OFF_BUTTON_MODE_GET(MessageType.COMMAND_1, 0xf6), + AUTOMATIC_POWER_OFF_BUTTON_MODE_RET(MessageType.COMMAND_1, 0xf7), + AUTOMATIC_POWER_OFF_BUTTON_MODE_SET(MessageType.COMMAND_1, 0xf8), + AUTOMATIC_POWER_OFF_BUTTON_MODE_NOTIFY(MessageType.COMMAND_1, 0xf9), + + // TODO: The headphones spit out a lot of json, analyze it + JSON_GET(MessageType.COMMAND_1, 0xc4), + JSON_RET(MessageType.COMMAND_1, 0xc9), + + // TODO: App sends those sometimes + SOMETHING_GET(MessageType.COMMAND_1, 0x90), + SOMETHING_RET(MessageType.COMMAND_1, 0x91), + + VOICE_NOTIFICATIONS_GET(MessageType.COMMAND_2, 0x46), + VOICE_NOTIFICATIONS_RET(MessageType.COMMAND_2, 0x47), + VOICE_NOTIFICATIONS_SET(MessageType.COMMAND_2, 0x48), + VOICE_NOTIFICATIONS_NOTIFY(MessageType.COMMAND_2, 0x49), + + UNKNOWN(MessageType.UNKNOWN, 0xff); + + private final MessageType messageType; + private final byte code; + + PayloadType(final MessageType messageType, final int code) { + this.messageType = messageType; + this.code = (byte) code; + } + + public MessageType getMessageType() { + return this.messageType; + } + + public byte getCode() { + return this.code; + } + + public static PayloadType fromCode(final MessageType messageType, final byte code) { + for (final PayloadType payloadType : values()) { + if (messageType.equals(payloadType.messageType) && payloadType.code == code) { + return payloadType; + } + } + + return PayloadType.UNKNOWN; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java new file mode 100644 index 000000000..93a3fc3a2 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java @@ -0,0 +1,829 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdateDeviceInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControl; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AudioUpsampling; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AutomaticPowerOff; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerCustomBands; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.EqualizerPreset; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SoundPosition; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.SurroundMode; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.TouchSensor; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.VoiceNotifications; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.deviceevents.SonyHeadphonesEnqueueRequestEvent; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.AbstractSonyProtocolImpl; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.AudioCodec; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.BatteryType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params.VirtualSoundParam; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class SonyProtocolImplV1 extends AbstractSonyProtocolImpl { + private static final Logger LOG = LoggerFactory.getLogger(SonyProtocolImplV1.class); + + public SonyProtocolImplV1(GBDevice device) { + super(device); + } + + @Override + public Request getAmbientSoundControl() { + return new Request( + PayloadType.AMBIENT_SOUND_CONTROL_GET.getMessageType(), + new byte[]{ + PayloadType.AMBIENT_SOUND_CONTROL_GET.getCode(), + (byte) 0x02 + } + ); + } + + @Override + public Request setAmbientSoundControl(final AmbientSoundControl ambientSoundControl) { + final ByteBuffer buf = ByteBuffer.allocate(8); + + buf.put(PayloadType.AMBIENT_SOUND_CONTROL_SET.getCode()); + buf.put((byte) 0x02); + if (AmbientSoundControl.Mode.OFF.equals(ambientSoundControl.getMode())) { + buf.put((byte) 0x00); + } else { + buf.put((byte) 0x11); + } + buf.put((byte) 0x01); + + switch (ambientSoundControl.getMode()) { + case NOISE_CANCELLING: + buf.put((byte) 2); + break; + case WIND_NOISE_REDUCTION: + buf.put((byte) 1); + break; + case OFF: + case AMBIENT_SOUND: + default: + buf.put((byte) 0); + break; + } + + buf.put((byte) 0x01); + buf.put((byte) (ambientSoundControl.isFocusOnVoice() ? 0x01 : 0x00)); + + switch (ambientSoundControl.getMode()) { + case OFF: + case AMBIENT_SOUND: + buf.put((byte) (ambientSoundControl.getAmbientSound() + 1)); + break; + case WIND_NOISE_REDUCTION: + case NOISE_CANCELLING: + buf.put((byte) 0); + break; + } + + return new Request(PayloadType.AMBIENT_SOUND_CONTROL_SET.getMessageType(), buf.array()); + } + + @Override + public Request getAudioCodec() { + return new Request( + PayloadType.AUDIO_CODEC_REQUEST.getMessageType(), + new byte[]{ + PayloadType.AUDIO_CODEC_REQUEST.getCode(), + (byte) 0x00 + } + ); + } + + @Override + public Request getBattery(BatteryType batteryType) { + return new Request( + PayloadType.BATTERY_LEVEL_REQUEST.getMessageType(), + new byte[]{ + PayloadType.BATTERY_LEVEL_REQUEST.getCode(), + batteryType.getCode() + } + ); + } + + @Override + public Request getFirmwareVersion() { + return new Request( + PayloadType.FW_VERSION_REQUEST.getMessageType(), + new byte[]{ + PayloadType.FW_VERSION_REQUEST.getCode(), + (byte) 0x02 + } + ); + } + + @Override + public Request getAudioUpsampling() { + return new Request( + PayloadType.AUDIO_UPSAMPLING_GET.getMessageType(), + new byte[]{ + PayloadType.AUDIO_UPSAMPLING_GET.getCode(), + (byte) 0x02 + } + ); + } + + @Override + public Request setAudioUpsampling(AudioUpsampling config) { + return new Request( + PayloadType.AUDIO_UPSAMPLING_SET.getMessageType(), + new byte[]{ + PayloadType.AUDIO_UPSAMPLING_SET.getCode(), + (byte) 0x02, + (byte) 0x00, + (byte) (config.isEnabled() ? 0x01 : 0x00) + } + ); + } + + @Override + public Request getAutomaticPowerOff() { + return new Request( + PayloadType.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(), + new byte[]{ + PayloadType.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(), + (byte) 0x04 + } + ); + } + + @Override + public Request setAutomaticPowerOff(final AutomaticPowerOff config) { + return new Request( + PayloadType.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(), + new byte[]{ + PayloadType.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(), + (byte) 0x04, + (byte) 0x01, + config.getCode()[0], + config.getCode()[1] + } + ); + } + + @Override + public Request getEqualizer() { + return new Request( + PayloadType.EQUALIZER_GET.getMessageType(), + new byte[]{ + PayloadType.EQUALIZER_GET.getCode(), + (byte) 0x01 + } + ); + } + + @Override + public Request setEqualizerPreset(final EqualizerPreset config) { + return new Request( + PayloadType.EQUALIZER_SET.getMessageType(), + new byte[]{ + PayloadType.EQUALIZER_SET.getCode(), + (byte) 0x01, + config.getCode(), + (byte) 0x00 + } + ); + } + + @Override + public Request setEqualizerCustomBands(final EqualizerCustomBands config) { + final ByteBuffer buf = ByteBuffer.allocate(10); + + buf.put(PayloadType.EQUALIZER_SET.getCode()); + buf.put((byte) 0x01); + buf.put((byte) 0xff); + buf.put((byte) 0x06); + + buf.put((byte) (config.getBass() + 10)); + for (final Integer band : config.getBands()) { + buf.put((byte) (band + 10)); + } + + return new Request( + PayloadType.EQUALIZER_SET.getMessageType(), + buf.array() + ); + } + + @Override + public Request getSoundPosition() { + return new Request( + PayloadType.SOUND_POSITION_OR_MODE_GET.getMessageType(), + new byte[]{ + PayloadType.SOUND_POSITION_OR_MODE_GET.getCode(), + (byte) 0x02 + } + ); + } + + @Override + public Request setSoundPosition(final SoundPosition config) { + return new Request( + PayloadType.SOUND_POSITION_OR_MODE_SET.getMessageType(), + new byte[]{ + PayloadType.SOUND_POSITION_OR_MODE_SET.getCode(), + VirtualSoundParam.SOUND_POSITION.getCode(), + config.getCode() + } + ); + } + + @Override + public Request getSurroundMode() { + return new Request( + PayloadType.SOUND_POSITION_OR_MODE_GET.getMessageType(), + new byte[]{ + PayloadType.SOUND_POSITION_OR_MODE_GET.getCode(), + VirtualSoundParam.SURROUND_MODE.getCode() + } + ); + } + + @Override + public Request setSurroundMode(final SurroundMode config) { + return new Request( + PayloadType.SOUND_POSITION_OR_MODE_SET.getMessageType(), + new byte[]{ + PayloadType.SOUND_POSITION_OR_MODE_SET.getCode(), + VirtualSoundParam.SURROUND_MODE.getCode(), + config.getCode() + } + ); + } + + @Override + public Request getTouchSensor() { + return new Request( + PayloadType.TOUCH_SENSOR_GET.getMessageType(), + new byte[]{ + PayloadType.TOUCH_SENSOR_GET.getCode(), + (byte) 0xd2 + } + ); + } + + @Override + public Request setTouchSensor(final TouchSensor config) { + return new Request( + PayloadType.TOUCH_SENSOR_SET.getMessageType(), + new byte[]{ + PayloadType.TOUCH_SENSOR_SET.getCode(), + (byte) 0xd2, + (byte) 0x01, + (byte) (config.isEnabled() ? 0x01 : 0x00) + } + ); + } + + @Override + public Request getVoiceNotifications() { + return new Request( + PayloadType.VOICE_NOTIFICATIONS_GET.getMessageType(), + new byte[]{ + PayloadType.VOICE_NOTIFICATIONS_GET.getCode(), + (byte) 0x01, + (byte) 0x01 + } + ); + } + + @Override + public Request setVoiceNotifications(final VoiceNotifications config) { + return new Request( + PayloadType.VOICE_NOTIFICATIONS_SET.getMessageType(), + new byte[]{ + PayloadType.VOICE_NOTIFICATIONS_SET.getCode(), + (byte) 0x01, + (byte) 0x01, + (byte) (config.isEnabled() ? 0x01 : 0x00) + } + ); + } + + @Override + public Request startNoiseCancellingOptimizer() { + return new Request( + PayloadType.NOISE_CANCELLING_OPTIMIZER_START_REQUEST.getMessageType(), + new byte[]{ + PayloadType.NOISE_CANCELLING_OPTIMIZER_START_REQUEST.getCode(), + (byte) 0x01, + (byte) 0x00, + (byte) 0x01 + } + ); + } + + @Override + public List handlePayload(final MessageType messageType, final byte[] payload) { + final PayloadType payloadType = PayloadType.fromCode(messageType, payload[0]); + + switch (payloadType) { + case INIT_REPLY: + return handleInitResponse(payload); + case FW_VERSION_REPLY: + return handleFirmwareVersion(payload); + case BATTERY_LEVEL_REPLY: + case BATTERY_LEVEL_NOTIFY: + return handleBattery(payload); + case AUDIO_CODEC_REPLY: + case AUDIO_CODEC_NOTIFY: + return handleAudioCodec(payload); + case SOUND_POSITION_OR_MODE_RET: + case SOUND_POSITION_OR_MODE_NOTIFY: + return handleVirtualSound(payload); + case EQUALIZER_RET: + case EQUALIZER_NOTIFY: + return handleEqualizer(payload); + case AMBIENT_SOUND_CONTROL_RET: + case AMBIENT_SOUND_CONTROL_NOTIFY: + return handleAmbientSoundControl(payload); + case TOUCH_SENSOR_RET: + case TOUCH_SENSOR_NOTIFY: + return handleTouchSensor(payload); + case AUDIO_UPSAMPLING_RET: + case AUDIO_UPSAMPLING_NOTIFY: + return handleAudioUpsampling(payload); + case AUTOMATIC_POWER_OFF_BUTTON_MODE_RET: + case AUTOMATIC_POWER_OFF_BUTTON_MODE_NOTIFY: + return handleAutomaticPowerOff(payload); + case VOICE_NOTIFICATIONS_RET: + case VOICE_NOTIFICATIONS_NOTIFY: + return handleVoiceNotifications(payload); + case JSON_RET: + return handleJson(payload); + } + + LOG.warn("Unhandled payload type code {}", String.format("%02x", payload[0])); + + return Collections.emptyList(); + } + + public List handleInitResponse(final byte[] payload) { + if (payload.length != 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + final DeviceType deviceType = getDevice().getType(); + + final List capabilityRequests = new ArrayList<>(); + + // Populate the init requests + // TODO: We should be determine which of these we need from the device... + switch (deviceType) { + case SONY_WH_1000XM3: + capabilityRequests.add(getFirmwareVersion()); + capabilityRequests.add(getBattery(BatteryType.SINGLE)); + capabilityRequests.add(getAudioCodec()); + capabilityRequests.add(getAmbientSoundControl()); + capabilityRequests.add(getAudioUpsampling()); + capabilityRequests.add(getVoiceNotifications()); + capabilityRequests.add(getAutomaticPowerOff()); + capabilityRequests.add(getTouchSensor()); + capabilityRequests.add(getSurroundMode()); + capabilityRequests.add(getSoundPosition()); + capabilityRequests.add(getEqualizer()); + break; + default: + LOG.error("Unsupported Sony device type '{}' with key '{}'", deviceType, deviceType.getKey()); + return null; + } + + return Collections.singletonList(new SonyHeadphonesEnqueueRequestEvent(capabilityRequests)); + } + + public List handleAmbientSoundControl(final byte[] payload) { + if (payload.length != 8) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + AmbientSoundControl.Mode mode = null; + + if (payload[2] == (byte) 0x00) { + mode = AmbientSoundControl.Mode.OFF; + } else if (payload[2] == (byte) 0x01) { + // Enabled, determine mode + + if (payload[4] == (byte) 0x00) { + mode = AmbientSoundControl.Mode.AMBIENT_SOUND; + } else if (payload[4] == (byte) 0x01) { + // FIXME: ANC gets incorrectly identified wind reduction for WF-SP800N + mode = AmbientSoundControl.Mode.WIND_NOISE_REDUCTION; + } else if (payload[4] == (byte) 0x02) { + mode = AmbientSoundControl.Mode.NOISE_CANCELLING; + } + } + + if (mode == null) { + LOG.warn("Unable to determine ambient sound control mode from {}", GB.hexdump(payload)); + return Collections.emptyList(); + } + + boolean focusOnVoice; + switch (payload[6]) { + case 0x00: + focusOnVoice = false; + break; + case 0x01: + focusOnVoice = true; + break; + default: + LOG.warn("Unknown focus on voice mode {}", String.format("%02x", payload[6])); + return Collections.emptyList(); + } + + int ambientSound = payload[7]; + if (ambientSound < 0 || ambientSound > 20) { + LOG.warn("Ambient sound level {} is out of range", String.format("%02x", payload[7])); + return Collections.emptyList(); + } + + final AmbientSoundControl ambientSoundControl = new AmbientSoundControl(mode, focusOnVoice, ambientSound); + + LOG.warn("Ambient sound control: {}", ambientSoundControl); + + final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences() + .withPreferences(ambientSoundControl.toPreferences()); + + return Collections.singletonList(eventUpdatePreferences); + } + + public List handleAudioUpsampling(final byte[] payload) { + if (payload.length != 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + boolean enabled; + + switch (payload[3]) { + case 0x00: + enabled = false; + break; + case 0x01: + enabled = true; + break; + default: + LOG.warn("Unknown audio upsampling code {}", String.format("%02x", payload[3])); + return Collections.emptyList(); + } + + LOG.debug("Audio Upsampling: {}", enabled); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new AudioUpsampling(enabled).toPreferences()); + + return Collections.singletonList(event); + } + + public List handleAutomaticPowerOff(final byte[] payload) { + if (payload.length != 5) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x04) { + // TODO: Handle these (Button Mode uses the same payload type?) + LOG.warn("Not automatic power off config, ignoring"); + return Collections.emptyList(); + } + + AutomaticPowerOff mode = null; + + for (AutomaticPowerOff value : AutomaticPowerOff.values()) { + if (value.getCode()[0] == payload[3] && value.getCode()[1] == payload[4]) { + mode = value; + break; + } + } + + if (mode == null) { + LOG.warn("Unknown automatic power off codes {}", String.format("%02x %02x", payload[3], payload[4])); + return Collections.emptyList(); + } + + LOG.debug("Automatic Power Off: {}", mode); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(mode.toPreferences()); + + return Collections.singletonList(event); + } + + public List handleBattery(final byte[] payload) { + final BatteryType batteryType = BatteryType.fromCode(payload[1]); + + if (batteryType == null) { + LOG.warn("Unknown battery type code {}", String.format("%02x", payload[1])); + return Collections.emptyList(); + } + + final List batteryEvents = new ArrayList<>(); + + if (BatteryType.SINGLE.equals(batteryType) || BatteryType.CASE.equals(batteryType)) { + // Single battery / Case battery + LOG.debug("Battery Level: {}: {}", batteryType, payload[2]); + + final GBDeviceEventBatteryInfo singleBatteryInfo = new GBDeviceEventBatteryInfo(); + singleBatteryInfo.batteryIndex = 0; + singleBatteryInfo.state = BatteryState.BATTERY_NORMAL; + singleBatteryInfo.level = payload[2]; + + batteryEvents.add(singleBatteryInfo); + } else if (BatteryType.DUAL.equals(batteryType)) { + // Dual Battery (L / R) + LOG.debug("Battery Level: L: {}, R: {}", payload[2], payload[4]); + + if (payload[2] != 0) { + final GBDeviceEventBatteryInfo gbDeviceEventBatteryInfoLeft = new GBDeviceEventBatteryInfo(); + + gbDeviceEventBatteryInfoLeft.batteryIndex = 1; + gbDeviceEventBatteryInfoLeft.state = BatteryState.BATTERY_NORMAL; + gbDeviceEventBatteryInfoLeft.level = payload[2]; + + batteryEvents.add(gbDeviceEventBatteryInfoLeft); + } + + if (payload[4] != 0) { + final GBDeviceEventBatteryInfo gbDeviceEventBatteryInfoRight = new GBDeviceEventBatteryInfo(); + + gbDeviceEventBatteryInfoRight.batteryIndex = 2; + gbDeviceEventBatteryInfoRight.state = BatteryState.BATTERY_NORMAL; + gbDeviceEventBatteryInfoRight.level = payload[4]; + + batteryEvents.add(gbDeviceEventBatteryInfoRight); + } + } + + return batteryEvents; + } + + public List handleAudioCodec(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + final AudioCodec audioCodec = AudioCodec.fromCode(payload[2]); + + if (audioCodec == null) { + LOG.warn("Unable to determine audio codec from {}", GB.hexdump(payload)); + return Collections.emptyList(); + } + + final GBDeviceEventUpdateDeviceInfo gbDeviceEventUpdateDeviceInfo = new GBDeviceEventUpdateDeviceInfo("AUDIO_CODEC: ", audioCodec.name()); + + final GBDeviceEventUpdatePreferences gbDeviceEventUpdatePreferences = new GBDeviceEventUpdatePreferences() + .withPreference(DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_CODEC, audioCodec.name().toLowerCase(Locale.getDefault())); + + return Arrays.asList(gbDeviceEventUpdateDeviceInfo, gbDeviceEventUpdatePreferences); + } + + public List handleEqualizer(final byte[] payload) { + if (payload.length != 10) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + EqualizerPreset mode = null; + for (EqualizerPreset value : EqualizerPreset.values()) { + if (value.getCode() == payload[2]) { + mode = value; + break; + } + } + + if (mode == null) { + LOG.warn("Unknown equalizer preset code {}", String.format("%02x", payload[2])); + return Collections.emptyList(); + } + + LOG.debug("Equalizer Preset: {}", mode); + + final int clearBass = payload[4] - 10; + final List bands = new ArrayList<>(5); + + for (int i = 0; i < 5; i++) { + bands.add(payload[5 + i] - 10); + } + + final EqualizerCustomBands customBands = new EqualizerCustomBands(bands, clearBass); + + LOG.info("Equalizer Custom Bands: {}", customBands); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(mode.toPreferences()) + .withPreferences(customBands.toPreferences()); + + return Collections.singletonList(event); + } + + public List handleFirmwareVersion(final byte[] payload) { + final Pattern VERSION_REGEX = Pattern.compile("^[0-9.\\-a-zA-Z_]+$"); + + if (payload.length < 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + final String firmwareVersion = new String(Arrays.copyOfRange(payload, 3, payload.length)); + + if (!VERSION_REGEX.matcher(firmwareVersion).find()) { + LOG.warn("Unexpected characters in version '{}'", firmwareVersion); + return Collections.emptyList(); + } + + LOG.debug("Firmware Version: {}", firmwareVersion); + + final GBDeviceEventVersionInfo gbDeviceEventVersionInfo = new GBDeviceEventVersionInfo(); + gbDeviceEventVersionInfo.fwVersion = firmwareVersion; + return Collections.singletonList(gbDeviceEventVersionInfo); + } + + public List handleJson(final byte[] payload) { + // TODO analyze json? + + if (payload.length < 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + final String jsonString = new String(Arrays.copyOfRange(payload, 4, payload.length)); + + LOG.debug("Got json: {}", jsonString); + + return Collections.emptyList(); + } + + public List handleVirtualSound(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + final VirtualSoundParam virtualSoundParam = VirtualSoundParam.fromCode(payload[1]); + if (virtualSoundParam == null) { + LOG.warn("Unknown payload subtype code {}", String.format("%02x", payload[1])); + return Collections.emptyList(); + } + + switch (virtualSoundParam) { + case SURROUND_MODE: + return handleSurroundMode(payload); + case SOUND_POSITION: + return handleSoundPosition(payload); + } + + return Collections.emptyList(); + } + + public List handleSoundPosition(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + SoundPosition mode = null; + + for (SoundPosition value : SoundPosition.values()) { + if (value.getCode() == payload[2]) { + mode = value; + break; + } + } + + if (mode == null) { + LOG.warn("Unknown sound position code {}", String.format("%02x", payload[2])); + return Collections.emptyList(); + } + + LOG.debug("Sound Position: {}", mode); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(mode.toPreferences()); + + return Collections.singletonList(event); + } + + public List handleSurroundMode(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + SurroundMode mode = null; + + for (SurroundMode value : SurroundMode.values()) { + if (value.getCode() == payload[2]) { + mode = value; + break; + } + } + + if (mode == null) { + LOG.warn("Unknown surround mode code {}", String.format("%02x", payload[2])); + return Collections.emptyList(); + } + + LOG.debug("Surround Mode: {}", mode); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(mode.toPreferences()); + + return Collections.singletonList(event); + } + + public List handleTouchSensor(final byte[] payload) { + if (payload.length != 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + boolean enabled; + + switch (payload[3]) { + case 0x00: + enabled = false; + break; + case 0x01: + enabled = true; + break; + default: + LOG.warn("Unknown touch sensor code {}", String.format("%02x", payload[3])); + return Collections.emptyList(); + } + + LOG.debug("Touch Sensor: {}", enabled); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new TouchSensor(enabled).toPreferences()); + + return Collections.singletonList(event); + } + + public List handleVoiceNotifications(final byte[] payload) { + if (payload.length != 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + boolean enabled; + + switch (payload[3]) { + case 0x00: + enabled = false; + break; + case 0x01: + enabled = true; + break; + default: + LOG.warn("Unknown voice notifications code {}", String.format("%02x", payload[3])); + return Collections.emptyList(); + } + + LOG.debug("Voice Notifications: {}", enabled); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new VoiceNotifications(enabled).toPreferences()); + + return Collections.singletonList(event); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/ButtonMode.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/AudioCodec.java similarity index 55% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/ButtonMode.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/AudioCodec.java index 6d0d25655..c3e042783 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/ButtonMode.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/AudioCodec.java @@ -14,17 +14,32 @@ 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.sony.headphones; +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params; -public enum ButtonMode { - OFF((byte) 0xff), - AMBIENT_SOUND_CONTROL((byte) 0x00), - PLAYBACK_CONTROL((byte) 0x20), - VOLUME_CONTROL((byte) 0x10); +public enum AudioCodec { + SBC(0x01), + AAC(0x02), + LDAC(0x10), + APTX(0x20), + APTX_HD(0x21); - public final byte code; + private final byte code; - ButtonMode(final byte code) { - this.code = code; + AudioCodec(final int code) { + this.code = (byte) code; + } + + public byte getCode() { + return this.code; + } + + public static AudioCodec fromCode(final byte code) { + for (final AudioCodec audioCodec : values()) { + if (audioCodec.code == code) { + return audioCodec; + } + } + + return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Protocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java similarity index 61% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Protocol.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java index 0ee549824..30e04235a 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/wh1000xm3/SonyWh1000Xm3Protocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java @@ -14,13 +14,30 @@ 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.sony.headphones.wh1000xm3; +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.SonyHeadphonesProtocol; +public enum BatteryType { + SINGLE(0x00), + DUAL(0x01), + CASE(0x02); -public class SonyWh1000Xm3Protocol extends SonyHeadphonesProtocol { - public SonyWh1000Xm3Protocol(GBDevice device) { - super(device); + private final byte code; + + BatteryType(final int code) { + this.code = (byte) code; + } + + public byte getCode() { + return this.code; + } + + public static BatteryType fromCode(final byte code) { + for (final BatteryType batteryType : values()) { + if (batteryType.code == code) { + return batteryType; + } + } + + return null; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/VirtualSoundParam.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/VirtualSoundParam.java new file mode 100644 index 000000000..cb1460da8 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/VirtualSoundParam.java @@ -0,0 +1,43 @@ +/* Copyright (C) 2021 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params; + +public enum VirtualSoundParam { + SURROUND_MODE(0x01), + SOUND_POSITION(0x02), + ; + + private final byte code; + + VirtualSoundParam(final int code) { + this.code = (byte) code; + } + + public byte getCode() { + return this.code; + } + + public static VirtualSoundParam fromCode(final byte code) { + for (final VirtualSoundParam virtualSound : values()) { + if (virtualSound.code == code) { + return virtualSound; + } + } + + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index b422784f4..99e6f98f4 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -113,7 +113,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.um25.Coordinator.UM25Coordin import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.waspos.WaspOSCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.wh1000xm3.SonyWh1000Xm3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -313,8 +313,8 @@ public class DeviceHelper { result.add(new FitProDeviceCoordinator()); result.add(new Ear1Coordinator()); result.add(new GalaxyBudsDeviceCoordinator()); - result.add(new GalaxyBudsLiveDeviceCoordinator()); - result.add(new SonyWh1000Xm3Coordinator()); + result.add(new GalaxyBudsLiveDeviceCoordinator()); + result.add(new SonyWH1000XM3Coordinator()); return result; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 843d5db5b..54574ac5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1401,7 +1401,7 @@ Manual Custom 1 Custom 2 - Manual Preset + Bands Custom Preset 1 Custom Preset 2 400 @@ -1410,7 +1410,7 @@ 6.3k 16k Clear Bass - DSEE HX + Audio Upsampling Touch sensor control Notifications & Voice Guide Automatic Power Off diff --git a/app/src/main/res/xml/devicesettings_automatic_power_off.xml b/app/src/main/res/xml/devicesettings_automatic_power_off.xml new file mode 100644 index 000000000..6b11d5424 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_automatic_power_off.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_header_other.xml b/app/src/main/res/xml/devicesettings_header_other.xml new file mode 100644 index 000000000..684ea1362 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_header_other.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_header_system.xml b/app/src/main/res/xml/devicesettings_header_system.xml new file mode 100644 index 000000000..d5408f624 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_header_system.xml @@ -0,0 +1,6 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_ambient_sound_control.xml b/app/src/main/res/xml/devicesettings_sony_headphones_ambient_sound_control.xml index e1fa7b422..0945b5df7 100644 --- a/app/src/main/res/xml/devicesettings_sony_headphones_ambient_sound_control.xml +++ b/app/src/main/res/xml/devicesettings_sony_headphones_ambient_sound_control.xml @@ -13,6 +13,7 @@ android:summary="%s" android:title="@string/sony_ambient_sound" /> + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml index 1cffd036c..abc973b2f 100644 --- a/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml +++ b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml @@ -1,7 +1,10 @@ - - + + + + + - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_notifications_voice_guide.xml b/app/src/main/res/xml/devicesettings_sony_headphones_notifications_voice_guide.xml new file mode 100644 index 000000000..1eb8d7ab9 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_notifications_voice_guide.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_other.xml b/app/src/main/res/xml/devicesettings_sony_headphones_other.xml deleted file mode 100644 index 7102a0562..000000000 --- a/app/src/main/res/xml/devicesettings_sony_headphones_other.xml +++ /dev/null @@ -1,31 +0,0 @@ - - - - - - - - - - - diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_sound_position.xml b/app/src/main/res/xml/devicesettings_sony_headphones_sound_position.xml new file mode 100644 index 000000000..5e3bb41dc --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_sound_position.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_surround_mode.xml b/app/src/main/res/xml/devicesettings_sony_headphones_surround_mode.xml new file mode 100644 index 000000000..0b8244cf5 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_surround_mode.xml @@ -0,0 +1,11 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_touch_sensor_single.xml b/app/src/main/res/xml/devicesettings_sony_headphones_touch_sensor_single.xml new file mode 100644 index 000000000..5f141262e --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_touch_sensor_single.xml @@ -0,0 +1,8 @@ + + + +