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/huami/amazfitbipu/AmazfitBipUCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbipu/AmazfitBipUCoordinator.java index 0ebb9e96f..bcd415b47 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbipu/AmazfitBipUCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/huami/amazfitbipu/AmazfitBipUCoordinator.java @@ -97,6 +97,7 @@ public class AmazfitBipUCoordinator extends HuamiCoordinator { public int[] getSupportedDeviceSpecificSettings(GBDevice device) { return new int[]{ R.xml.devicesettings_amazfitbipu, + //R.xml.devicesettings_canned_dismisscall_16, R.xml.devicesettings_timeformat, R.xml.devicesettings_wearlocation, R.xml.devicesettings_custom_emoji_font, 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/model/AppNotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java index 6cda8596e..9f1c7312b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/AppNotificationType.java @@ -78,6 +78,7 @@ public class AppNotificationType extends HashMap { // Telegram put("org.telegram.messenger", NotificationType.TELEGRAM); put("org.telegram.messenger.beta", NotificationType.TELEGRAM); + put("org.telegram.messenger.web", NotificationType.TELEGRAM); put("org.telegram.plus", NotificationType.TELEGRAM); // "Plus Messenger" put("org.thunderdog.challegram", NotificationType.TELEGRAM); put("nekox.messenger", NotificationType.TELEGRAM); @@ -170,6 +171,9 @@ public class AppNotificationType extends HashMap { // Etar put("ws.xsoh.etar", NotificationType.GENERIC_CALENDAR); + + // Discord + put("com.discord", NotificationType.DISCORD); } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java index 759a4a315..56389ab09 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/NotificationType.java @@ -63,6 +63,7 @@ public enum NotificationType { THREEMA(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.JaegerGreen), KONTALK(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.JaegerGreen), ANTOX(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.JaegerGreen), + DISCORD(PebbleIconID.NOTIFICATION_HIPCHAT, PebbleColor.Purpureus), TRANSIT(PebbleIconID.LOCATION, PebbleColor.JaegerGreen), TWITTER(PebbleIconID.NOTIFICATION_TWITTER, PebbleColor.BlueMoon), VIBER(PebbleIconID.NOTIFICATION_VIBER, PebbleColor.VividViolet), @@ -122,6 +123,7 @@ public enum NotificationType { case SLACK: case LINE: case VIBER: + case DISCORD: return "generic_chat"; case GMAIL: case GOOGLE_INBOX: 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/huami/HuamiIcon.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiIcon.java index 0a790a9db..72550e85b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiIcon.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiIcon.java @@ -64,28 +64,28 @@ public class HuamiIcon { public static byte mapToIconId(NotificationType type) { switch (type) { case UNKNOWN: + case GENERIC_NAVIGATION: return APP_11; case CONVERSATIONS: case RIOT: case HIPCHAT: case KONTALK: case ANTOX: + case GENERIC_SMS: + case WECHAT: return WECHAT; case GENERIC_EMAIL: case GMAIL: case YAHOO_MAIL: case OUTLOOK: return EMAIL; - case GENERIC_NAVIGATION: - return APP_11; - case GENERIC_SMS: - return WECHAT; case GENERIC_CALENDAR: case BUSINESS_CALENDAR: return CALENDAR; case FACEBOOK: return FACEBOOK; case FACEBOOK_MESSENGER: + case SIGNAL: return FACEBOOK_MESSENGER; case GOOGLE_HANGOUTS: case GOOGLE_MESSENGER: @@ -97,9 +97,8 @@ public class HuamiIcon { return KAKAOTALK; case LINE: return LINE; - case SIGNAL: - return FACEBOOK_MESSENGER; case WIRE: + case THREEMA: return CHAT_BLUE_13; case TWITTER: return TWITTER; @@ -109,12 +108,9 @@ public class HuamiIcon { return SNAPCHAT; case TELEGRAM: return TELEGRAM; - case THREEMA: - return CHAT_BLUE_13; case VIBER: + case DISCORD: return VIBER; - case WECHAT: - return WECHAT; case WHATSAPP: return WHATSAPP; case GENERIC_ALARM_CLOCK: @@ -122,4 +118,30 @@ public class HuamiIcon { } return APP_11; } + + //amazfit workaround + public static boolean acceptsSender(byte iconId){ + switch(iconId){ + case WECHAT: + case PENGUIN_1: + case MI_CHAT_2: + case SNAPCHAT: + case WHATSAPP: + case RED_WHITE_FIRE_8: + case INSTAGRAM: + case CHAT_BLUE_13: + case COW_14: + case CHINESE_20: + case FACEBOOK_MESSENGER: + case VIBER: + case LINE: + case TELEGRAM: + case VKONTAKTE: + case CHINESE_32: + case EMAIL: + return true; + } + + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java index c927c1958..654927db9 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/HuamiSupport.java @@ -1004,6 +1004,27 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { @Override public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + if (cannedMessagesSpec.type == CannedMessagesSpec.TYPE_REJECTEDCALLS) { + try { + TransactionBuilder builder = performInitialized("Set canned messages"); + + int handle = 0x12345678; + for (String cannedMessage : cannedMessagesSpec.cannedMessages) { + int length = cannedMessage.getBytes().length + 5; + ByteBuffer buf = ByteBuffer.allocate(length); + buf.order(ByteOrder.LITTLE_ENDIAN); + + buf.put((byte) 0x05); // create + buf.putInt(handle++); + buf.put(cannedMessage.getBytes()); + + writeToChunked2021(builder, (short) 0x0013, getNextHandle(), buf.array(), false, false); + } + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to set time on Huami device", ex); + } + } } private boolean isAlarmClockRinging() { @@ -2976,7 +2997,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } byte[] command = ArrayUtils.addAll(new byte[]{0x00, 0x00, (byte) (0xc0 | type), 0x00}, data); - writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_COMPAT, getNextHandle(), command, encrypt); + writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_COMPAT, getNextHandle(), command, true, encrypt); } else { writeToChunkedOld(builder, type, data); } @@ -3010,13 +3031,17 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } } - public void writeToChunked2021(TransactionBuilder builder, short type, byte handle, byte[] data, boolean encrypt) { + public void writeToChunked2021(TransactionBuilder builder, short type, byte handle, byte[] data, boolean extended_flags, boolean encrypt) { int remaining = data.length; int length = data.length; byte count = 0; - int header_size = 11; + int header_size = 10; - if (encrypt) { + if (extended_flags) { + header_size++; + } + + if (extended_flags && encrypt) { byte[] messagekey = new byte[16]; for (int i = 0; i < 16; i++) { messagekey[i] = (byte) (sharedSessionKey[i] ^ handle); @@ -3060,26 +3085,40 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { } if (count == 0) { flags |= 0x01; - chunk[5] = (byte) (length & 0xff); - chunk[6] = (byte) ((length >> 8) & 0xff); - chunk[7] = (byte) ((length >> 16) & 0xff); - chunk[8] = (byte) ((length >> 24) & 0xff); - chunk[9] = (byte) (type & 0xff); - chunk[10] = (byte) ((type >> 8) & 0xff); + int i = 4; + if (extended_flags) { + i++; + } + chunk[i++] = (byte) (length & 0xff); + chunk[i++] = (byte) ((length >> 8) & 0xff); + chunk[i++] = (byte) ((length >> 16) & 0xff); + chunk[i++] = (byte) ((length >> 24) & 0xff); + chunk[i++] = (byte) (type & 0xff); + chunk[i] = (byte) ((type >> 8) & 0xff); } if (remaining <= MAX_CHUNKLENGTH) { flags |= 0x06; // last chunk? } chunk[0] = 0x03; chunk[1] = flags; - chunk[2] = 0; - chunk[3] = handle; - chunk[4] = count; + if (extended_flags) { + chunk[2] = 0; + chunk[3] = handle; + chunk[4] = count; + } else { + chunk[2] = handle; + chunk[3] = count; + } System.arraycopy(data, data.length - remaining, chunk, header_size, copybytes); builder.write(characteristicChunked2021Write, chunk); remaining -= copybytes; - header_size = 5; + header_size = 4; + + if (extended_flags) { + header_size++; + } + count++; } } @@ -3087,7 +3126,7 @@ public class HuamiSupport extends AbstractBTLEDeviceSupport { public void writeToConfiguration(TransactionBuilder builder, byte[] data) { if (force2021Protocol) { data = ArrayUtils.insert(0, data, (byte) 1); - writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_COMPAT, getNextHandle(), data, true); + writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_COMPAT, getNextHandle(), data, true, true); } else { builder.write(getCharacteristic(HuamiService.UUID_CHARACTERISTIC_3_CONFIGURATION), data); } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitgts2/AmazfitGTS2MiniSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitgts2/AmazfitGTS2MiniSupport.java index 2730c7a99..612e94c8f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitgts2/AmazfitGTS2MiniSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/amazfitgts2/AmazfitGTS2MiniSupport.java @@ -18,17 +18,32 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgts2; import android.content.Context; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.net.Uri; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.io.IOException; import nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiFWHelper; import nodomain.freeyourgadget.gadgetbridge.devices.huami.amazfitgts2.AmazfitGTS2MiniFWHelper; import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiSupport; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationType; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertCategory; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.AlertNotificationProfile; +import nodomain.freeyourgadget.gadgetbridge.service.btle.profiles.alertnotification.NewAlert; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.HuamiIcon; +import nodomain.freeyourgadget.gadgetbridge.service.devices.huami.amazfitgts.AmazfitGTSSupport; +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class AmazfitGTS2MiniSupport extends AmazfitGTS2Support { + private static final Logger LOG = LoggerFactory.getLogger(AmazfitGTS2MiniSupport.class); + @Override protected HuamiSupport setLanguage(TransactionBuilder builder) { return setLanguageByIdNew(builder); @@ -38,4 +53,122 @@ public class AmazfitGTS2MiniSupport extends AmazfitGTS2Support { public HuamiFWHelper createFWHelper(Uri uri, Context context) throws IOException { return new AmazfitGTS2MiniFWHelper(uri, context); } + + @Override + protected void sendNotificationNew(NotificationSpec notificationSpec, boolean hasExtraHeader, int maxLength) { + // step 1: bail out if this is an alarm clock notification + + if (notificationSpec.type == NotificationType.GENERIC_ALARM_CLOCK) { + onAlarmClock(notificationSpec); + return; + } + + // step 2: (formerly in try block) get notification type + AlertCategory alertCategory = AlertCategory.CustomHuami; + byte customIconId = HuamiIcon.mapToIconId(notificationSpec.type); + + // step 3: build notification (sender+body) + /* + * Format followed by the device: + * \0 \0 + * sender will get ignored except for the icons + * specified on the HuamiIcon class. + * for email, App Suffix will be taken as sender + */ + String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title); + boolean acceptsSender = HuamiIcon.acceptsSender(customIconId); + String message; + + if (!acceptsSender && !senderOrTitle.equals(notificationSpec.sourceName)) { + // make sure we always include the notification sender/title + message = "-\0"; //leave title blank, it's useless + message += StringUtils.truncate(senderOrTitle, 64) + "\n"; + } else { + message = StringUtils.truncate(senderOrTitle, 64) + "\0"; + } + + if (notificationSpec.subject != null) { + message += StringUtils.truncate(notificationSpec.subject, 128) + "\n\n"; + } + if (notificationSpec.body != null) { + message += StringUtils.truncate(notificationSpec.body, 512); + } + if (notificationSpec.body == null && notificationSpec.subject == null) { + message += " "; // if we have no body we have to send at least something on some devices, else they reboot (Bip S) + } + + try { + TransactionBuilder builder = performInitialized("new notification"); + + // step 4: append suffix + byte[] appSuffix = "\0 \0".getBytes(); + int suffixlength = appSuffix.length; + // The SMS icon for AlertCategory.SMS is unique and not available as iconId + if (notificationSpec.type == NotificationType.GENERIC_SMS) { + alertCategory = AlertCategory.SMS; + } + // EMAIL icon does not work in FW 0.0.8.74, it did in 0.0.7.90 (old comment) + // EMAIL will take the sender from the suffix instead + else if (customIconId == HuamiIcon.EMAIL) { + alertCategory = AlertCategory.Email; + appSuffix = ("\0"+senderOrTitle+"\0").getBytes(); + suffixlength = appSuffix.length; + } + + // if I understood correctly, we don't need the extra logic for mi band 2 here + int prefixlength = 2; + + if (alertCategory == AlertCategory.CustomHuami) { + String appName; + prefixlength = 3; + final PackageManager pm = getContext().getPackageManager(); + ApplicationInfo ai = null; + try { + ai = pm.getApplicationInfo(notificationSpec.sourceAppId, 0); + } catch (PackageManager.NameNotFoundException ignored) { + } + + if (ai == null) { + appName = "\0" + "UNKNOWN" + "\0"; + } else { + appName = "\0" + pm.getApplicationLabel(ai) + "\0"; + } + appSuffix = appName.getBytes(); + suffixlength = appSuffix.length; + } + if (hasExtraHeader) { + prefixlength += 4; + } + + // final step: build command + byte[] rawmessage = message.getBytes(); + int length = Math.min(rawmessage.length, maxLength - prefixlength); + if (length < rawmessage.length) { + length = StringUtils.utf8ByteLength(message, length); + } + + byte[] command = new byte[length + prefixlength + suffixlength]; + int pos = 0; + command[pos++] = (byte) alertCategory.getId(); + if (hasExtraHeader) { + command[pos++] = 0; // TODO + command[pos++] = 0; + command[pos++] = 0; + command[pos++] = 0; + } + command[pos++] = 1; + if (alertCategory == AlertCategory.CustomHuami) { + command[pos] = customIconId; + } + + System.arraycopy(rawmessage, 0, command, prefixlength, length); + System.arraycopy(appSuffix, 0, command, prefixlength + length, appSuffix.length); + + writeToChunked(builder, 0, command); + + builder.queue(getQueue()); + } catch (IOException ex) { + LOG.error("Unable to send notification to device", ex); + } + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java index a42ff6483..e4a5b17cc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/huami/operations/InitOperation2021.java @@ -87,7 +87,7 @@ public class InitOperation2021 extends InitOperation { sendPubkeyCommand[3] = 0x02; System.arraycopy(publicEC, 0, sendPubkeyCommand, 4, 48); //testAuth(); - huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), sendPubkeyCommand, false); + huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), sendPubkeyCommand, true, false); } private native byte[] ecdh_generate_public(byte[] privateEC); @@ -170,7 +170,7 @@ public class InitOperation2021 extends InitOperation { System.arraycopy(encryptedRandom1, 0, command, 1, 16); System.arraycopy(encryptedRandom2, 0, command, 17, 16); TransactionBuilder builder = createTransactionBuilder("Sending double encryted random to device"); - huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), command, false); + huamiSupport.writeToChunked2021(builder, HuamiService.CHUNKED2021_ENDPOINT_AUTH, huamiSupport.getNextHandle(), command, true, false); huamiSupport.performImmediately(builder); } } catch (Exception e) { 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-ca/strings.xml b/app/src/main/res/values-ca/strings.xml index c66f90e54..403fa91c5 100644 --- a/app/src/main/res/values-ca/strings.xml +++ b/app/src/main/res/values-ca/strings.xml @@ -89,7 +89,7 @@ Suport per a notificacions genèriques … també amb la pantalla encesa No molesteu - En aquest mode s\'aturen les notifications no desitjades + Blocar totes les notificacions quan el \"No molesteu\" estigui activat al telèfon Transliteració Activeu aquesta opció si el vostre aparell no pot mostrar els caràcters de la vostra llengua Sempre @@ -550,10 +550,10 @@ Comparteix Reinicialitza la dada d\'extracció Estat - Activitat + Historial d\'entrenaments El Temps Alarma - Temporitzador + Compte enrere Brúixola Configuració Alipay @@ -1001,13 +1001,13 @@ Última notificació Cronòmetre Alexa - Fes una foto + Comandament de la càmera Silencia el telèfon Troba el telèfon Rellotge mundial Desconegut Estrès - Període + Seguiment del cicle Respiració Lefun Lemfo SG2 @@ -1260,4 +1260,76 @@ Passes Tipus: Coordenada X (màxim 240): + Configureu els recordatoris + Configureu els recordatoris + Hora + %1$s, una vegada + %1$s, cada setmana + una vegada + Cada any + Esborrar recordatori + Esteu segur que voleu esborrar el recordatori\? + No hi ha espais lliures + El dispositiu no té espais lliures per a recordatoris (espais totals: %1$s) + Detalls del recordatori + Resultats de la mesura + Recordatoris a reservar per esdeveniments futurs + Nombre d\'esdeveniments del calendari que seran sincronitzats + Configureu quan l\'aparell pitarà + Preconfiguracions + Informació d\'activitat a la tarja de l\'aparell + Configuració per aplicació + Useu la llista d\'aplicacions per a ... + Rebutgeu les notificacions de les aplicacions triades + Permeteu les notificacions de les aplicacions triades + Transferència de dades + Bateria baixa + Cada dia + Missatge + %1$s, cada dia + La distància es calcula a partir de les passes i la llargada de la passa (configurable a “Configuració - Sobre tu”) + Useu fons negre amb el Tema Fosc + Activeu en cas que l\'aparell no es connecti després d\'una actualització de microprogramari + Repetir + Data + %1$s, cada mes + %1$s, cada any + Cada mes + Cada setmana + Seguiment Pomodoro + Voleu exportar dades\? Les dades d’activitat exportades anteriorment (si n’hi ha) i les preferències se sobreescriuran. + Sony WH-1000XM3 + Premeu una estona el botó per desar la preconfiguració + Activar totes les aplicacions + Desactivar totes les aplicacions + Llargada mitjana d\'una braçada + L\'aplicació no ha d\'estar seleccionar per a ser configurada + L\'aplicació ha d\'estar seleccionar per a ser configurada + 10 passes + 100 passes + Accions per a l\'aplicació Commute + Configuració per a programadors + Envieu i descarregueu fitxers + Configureu la funcionalitat dels botons físics del rellotge + bpm + Gestió d\'apps + Acció d\'editar + Envieu els missatges configurats més avall al vostre aparell + Configuració i funcionalitat usada per programadors + Permetre el intents externs perillosos + SMA-Q2 OSS + Algun(s) fitxer(s) ja existeixen. Sobreescriure\'ls\? + Activant aquesta opció farà que s\'ignorin els aparells que ja han estat vinculats/aparellats a l\'escanejar + Fitxer(s) GPX rebut(s): + Autenticació fallada, funcionalitat limitada + No disponible en mode no autenticat + 1 passa + Comproveu i demaneu els permisos que faltin tot i que poden no ser necessitats en aquest moment. Desactiveu això només si el vostre aparell realment no suporta cap d\'aquestes característiques. No donar un permís pot causar problemes! + Algunes funcions estan desactivades perquè el microprogramari del rellotge és massa nou + Activa el suport de la nova API de CompanionDevice (només té efecte en Android 8 o superior) que incrementarà la fiabilitat si el servei s\'ha de reiniciar en segon pla; requereix re-aparellar amb Gadgetbridge per tenir efecte + COMPTE: Error al comprovar la informació de versió! No hauríeu de continuar! Nom de versió vist: \"%s\" + Mitjana de braçades + Les accions configurades aquí apareixeran a l\'aplicació Commute del rellotge. Llegiu la wiki per obtenir informació sobre com gestionar els intents produïts per aquestes accions. + Ignoreu les trucades del rellotge amb un missatge SMS + Botons físics \ No newline at end of file diff --git a/app/src/main/res/values-cs/strings.xml b/app/src/main/res/values-cs/strings.xml index b3f1ad603..4737018e7 100644 --- a/app/src/main/res/values-cs/strings.xml +++ b/app/src/main/res/values-cs/strings.xml @@ -514,7 +514,7 @@ Alipay (Zástupce) Počasí (Zástupce) Stav - Aktivita + Historie Aktivit Počasí Budík Časovač @@ -988,7 +988,7 @@ Párování systémem Doprovodného Zařízení Poloha musí být povolena Stres - Cykly + Záznam cyklistiky Dýchání PineTime (JF Firmware) TLW64 @@ -1452,4 +1452,46 @@ Ruční předvolba Vlastní předvolba 2 Při sundání + Upravit připomenutí + Upravit připomenutí + %1$s, Jednou + %1$s, Týdně + %1$s, Denně + %1$s, Měsíčně + Měsíčně + Ročně + Smazat připomenutí + Opravdu chcete smazat připomenutí\? + Nejsou volná místa + Zařízení nemá volná místa pro další připomenutí (celková místa: %1$s) + Detaily připomenutí + Rezervovaná místa připomenutí pro nadcházející události kalendáře + Povolit potvrzení párování na zařízení + Dlouhým podržením tlačítka uložte předvolbu + Denně + Text + Opakování + Čas + Datum + %1$s, Ročně + Jednou + Týdně + Počet kalendářových událostí k synchronizaci + Zobrazit orámování po časovém limitu: + Potvrzení párování na zařízení může být otravné. Jejich vypnutím můžete přijít o funkčnost. + Předvolby + Časová zóna: + Časový limit aktualizace v minutách: + Skrytí textu při vypršení časového limitu: + Průměr + Minimum + Max krok + Min krok + Průměrný rytmus + Max rytmus + kroky/min + Zapnout displej při zvednutí během režimu Nerušit + Min Tepová frekvence + Max Tepová frekvence + Min rytmus \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index af7e3b2b9..b07568f92 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -457,7 +457,7 @@ \nINSTALLATION AUF EIGENE GEFAHR! Anzahl der Tastendrücke Streichen nach links/rechts im Diagrammbetrieb aktivieren - Wenn aktiviert, können Zifferblätter Wetter, Akkuinfos, usw. anzeigen + Wenn aktiviert, kann die Uhr Wetter, Akkuinfos, usw. anzeigen Ganztägige Messung der Herzfrequenz einmal pro Minute alle 5 Minuten @@ -520,10 +520,10 @@ Speicherort für Export auswählen Allgemein Status - Aktivität + Trainingsverlauf Wetter Alarm - Timer + Countdown Kompass Einstellungen Alipay @@ -764,8 +764,8 @@ Status und Wecker Wecker einstellen nach: - %d Std - %d Std + %d Stunde + %d Stunden Makibes HR3-Einstellungen Makibes HR3 @@ -866,7 +866,7 @@ Automatischer Abruf Raw Activity Dateien speichern Training - Herzfrequenz + Puls Watch X Plus Watch X Kalibrierung @@ -886,7 +886,7 @@ Uhr-Energiesparmodus Ereigniserinnerung PAI - Erzwinge die automatische Zeitsynchronisation beim erneuten Verbinden. Analoge Zeiger können die falsche Zeit anzeigen! + Erzwinge automatische Zeitsynchronisation beim erneuten Verbinden. Analoge Uhrzeiger können dabei die falsche Uhrzeit anzeigen! Kopiere Aktion vom Uhr-Knopf Schüttelgeste Anruf ignorieren/zurückzuweisen Aus - ignorieren, An - zurückweisen @@ -926,15 +926,15 @@ Über Gadgetbridge Über Wird für den LineageOS Wetterdienst genutzt, andere Android-Versionen müssen eine Anwendung wie Weather notification nutzen. Mehr Informationen gibt es im Gadgetbride-Wiki. - Weltzeit + Weltzeituhr ACHTUNG: Fehler beim Prüfen der Information zur Version! Du solltest nicht fortfahren! Versionsname \"%s\" gefunden Alle Berechtigungen sind notwendig und es kann zu Problemen führen, wenn sie nicht genehmigt werden Stress - Periode + Zyklus-Tracking Start des Hintergrunddienstes fehlgeschlagen wegen einem Fehler. Fehlermeldung: Hintergrunddienst konnte nicht gestartet werden, wegen… Start des Hintergrunddienstes fehlgeschlagen - Atmung + Atemübungen Mi Band 5 Du bist dabei, die Firmware %s auf dein Mi Band 5 zu installieren. \n @@ -996,12 +996,12 @@ Beim Einschlafen Yoga Schwimmen (Freiwasser) - Schläge + Züge kcal Seilspringen Crosstrainer - Indoor Cycling - str/s + Spinning + Züge/s Schwimmstil sek min/km @@ -1031,7 +1031,7 @@ Höhe bpm sek/km - str + Züge Flach Durchschnittliche Schläge Durchschnittliche Schlag Distanz @@ -1079,8 +1079,8 @@ Um diesen Screenshot zu teilen, installiere eine App, die Bilddateien verarbeiten kann. Minimale Aktivitätsdauer (Minuten) Pausenlänge zum Trennen von Aktivitäten (Minuten) - Minimale Schritte pro Minute zur Erkennung von Aktivität - Minimale Schritte pro Minute zur Lauferkennung + Minimale Schritte pro Minute zur Erkennung einer Aktivität (Gehen) + Minimale Schritte pro Minute zur Erkennung einer Laufaktivität Sprache der Benutzeroberfläche Das Band wird vibrieren, wenn dein Telefon die Verbindung zum Band unterbricht Anti-Verlust @@ -1125,9 +1125,9 @@ Stoppuhr Nicht stören Alexa - Foto machen - Telefon stumm schalten - Telefon finden + Kamera-Fernauslösung + Telefon stummschalten + Telefon suchen Amazfit GTR 2 Amazfit GTS 2 Bewegungsintensität @@ -1322,8 +1322,8 @@ Zweite Zeitzone Kalorien Akku - Handbewegung ohne Handgelenk deaktivieren - Bildschirmaktualisierungen ohne Handgelenk deaktivieren + Bewegungserkennung deaktivieren wenn Uhr nicht getragen + Bildschirmaktualisierungen deaktivieren wenn Uhr nicht getragen Aktivieren, wenn das Gerät nach einem Firmware-Upgrade keine Verbindung mehr herstellt Neues Auth-Protokoll Taschenlampe @@ -1482,4 +1482,13 @@ App muss zum Konfigurieren ausgewählt sein Druckentlastung mit Umgebungsgeräuschen Verhindert Druckgefühl in den Ohren, wenn Active Noise Cancelling nicht verwendet wird + Im Ruhemodes Anzeige beim Anheben aktivieren + Aktualisierungszeit in Minuten: + Aktivitätsinfo in der Geräteübersicht + Auswahl der Aktivitätsinfos in der Geräteübersicht + Aktivitätsinfos in der Geräteübersicht anzeigen + Zeige Schritte, Distanz oder Schlaf in der Geräteübersicht + Strecke basiert auf Schrittzahl und Schrittlänge (siehe Einstellungen - Über Dich) + Angeregt + Batteriefach \ No newline at end of file diff --git a/app/src/main/res/values-en-rGB/strings.xml b/app/src/main/res/values-en-rGB/strings.xml index 9d00acca0..7a2f0f9ce 100644 --- a/app/src/main/res/values-en-rGB/strings.xml +++ b/app/src/main/res/values-en-rGB/strings.xml @@ -335,10 +335,10 @@ Alipay Settings Compass - Timer + Countdown Alarm Weather - Activity + Workout History Status Reset fetch date Share @@ -950,7 +950,7 @@ Data transfer Low battery Lefun - Period + Cycle Tracking Stress DND Stopwatch @@ -1329,7 +1329,7 @@ Amazfit X Zepp E Find Phone - Take a photo + Camera Remote Alexa Middle Button short Upper Button long @@ -1479,4 +1479,15 @@ Every month The device has no free slots for reminders (total slots: %1$s) Number of calendar events that will be synchronized + Activate display upon lift during Do Not Disturb + Max Heartrate + steps/min + Minimum + Average Cadence + Average + Max Cadence + Min Cadence + Min Heartrate + Max Stride + Min Stride \ No newline at end of file diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 2cf7464eb..d55914263 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -513,7 +513,7 @@ Notificaciones Tiempo Alarma - Temporizador + Cuenta atrás Brújula Ajustes Música @@ -605,7 +605,7 @@ Roidmi 3 Casio GB-6900 Estado - Actividad + Historial del entrenamiento Alipay Establezca la hora que su dispositivo le está mostrando en este momento. Tenga en cuenta que Gadgetbridge registra archivos que pueden contener mucha información personal, incluidos, entre otros, datos de salud, identificadores únicos (como la dirección MAC de un dispositivo), preferencias de música, etc. Considere editar el archivo y eliminar esta información antes de enviar el archivo a un informe público. @@ -1118,7 +1118,7 @@ Cronómetro No molestar Alexa - Tomar una foto + Control remoto de la cámara Silenciar teléfono Encontrar teléfono Reloj mundial @@ -1128,7 +1128,7 @@ SpO2 Ritmo cardíaco PAI - Periodo + Seguimiento del ciclo Respiración Lefun Lemfo SG2 @@ -1486,4 +1486,15 @@ Número de eventos del calendario que se sincronizarán Activar la confirmación del emparejamiento en el dispositivo Las confirmaciones de emparejamiento en el dispositivo pueden resultar molestas. Desactivarlas puede hacer que pierda funcionalidad. + Activar la pantalla al levantarse durante la función \"No molestar\" + Media + Mínimo + Ritmo cardíaco máximo + Ritmo cardíaco mínimo + Zancada mínima + Cadencia media + Cadencia mínima + pasos/min + Zancada máxima + Cadencia máxima \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cdd130468..86adbbd62 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -516,10 +516,10 @@ Temps de sommeil préféré en heures Partager Réinitialiser la date de récupération État - Activité + Historique d\'activité Météo Alarme - Minuteur + Décompte Boussole Réglages Alipay @@ -1121,7 +1121,7 @@ Temps de sommeil préféré en heures Étape NPD Alexa - Prendre une photo + Appareil photo à distance Téléphone muet Trouver mon téléphone Intensité du mouvement @@ -1486,4 +1486,14 @@ Temps de sommeil préféré en heures Nombre d\'évènements du calendrier qui seront synchronisés Détails du rappel Activer l\'écran lors d\'un retournement en mode Ne Pas Déranger + Rythme cardiaque max + Distance de pas max + Distance de pas min + Cadence min + pas/min + Moyenne + Cadence moyenne + Minimum + Rythme cardiaque min + Cadence max \ No newline at end of file diff --git a/app/src/main/res/values-he/strings.xml b/app/src/main/res/values-he/strings.xml index b63a5fb90..299f7ffff 100644 --- a/app/src/main/res/values-he/strings.xml +++ b/app/src/main/res/values-he/strings.xml @@ -509,10 +509,10 @@ שיתוף איפוס מועד הקבלה מצב - פעילות + היסטוריית אימונים מזג אוויר שעון מעורר - קוצב זמן + ספירה לאחור מצפן הגדרות Alipay @@ -931,7 +931,7 @@ \n \nהמשך מעבר לנקודה זו הוא על אחריותך! לחץ - משך + מעקב מחזוריות נשימה Mi Band 5 הפעלת אפשרות זו תתעלם ממכשירים שכבר אוגדו/צומדו טרם הסריקה @@ -1117,7 +1117,7 @@ שעון עצר לא להפריע Alexa - צילום תמונה + שליטה במצלמה השתקת הטלפון איתור הטלפון חוזק תנועה @@ -1481,4 +1481,15 @@ כל שנה אין חלונות פנויים מספר אירועי לוח השנה שיסונכרנו + הפעלת התצוגה עקב הנפה במצב לא להפריע + ממוצע + קצב מזערי + מזערי + דופק מרבי + דופק מזערי + קצב ממוצע + קצב מרבי + צעדים לדקה + צעד מרבי + צעד מזערי \ No newline at end of file diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 3bfce0826..bf0d8d275 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -530,10 +530,10 @@ Alipay (snelkoppeling) Weer (snelkoppeling) Status - Activiteit + Trainingsgeschiedenis Weer Alarm - Timer + Aftellen Kompas Instellingen Alipay @@ -925,7 +925,7 @@ LET OP: Fout bij het controleren van versie-informatie! Je moet niet doorgaan! Zag versienaam \"%s\" Al deze toestemmingen zijn vereist en er kan instabiliteit ontstaan als ze niet worden verleend Spanning - Cycli + Cyclus volgen Ademhaling Mi Band 5 U staat op het punt om de %s-firmware op je Mi Band 5 te installeren. @@ -1031,7 +1031,7 @@ Stopwatch Niet storen Alexa - Neem een foto + Camera-afstandsbediening Telefoon dempen Vind telefoon SpO2 @@ -1481,4 +1481,15 @@ Weet je zeker dat je de herinnering wilt verwijderen\? Geen vrije plekken Herinneringen instellen + Activeer het display bij het optillen tijdens Niet Storen + Max hartslag + Max paslengte + Gemiddelde cadans + Min hartslag + Minimum + Gemiddeld + Min paslengte + Min cadans + stappen/min + Max cadans \ No newline at end of file diff --git a/app/src/main/res/values-pl/strings.xml b/app/src/main/res/values-pl/strings.xml index 2c6fca512..419b7a1cd 100644 --- a/app/src/main/res/values-pl/strings.xml +++ b/app/src/main/res/values-pl/strings.xml @@ -282,7 +282,7 @@ Ukryj powiadomienia z Gadgetbridge Ikona na pasku stanu i powiadomienia będą wyświetlane na zablokowanym ekranie Ikona na pasku stanu i powiadomienia będą ukrywane na zablokowanym ekranie - Niechciane powiadomienia są wyłączone w tym trybie + Blokuj wszystkie powiadomienia, gdy tryb \"Nie przeszkadzać\" jest włączony w telefonie Transliteracja Włącz tę opcję, jeśli twoje urządzenie nie obsługuje czcionki twojego języka Prywatność @@ -576,10 +576,10 @@ Teclast H30 XWatch Status - Aktywność + Historia treningów Pogoda Budzik - Minutnik + Odliczanie Kompas Ustawienia Alipay @@ -941,7 +941,7 @@ Lokalizacja musi być włączona Wszystkie te uprawnienia są wymagane, a ich brak może spowodować niestabilność Dodatkowe wsparcie dla urządzeń - Okres + Śledzenie cyklu Oddychanie PineTime (Firmware JF) Szczegóły aktywności sportowej @@ -1123,7 +1123,7 @@ Stoper Nie przeszkadzać Alexa - Zrób zdjęcie + Zdalna kamera Wycisz telefon Znajdź telefon Dystans @@ -1401,4 +1401,15 @@ 3 godziny Sony WH-1000XM3 5 minut + Data + Czas + Wiadomość + Mowa + Podbicie basu + Klub + Wokal + Korektor dźwięku + Strefa czasowa: + Wyłączony + Korektor dźwięku \ No newline at end of file diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index e4a7dc928..eada6eda6 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -372,7 +372,7 @@ Canlı etkinlik Erkek Süre - Etkinlik + Egzersiz Geçmişi Alarm Alipay Pusula @@ -384,7 +384,7 @@ Alipay (Kısayol) Hava Durumu (Kısayol) Durum - Zamanlama + Geri Sayım Hava Durumu MAC adresi verilmedi, eşleştirilemiyor. Otomatik (uyku algılama) @@ -948,7 +948,7 @@ Bir istisna nedeniyle arka plan hizmetini başlatma başarısız oldu. Hata: Arka plan hizmeti başlatılamadı Stres - Döngü + Döngü İzleme Nefes Alma Mi Band 5 Mi Band 5 aygıtınıza %s ürün yazılımını kurmak üzeresiniz. @@ -1139,9 +1139,9 @@ %1$s aransın mı\? Amazfit GTR 2 Kronometre - Rahatsız etmeyin + Rahatsız Etme Alexa - Fotoğraf çek + Kamera Kumandası Telefonun Sesini Kapat Telefonu Bul Hareket yoğunluğu @@ -1508,4 +1508,14 @@ %1$s, her yıl Bir defa Hatırlatıcıyı silmek istediğinizden emin misiniz\? + Rahatsız Etme sırasında kaldırıldığında ekranı etkinleştir + En Yüksek Kalp Ritmi + Ortalama + Asgari + En Düşük Kalp Ritmi + En Yüksek Tempo + En Uzun Adım + En Kısa Adım + Ortalama Tempo + En Düşük Tempo \ No newline at end of file diff --git a/app/src/main/res/values-uk/strings.xml b/app/src/main/res/values-uk/strings.xml index 2b11eefa5..97e68a713 100644 --- a/app/src/main/res/values-uk/strings.xml +++ b/app/src/main/res/values-uk/strings.xml @@ -435,10 +435,10 @@ Вибрати теку експорту Стан Сповіщення - Активність + Історія тренувань Погода Будильник - Таймер + Відлік Компас Налаштування Музика @@ -990,7 +990,7 @@ Звичайний крок Швидки темп Найповільніший темп - Максимум + Максимальна Калорії Активно Кроки @@ -1028,7 +1028,7 @@ Останнє сповіщення Світовий час Стрес - Період + Відстеження циклу Дихання PineTime (Прошивка JF) TLW64 @@ -1115,7 +1115,7 @@ Секундомер Тихий режим Alexa - Зробити фото + Віддалене керування камерою Вимкнути звук телефону Знайти телефон SpO2 @@ -1488,4 +1488,15 @@ Видалити нагадування Немає вільних комірок На пристрої немає вільних комірок для нагадувань (загальна кількість комірок: %1$s) + Вмикати дисплей під час підйому у режимі «Не турбувати» + Середня + Мінімальна + Максимальний пульс + кроків/хв + Середній темп + Максимальний темп + Мінімальний пульс + Максимальний крок + Мінімальний крок + Мінімальний темп \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 77e033568..18402638b 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -586,10 +586,10 @@ 天气(快捷方式) 状态 通知 - 活动 + 锻炼历史 天气 闹钟 - 计时器 + 倒计时 指南针 设置 支付宝 @@ -928,7 +928,7 @@ 启动后台服务失败 注意:检查版本信息时发生错误!您不应该继续下去,已知版本名“%s” 压力 - 周期 + 周期跟踪 呼吸 小米手环5 您即将在您的 小米手环5 上安装 %s 固件。 @@ -1122,7 +1122,7 @@ 秒表 勿扰 Alexa - 拍照 + 相机遥控 静音手机 查找手机 运动强度 @@ -1488,4 +1488,14 @@ 没有可用空位 该设备已经没有可用于提醒的可用的空位(总计空位:%1$s) 在请勿打扰期间在抬起时激活显示 + 平均 + 最小 + 最大步幅 + 平均节奏 + 最大节奏 + 最小节奏 + 最大心率 + 最小心率 + 最小步幅 + 步数/分钟 \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c34f97f57..54574ac5c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -936,12 +936,12 @@ Status Notifications - Activity + Workout History Weather Breathing - Period + Cycle Tracking Alarm - Timer + Countdown Compass Settings Alipay @@ -959,7 +959,7 @@ World Clock Find Phone Mute Phone - Take a photo + Camera Remote Alexa DND Stopwatch @@ -1317,7 +1317,7 @@ Power saving Disable display updates while off wrist Disable hands movement while off wrist -Sleep times + Sleep times Define sleep hours Specifies times when sleep is registered Enable vibrations @@ -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 @@ -1427,5 +1427,4 @@ Show circle on timeout: Enable on-device pairing confirmation On-device pairing confirmations can get annoying. Disabling them might lose you functionality. - - + \ No newline at end of file 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 @@ + + + +