1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-29 05:16:51 +01:00

Sony WH-1000XM3: Read information from device

This commit is contained in:
José Rebelo 2021-12-12 18:48:14 +00:00 committed by Gitea
parent 13e7c94e09
commit 33b5a6de38
44 changed files with 2357 additions and 825 deletions

View File

@ -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";

View File

@ -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);

View File

@ -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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones;
public enum AmbientSoundControl {
OFF,
NOISE_CANCELLING,
WIND_NOISE_REDUCTION,
AMBIENT_SOUND
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones;
import java.util.List;
public class EqualizerCustomBands {
private List<Integer> bands;
private int clearBass;
public EqualizerCustomBands(List<Integer> 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<Integer> getBands() {
return bands;
}
public int getClearBass() {
return clearBass;
}
}

View File

@ -41,7 +41,7 @@ public abstract class SonyHeadphonesCoordinator extends AbstractDeviceCoordinato
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new SonySettingsCustomizer();
return new SonyHeadphonesSettingsCustomizer();
}
@Override

View File

@ -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 <http://www.gnu.org/licenses/>. */
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);
}
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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);
}
}
}

View File

@ -14,7 +14,7 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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
};
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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));
}
}

View File

@ -14,7 +14,15 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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());
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<Integer> bands;
private int bass;
public EqualizerCustomBands(final List<Integer> 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<Integer> getBands() {
return bands;
}
public int getBass() {
return bass;
}
public String toString() {
return String.format("EqualizerCustomBands{clearBass=%d, bands=%s}", bass, bands);
}
public Map<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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());
}
}

View File

@ -14,7 +14,15 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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());
}
}

View File

@ -14,7 +14,15 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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());
}
}

View File

@ -14,25 +14,33 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<String, Object> toPreferences() {
return new HashMap<String, Object>() {{
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));
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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:
* <p>
* - 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
* <p>
* 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<Request> 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<Object> 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<Request> 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;
}
}

View File

@ -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;

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<Request> requests = new ArrayList<>();
public SonyHeadphonesEnqueueRequestEvent(final Request request) {
this.requests.add(request);
}
public SonyHeadphonesEnqueueRequestEvent(final Collection<Request> requests) {
this.requests.addAll(requests);
}
public List<Request> getRequests() {
return this.requests;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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:
* <p>
* - 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
* <p>
* 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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));
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<? extends GBDeviceEvent> handlePayload(final MessageType messageType, final byte[] payload);
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<Request> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<GBDeviceEventBatteryInfo> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<Integer> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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<? extends GBDeviceEvent> 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);
}
}

View File

@ -14,17 +14,32 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -14,13 +14,30 @@
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>. */
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;
}
}

View File

@ -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;
}

View File

@ -1401,7 +1401,7 @@
<string name="sony_equalizer_preset_manual">Manual</string>
<string name="sony_equalizer_preset_custom_1">Custom 1</string>
<string name="sony_equalizer_preset_custom_2">Custom 2</string>
<string name="pref_header_sony_equalizer_preset_manual">Manual Preset</string>
<string name="pref_header_sony_equalizer_bands">Bands</string>
<string name="pref_header_sony_equalizer_preset_custom_1">Custom Preset 1</string>
<string name="pref_header_sony_equalizer_preset_custom_2">Custom Preset 2</string>
<string name="sony_equalizer_band_400">400</string>
@ -1410,7 +1410,7 @@
<string name="sony_equalizer_band_6300">6.3k</string>
<string name="sony_equalizer_band_16000">16k</string>
<string name="sony_equalizer_clear_bass">Clear Bass</string>
<string name="sony_dsee_hx">DSEE HX</string>
<string name="sony_audio_upsampling">Audio Upsampling</string>
<string name="sony_touch_sensor">Touch sensor control</string>
<string name="sony_notification_voice_guide">Notifications &amp; Voice Guide</string>
<string name="sony_automatic_power_off">Automatic Power Off</string>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="off"
android:entries="@array/sony_automatic_power_off_names"
android:entryValues="@array/sony_automatic_power_off_values"
android:icon="@drawable/ic_power_settings_new"
android:key="pref_sony_automatic_power_off"
android:summary="%s"
android:title="@string/sony_automatic_power_off" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_other"
android:title="@string/pref_header_other" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_other"
android:title="@string/pref_header_system" />
</androidx.preference.PreferenceScreen>

View File

@ -13,6 +13,7 @@
android:summary="%s"
android:title="@string/sony_ambient_sound" />
<!-- [0, 19], which maps to [1, 20] on the device, as we can't configure the min on the current API level -->
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_volume_up"

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_extension"
android:key="pref_sony_audio_upsampling"
android:title="@string/sony_audio_upsampling" />
</androidx.preference.PreferenceScreen>

View File

@ -1,7 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
<PreferenceScreen
android:icon="@drawable/ic_graphic_eq"
android:key="pref_key_equalizer"
android:persistent="false"
android:title="@string/pref_header_equalizer">
<ListPreference
@ -13,151 +16,43 @@
android:summary="%s"
android:title="@string/sony_equalizer" />
<PreferenceScreen
android:icon="@drawable/ic_extension"
android:key="pref_sony_equalizer_preset_manual"
android:persistent="false"
android:title="@string/pref_header_sony_equalizer_preset_manual">
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_band_400"
android:max="20"
android:title="@string/sony_equalizer_band_400" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_band_1000"
android:max="20"
android:title="@string/sony_equalizer_band_1000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_band_2500"
android:max="20"
android:title="@string/sony_equalizer_band_2500" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_band_6300"
android:max="20"
android:title="@string/sony_equalizer_band_6300" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_band_16000"
android:max="20"
android:title="@string/sony_equalizer_band_16000" />
<PreferenceCategory
android:key="pref_key_equalizer"
android:title="@string/pref_header_sony_equalizer_preset_manual">
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_speaker"
android:key="pref_sony_equalizer_bass"
android:max="20"
android:title="@string/sony_equalizer_clear_bass" />
</PreferenceScreen>
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_manual_band_400"
android:max="20"
android:title="@string/sony_equalizer_band_400" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_manual_band_1000"
android:max="20"
android:title="@string/sony_equalizer_band_1000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_manual_band_2500"
android:max="20"
android:title="@string/sony_equalizer_band_2500" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_manual_band_6300"
android:max="20"
android:title="@string/sony_equalizer_band_6300" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_manual_band_16000"
android:max="20"
android:title="@string/sony_equalizer_band_16000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_speaker"
android:key="pref_sony_equalizer_manual_clear_bass"
android:max="20"
android:title="@string/sony_equalizer_clear_bass" />
</PreferenceCategory>
</PreferenceScreen>
<PreferenceScreen
android:icon="@drawable/ic_extension"
android:key="pref_sony_equalizer_preset_custom_1"
android:persistent="false"
android:title="@string/pref_header_sony_equalizer_preset_custom_1">
<PreferenceCategory
android:key="pref_key_equalizer"
android:title="@string/pref_header_sony_equalizer_preset_custom_1">
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_1_band_400"
android:max="20"
android:title="@string/sony_equalizer_band_400" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_1_band_1000"
android:max="20"
android:title="@string/sony_equalizer_band_1000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_1_band_2500"
android:max="20"
android:title="@string/sony_equalizer_band_2500" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_1_band_6300"
android:max="20"
android:title="@string/sony_equalizer_band_6300" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_1_band_16000"
android:max="20"
android:title="@string/sony_equalizer_band_16000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_speaker"
android:key="pref_sony_equalizer_custom_1_clear_bass"
android:max="20"
android:title="@string/sony_equalizer_clear_bass" />
</PreferenceCategory>
</PreferenceScreen>
<PreferenceScreen
android:icon="@drawable/ic_extension"
android:key="pref_sony_equalizer_preset_custom_2"
android:persistent="false"
android:title="@string/pref_header_sony_equalizer_preset_custom_2">
<PreferenceCategory
android:key="pref_key_equalizer"
android:title="@string/pref_header_sony_equalizer_preset_custom_2">
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_2_band_400"
android:max="20"
android:title="@string/sony_equalizer_band_400" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_2_band_1000"
android:max="20"
android:title="@string/sony_equalizer_band_1000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_2_band_2500"
android:max="20"
android:title="@string/sony_equalizer_band_2500" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_2_band_6300"
android:max="20"
android:title="@string/sony_equalizer_band_6300" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_sony_equalizer_custom_2_band_16000"
android:max="20"
android:title="@string/sony_equalizer_band_16000" />
<SeekBarPreference
android:defaultValue="10"
android:icon="@drawable/ic_speaker"
android:key="pref_sony_equalizer_custom_2_clear_bass"
android:max="20"
android:title="@string/sony_equalizer_clear_bass" />
</PreferenceCategory>
</PreferenceScreen>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_notifications"
android:key="pref_sony_notification_voice_guide"
android:title="@string/sony_notification_voice_guide" />
</androidx.preference.PreferenceScreen>

View File

@ -1,31 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_other"
android:title="@string/pref_header_other">
<ListPreference
android:defaultValue="off"
android:entries="@array/sony_sound_position_names"
android:entryValues="@array/sony_sound_position_values"
android:icon="@drawable/ic_switch_left"
android:key="pref_sony_sound_position"
android:summary="%s"
android:title="@string/sony_sound_position" />
<ListPreference
android:defaultValue="off"
android:entries="@array/sony_surround_mode_names"
android:entryValues="@array/sony_surround_mode_values"
android:icon="@drawable/ic_surround"
android:key="pref_sony_surround_mode"
android:summary="%s"
android:title="@string/sony_surround_mode" />
<SwitchPreference
android:defaultValue="false"
android:icon="@drawable/ic_extension"
android:key="pref_sony_dsee_hx"
android:title="@string/sony_dsee_hx" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="off"
android:entries="@array/sony_sound_position_names"
android:entryValues="@array/sony_sound_position_values"
android:icon="@drawable/ic_switch_left"
android:key="pref_sony_sound_position"
android:summary="%s"
android:title="@string/sony_sound_position" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<ListPreference
android:defaultValue="off"
android:entries="@array/sony_surround_mode_names"
android:entryValues="@array/sony_surround_mode_values"
android:icon="@drawable/ic_surround"
android:key="pref_sony_surround_mode"
android:summary="%s"
android:title="@string/sony_surround_mode" />
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_touch"
android:key="pref_sony_touch_sensor"
android:title="@string/sony_touch_sensor" />
</androidx.preference.PreferenceScreen>