From 8fdfbfa97c639dc7ad49439c546ce4f265f70e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sat, 22 Oct 2022 13:14:50 +0100 Subject: [PATCH] Sony WF-1000XM4: Initial support --- README.md | 5 +- .../SonyHeadphonesCapabilities.java | 3 +- .../headphones/SonyHeadphonesCoordinator.java | 4 +- .../SonyWF1000XM3Coordinator.java | 2 +- .../SonyWF1000XM4Coordinator.java | 71 ++ .../coordinators/SonyWFSP800NCoordinator.java | 2 +- .../SonyWH1000XM2Coordinator.java | 2 +- .../SonyWH1000XM3Coordinator.java | 2 +- .../SonyWH1000XM4Coordinator.java | 2 +- .../gadgetbridge/model/DeviceType.java | 1 + .../service/DeviceSupportFactory.java | 2 + .../headphones/SonyHeadphonesIoThread.java | 31 +- .../headphones/SonyHeadphonesProtocol.java | 6 +- .../impl/AbstractSonyProtocolImpl.java | 6 + .../protocol/impl/v1/SonyProtocolImplV1.java | 3 +- .../protocol/impl/v1/params/BatteryType.java | 26 +- .../protocol/impl/v2/PayloadTypeV2.java | 62 ++ .../protocol/impl/v2/SonyProtocolImplV2.java | 635 ++++++++++++++++++ .../gadgetbridge/util/DeviceHelper.java | 2 + app/src/main/res/values/strings.xml | 1 + ...vicesettings_sony_headphones_equalizer.xml | 61 +- ...headphones_equalizer_with_custom_bands.xml | 58 ++ .../protocol/impl/SonyTestUtils.java | 54 ++ .../impl/v1/SonyProtocolImplV1Test.java | 300 +++++++++ .../impl/v2/SonyProtocolImplV2Test.java | 371 ++++++++++ 25 files changed, 1617 insertions(+), 95 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM4Coordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/PayloadTypeV2.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2.java create mode 100644 app/src/main/res/xml/devicesettings_sony_headphones_equalizer_with_custom_bands.xml create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/SonyTestUtils.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1Test.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2Test.java diff --git a/README.md b/README.md index 10e04b1eb..333ed2cc2 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,10 @@ vendor's servers. - PineTime (InfiniTime Firmware) - Roidmi, Roidmi 3, Mojietu 3 (Bluetooth FM Transmitters) - [SMA](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/SMA) Q2 (SMA-Q2-OSS Firmware) -- [Sony WH-1000XM2, WH-1000XM3, WH-1000XM4, WF-SP800N, WF-1000XM3](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Sony-Headphones) +- [Sony Headphones](https://codeberg.org/Freeyourgadget/Gadgetbridge/wiki/Sony-Headphones) + - WH-1000XM2, WH-1000XM3, WH-1000XM4 + - WF-SP800N + - WF-1000XM3, WF-1000XM4 - Teclast H10, H30 - TLW64 - Vibratissimo (Experimental) diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCapabilities.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCapabilities.java index 8fbf9b72e..49fd4efb7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCapabilities.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCapabilities.java @@ -31,7 +31,8 @@ public enum SonyHeadphonesCapabilities { AutomaticPowerOffWhenTakenOff, AutomaticPowerOffByTime, TouchSensorSingle, - Equalizer, + EqualizerSimple, + EqualizerWithCustomBands, SoundPosition, SurroundMode, PauseWhenTakenOff diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java index 0b9deb28e..9658b2058 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/SonyHeadphonesCoordinator.java @@ -43,7 +43,6 @@ import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; -import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; public abstract class SonyHeadphonesCoordinator extends AbstractBLClassicDeviceCoordinator { @Override @@ -186,7 +185,8 @@ public abstract class SonyHeadphonesCoordinator extends AbstractBLClassicDeviceC addSettingsUnderHeader(settings, R.xml.devicesettings_header_other, new LinkedHashMap() {{ put(SonyHeadphonesCapabilities.AudioSettingsOnlyOnSbcCodec, R.xml.devicesettings_sony_warning_wh1000xm3); - put(SonyHeadphonesCapabilities.Equalizer, R.xml.devicesettings_sony_headphones_equalizer); + put(SonyHeadphonesCapabilities.EqualizerSimple, R.xml.devicesettings_sony_headphones_equalizer); + put(SonyHeadphonesCapabilities.EqualizerWithCustomBands, R.xml.devicesettings_sony_headphones_equalizer_with_custom_bands); put(SonyHeadphonesCapabilities.SoundPosition, R.xml.devicesettings_sony_headphones_sound_position); put(SonyHeadphonesCapabilities.SurroundMode, R.xml.devicesettings_sony_headphones_surround_mode); put(SonyHeadphonesCapabilities.AudioUpsampling, R.xml.devicesettings_sony_headphones_audio_upsampling); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM3Coordinator.java index 00e0190ea..cf30de5bd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM3Coordinator.java @@ -62,7 +62,7 @@ public class SonyWF1000XM3Coordinator extends SonyHeadphonesCoordinator { SonyHeadphonesCapabilities.PowerOffFromPhone, SonyHeadphonesCapabilities.AmbientSoundControl, SonyHeadphonesCapabilities.WindNoiseReduction, - SonyHeadphonesCapabilities.Equalizer, + SonyHeadphonesCapabilities.EqualizerWithCustomBands, SonyHeadphonesCapabilities.AudioUpsampling, SonyHeadphonesCapabilities.ButtonModesLeftRight, SonyHeadphonesCapabilities.PauseWhenTakenOff, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM4Coordinator.java new file mode 100644 index 000000000..b7c73e286 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWF1000XM4Coordinator.java @@ -0,0 +1,71 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators; + +import androidx.annotation.NonNull; + +import java.util.Arrays; +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCapabilities; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.model.DeviceType; + +public class SonyWF1000XM4Coordinator extends SonyHeadphonesCoordinator { + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + if (candidate.getName().contains("WF-1000XM4")) { + return DeviceType.SONY_WF_1000XM4; + } + + return DeviceType.UNKNOWN; + } + + @Override + public DeviceType getDeviceType() { + return DeviceType.SONY_WF_1000XM4; + } + + @Override + public BatteryConfig[] getBatteryConfig() { + final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_tws_case, R.string.battery_case); + final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_galaxy_buds_l, R.string.left_earbud); + final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_galaxy_buds_r, R.string.right_earbud); + + return new BatteryConfig[]{battery1, battery2, battery3}; + } + + @Override + public List getCapabilities() { + return Arrays.asList( + SonyHeadphonesCapabilities.BatteryDual, + SonyHeadphonesCapabilities.BatteryCase, + SonyHeadphonesCapabilities.AmbientSoundControl, + SonyHeadphonesCapabilities.WindNoiseReduction, + SonyHeadphonesCapabilities.EqualizerSimple, + SonyHeadphonesCapabilities.AudioUpsampling, + SonyHeadphonesCapabilities.ButtonModesLeftRight, + SonyHeadphonesCapabilities.PauseWhenTakenOff, + SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff + ); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWFSP800NCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWFSP800NCoordinator.java index 6dd9d0124..55afcfd82 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWFSP800NCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWFSP800NCoordinator.java @@ -60,7 +60,7 @@ public class SonyWFSP800NCoordinator extends SonyHeadphonesCoordinator { SonyHeadphonesCapabilities.BatteryCase, SonyHeadphonesCapabilities.PowerOffFromPhone, SonyHeadphonesCapabilities.AmbientSoundControl, - SonyHeadphonesCapabilities.Equalizer, + SonyHeadphonesCapabilities.EqualizerWithCustomBands, SonyHeadphonesCapabilities.ButtonModesLeftRight, SonyHeadphonesCapabilities.PauseWhenTakenOff, SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM2Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM2Coordinator.java index 6f754fd0f..61392e593 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM2Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM2Coordinator.java @@ -50,7 +50,7 @@ public class SonyWH1000XM2Coordinator extends SonyHeadphonesCoordinator { SonyHeadphonesCapabilities.WindNoiseReduction, SonyHeadphonesCapabilities.AncOptimizer, SonyHeadphonesCapabilities.AudioSettingsOnlyOnSbcCodec, - SonyHeadphonesCapabilities.Equalizer, + SonyHeadphonesCapabilities.EqualizerWithCustomBands, SonyHeadphonesCapabilities.SoundPosition, SonyHeadphonesCapabilities.SurroundMode, SonyHeadphonesCapabilities.AudioUpsampling diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java index 0318b40db..479c282b2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM3Coordinator.java @@ -50,7 +50,7 @@ public class SonyWH1000XM3Coordinator extends SonyHeadphonesCoordinator { SonyHeadphonesCapabilities.WindNoiseReduction, SonyHeadphonesCapabilities.AncOptimizer, SonyHeadphonesCapabilities.AudioSettingsOnlyOnSbcCodec, - SonyHeadphonesCapabilities.Equalizer, + SonyHeadphonesCapabilities.EqualizerWithCustomBands, SonyHeadphonesCapabilities.SoundPosition, SonyHeadphonesCapabilities.SurroundMode, SonyHeadphonesCapabilities.AudioUpsampling, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM4Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM4Coordinator.java index 47e846c6b..58b63dc54 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM4Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/sony/headphones/coordinators/SonyWH1000XM4Coordinator.java @@ -52,7 +52,7 @@ public class SonyWH1000XM4Coordinator extends SonyHeadphonesCoordinator { SonyHeadphonesCapabilities.AmbientSoundControl, SonyHeadphonesCapabilities.WindNoiseReduction, SonyHeadphonesCapabilities.AncOptimizer, - SonyHeadphonesCapabilities.Equalizer, + SonyHeadphonesCapabilities.EqualizerWithCustomBands, SonyHeadphonesCapabilities.AudioUpsampling, SonyHeadphonesCapabilities.TouchSensorSingle, SonyHeadphonesCapabilities.PauseWhenTakenOff, diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index f33db8020..ed0e3581f 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -114,6 +114,7 @@ public enum DeviceType { SONY_WH_1000XM4(432, R.drawable.ic_device_sony_overhead, R.drawable.ic_device_sony_overhead_disabled, R.string.devicetype_sony_wh_1000xm4), SONY_WF_1000XM3(433, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_wf_1000xm3), SONY_WH_1000XM2(434, R.drawable.ic_device_sony_overhead, R.drawable.ic_device_sony_overhead_disabled, R.string.devicetype_sony_wh_1000xm2), + SONY_WF_1000XM4(435, R.drawable.ic_device_galaxy_buds, R.drawable.ic_device_galaxy_buds_disabled, R.string.devicetype_sony_wf_1000xm4), BOSE_QC35(440, R.drawable.ic_device_headphones, R.drawable.ic_device_headphones_disabled, R.string.devicetype_bose_qc35), VESC_NRF(500, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc), VESC_HM10(501, R.drawable.ic_device_vesc, R.drawable.ic_device_vesc_disabled, R.string.devicetype_vesc), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 3dd20763b..d1281e853 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -327,6 +327,8 @@ public class DeviceSupportFactory { return new ServiceDeviceSupport(new SonyHeadphonesSupport()); case SONY_WH_1000XM2: return new ServiceDeviceSupport(new SonyHeadphonesSupport()); + case SONY_WF_1000XM4: + return new ServiceDeviceSupport(new SonyHeadphonesSupport()); case VESC_NRF: case VESC_HM10: return new ServiceDeviceSupport(new VescDeviceSupport(device.getType())); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java index 88d8b6bcf..994358c8b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesIoThread.java @@ -41,6 +41,9 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { private final SonyHeadphonesProtocol mProtocol; + private final UUID btrfcommUuidV1 = UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA"); + private final UUID btrfcommUuidV2 = UUID.fromString("956C7B26-D49A-4BA8-B03F-B17D393CB6E2"); + // Track whether we got the first init reply private final Handler handler = new Handler(); private int initRetries = 0; @@ -67,7 +70,11 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { } }; - public SonyHeadphonesIoThread(GBDevice gbDevice, Context context, SonyHeadphonesProtocol protocol, SonyHeadphonesSupport support, BluetoothAdapter btAdapter) { + public SonyHeadphonesIoThread(final GBDevice gbDevice, + final Context context, + final SonyHeadphonesProtocol protocol, + final SonyHeadphonesSupport support, + final BluetoothAdapter btAdapter) { super(gbDevice, context, protocol, support, btAdapter); mProtocol = protocol; } @@ -80,7 +87,7 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { } @Override - public synchronized void write(byte[] bytes) { + public synchronized void write(final byte[] bytes) { // Log the human-readable message, for debugging LOG.info("Writing {}", Message.fromBytes(bytes)); @@ -88,7 +95,7 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { } @Override - protected byte[] parseIncoming(InputStream inputStream) throws IOException { + protected byte[] parseIncoming(final InputStream inputStream) throws IOException { final ByteArrayOutputStream msgStream = new ByteArrayOutputStream(); final byte[] incoming = new byte[1]; @@ -109,8 +116,22 @@ public class SonyHeadphonesIoThread extends BtClassicIoThread { @NonNull @Override - protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) { - return UUID.fromString("96CC203E-5068-46ad-B32D-E316F5E069BA"); + protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) { + boolean hasV2 = false; + for (final ParcelUuid uuid : uuids) { + if (uuid.getUuid().equals(btrfcommUuidV1)) { + // Prioritize V1 + return btrfcommUuidV1; + } else if (uuid.getUuid().equals(btrfcommUuidV2)) { + hasV2 = true; + } + } + + if (hasV2) { + return btrfcommUuidV2; + } + + return super.getUuidToConnect(uuids); } private void scheduleInitRetry() { diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java index 8723c3b69..6b2e4d81e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/SonyHeadphonesProtocol.java @@ -48,6 +48,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.prot 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.devices.sony.headphones.protocol.impl.v2.SonyProtocolImplV2; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; public class SonyHeadphonesProtocol extends GBDeviceProtocol { @@ -101,9 +102,8 @@ public class SonyHeadphonesProtocol extends GBDeviceProtocol { // 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; + } else if (message.getPayload().length == 8) { + protocolImpl = new SonyProtocolImplV2(getDevice()); } else { LOG.error("Unexpected init response payload length: {}", message.getPayload().length); return null; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java index a1be9a21e..4d0ec48bc 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/AbstractSonyProtocolImpl.java @@ -19,6 +19,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.pro import java.util.List; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator; 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; @@ -34,6 +35,7 @@ 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; +import nodomain.freeyourgadget.gadgetbridge.util.DeviceHelper; public abstract class AbstractSonyProtocolImpl { private final GBDevice device; @@ -46,6 +48,10 @@ public abstract class AbstractSonyProtocolImpl { return this.device; } + protected SonyHeadphonesCoordinator getCoordinator() { + return (SonyHeadphonesCoordinator) DeviceHelper.getInstance().getCoordinator(getDevice()); + } + public abstract Request getAmbientSoundControl(); public abstract Request setAmbientSoundControl(final AmbientSoundControl config); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java index 51a45ba33..8edc658fd 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1.java @@ -512,7 +512,8 @@ public class SonyProtocolImplV1 extends AbstractSonyProtocolImpl { put(SonyHeadphonesCapabilities.AutomaticPowerOffWhenTakenOff, getAutomaticPowerOff()); put(SonyHeadphonesCapabilities.AutomaticPowerOffByTime, getAutomaticPowerOff()); put(SonyHeadphonesCapabilities.TouchSensorSingle, getTouchSensor()); - put(SonyHeadphonesCapabilities.Equalizer, getEqualizer()); + put(SonyHeadphonesCapabilities.EqualizerSimple, getEqualizer()); + put(SonyHeadphonesCapabilities.EqualizerWithCustomBands, getEqualizer()); put(SonyHeadphonesCapabilities.SoundPosition, getSoundPosition()); put(SonyHeadphonesCapabilities.SurroundMode, getSurroundMode()); put(SonyHeadphonesCapabilities.PauseWhenTakenOff, getPauseWhenTakenOff()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java index 30e04235a..99b8c3183 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/params/BatteryType.java @@ -17,27 +17,7 @@ package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.params; public enum BatteryType { - SINGLE(0x00), - DUAL(0x01), - CASE(0x02); - - 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; - } + SINGLE, + DUAL, + CASE, } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/PayloadTypeV2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/PayloadTypeV2.java new file mode 100644 index 000000000..cd00d8570 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/PayloadTypeV2.java @@ -0,0 +1,62 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2; + +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; + +public enum PayloadTypeV2 { + AUDIO_CODEC_REQUEST(MessageType.COMMAND_1, 0x13), // TODO confirm + AUDIO_CODEC_REPLY(MessageType.COMMAND_1, 0x14), // TODO confirm + AUDIO_CODEC_NOTIFY(MessageType.COMMAND_1, 0x15), + + BATTERY_LEVEL_REQUEST(MessageType.COMMAND_1, 0x22), + BATTERY_LEVEL_REPLY(MessageType.COMMAND_1, 0x23), + BATTERY_LEVEL_NOTIFY(MessageType.COMMAND_1, 0x24), // TODO confirm + + AUTOMATIC_POWER_OFF_GET(MessageType.COMMAND_1, 0x26), + AUTOMATIC_POWER_OFF_RET(MessageType.COMMAND_1, 0x27), + AUTOMATIC_POWER_OFF_SET(MessageType.COMMAND_1, 0x28), + AUTOMATIC_POWER_OFF_NOTIFY(MessageType.COMMAND_1, 0x29), + + UNKNOWN(MessageType.UNKNOWN, 0xff); + + private final MessageType messageType; + private final byte code; + + PayloadTypeV2(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 PayloadTypeV2 fromCode(final MessageType messageType, final byte code) { + for (final PayloadTypeV2 payloadType : values()) { + if (messageType.equals(payloadType.messageType) && payloadType.code == code) { + return payloadType; + } + } + + return PayloadTypeV2.UNKNOWN; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2.java new file mode 100644 index 000000000..5789dc45d --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2.java @@ -0,0 +1,635 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2; + +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 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.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.ButtonModes; +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.PauseWhenTakenOff; +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.service.devices.sony.headphones.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.PayloadTypeV1; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1.SonyProtocolImplV1; +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.util.GB; + +public class SonyProtocolImplV2 extends SonyProtocolImplV1 { + private static final Logger LOG = LoggerFactory.getLogger(SonyProtocolImplV2.class); + + public SonyProtocolImplV2(final GBDevice device) { + super(device); + } + + @Override + public Request getAmbientSoundControl() { + return new Request( + PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getMessageType(), + new byte[]{ + PayloadTypeV1.AMBIENT_SOUND_CONTROL_GET.getCode(), + (byte) 0x15 + } + ); + } + + @Override + public Request setAmbientSoundControl(final AmbientSoundControl ambientSoundControl) { + final ByteBuffer buf = ByteBuffer.allocate(8); + + buf.put(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getCode()); + buf.put((byte) 0x15); + buf.put((byte) 0x01); // 0x00 while dragging the slider? + + if (AmbientSoundControl.Mode.OFF.equals(ambientSoundControl.getMode())) { + buf.put((byte) 0x00); + } else { + buf.put((byte) 0x01); + } + + if (AmbientSoundControl.Mode.AMBIENT_SOUND.equals(ambientSoundControl.getMode())) { + buf.put((byte) 0x01); + } else { + buf.put((byte) 0x00); + } + + if (AmbientSoundControl.Mode.WIND_NOISE_REDUCTION.equals(ambientSoundControl.getMode())) { + buf.put((byte) 0x03); + } else { + buf.put((byte) 0x02); + } + + buf.put((byte) (ambientSoundControl.isFocusOnVoice() ? 0x01 : 0x00)); + buf.put((byte) (ambientSoundControl.getAmbientSound())); + + return new Request(PayloadTypeV1.AMBIENT_SOUND_CONTROL_SET.getMessageType(), buf.array()); + } + + @Override + public Request getNoiseCancellingOptimizerState() { + LOG.warn("Noise cancelling optimizer not implemented for V2"); + return null; + } + + @Override + public Request getAudioCodec() { + return new Request( + PayloadTypeV2.AUDIO_CODEC_REQUEST.getMessageType(), + new byte[]{ + PayloadTypeV2.AUDIO_CODEC_REQUEST.getCode(), + (byte) 0x00 + } + ); + } + + @Override + public Request getBattery(final BatteryType batteryType) { + return new Request( + PayloadTypeV2.BATTERY_LEVEL_REQUEST.getMessageType(), + new byte[]{ + PayloadTypeV2.BATTERY_LEVEL_REQUEST.getCode(), + encodeBatteryType(batteryType) + } + ); + } + + @Override + public Request getFirmwareVersion() { + return super.getFirmwareVersion(); + } + + @Override + public Request getAudioUpsampling() { + return new Request( + PayloadTypeV1.AUDIO_UPSAMPLING_GET.getMessageType(), + new byte[]{ + PayloadTypeV1.AUDIO_UPSAMPLING_GET.getCode(), + (byte) 0x01 + } + ); + } + + @Override + public Request setAudioUpsampling(final AudioUpsampling config) { + return new Request( + PayloadTypeV1.AUDIO_UPSAMPLING_SET.getMessageType(), + new byte[]{ + PayloadTypeV1.AUDIO_UPSAMPLING_SET.getCode(), + (byte) 0x01, + (byte) (config.isEnabled() ? 0x01 : 0x00) + } + ); + } + + @Override + public Request getAutomaticPowerOff() { + return new Request( + PayloadTypeV2.AUTOMATIC_POWER_OFF_GET.getMessageType(), + new byte[]{ + PayloadTypeV2.AUTOMATIC_POWER_OFF_GET.getCode(), + (byte) 0x05 + } + ); + } + + @Override + public Request setAutomaticPowerOff(final AutomaticPowerOff config) { + return new Request( + PayloadTypeV2.AUTOMATIC_POWER_OFF_SET.getMessageType(), + new byte[]{ + PayloadTypeV2.AUTOMATIC_POWER_OFF_SET.getCode(), + (byte) 0x05, + config.getCode()[0], + config.getCode()[1] + } + ); + } + + @Override + public Request getButtonModes() { + return new Request( + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getMessageType(), + new byte[]{ + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_GET.getCode(), + (byte) 0x03 + } + ); + } + + @Override + public Request setButtonModes(final ButtonModes config) { + return new Request( + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(), + new byte[]{ + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(), + (byte) 0x03, + (byte) 0x02, + config.getModeLeft().getCode(), + config.getModeRight().getCode() + } + ); + } + + @Override + public Request getPauseWhenTakenOff() { + return new Request( + PayloadTypeV2.AUTOMATIC_POWER_OFF_GET.getMessageType(), + new byte[]{ + PayloadTypeV2.AUTOMATIC_POWER_OFF_GET.getCode(), + (byte) 0x01 + } + ); + } + + @Override + public Request setPauseWhenTakenOff(final PauseWhenTakenOff config) { + return new Request( + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getMessageType(), + new byte[]{ + PayloadTypeV1.AUTOMATIC_POWER_OFF_BUTTON_MODE_SET.getCode(), + (byte) 0x01, + (byte) (config.isEnabled() ? 0x00 : 0x01) // this is reversed on V2...? + } + ); + } + + @Override + public Request getEqualizer() { + return new Request( + PayloadTypeV1.EQUALIZER_GET.getMessageType(), + new byte[]{ + PayloadTypeV1.EQUALIZER_GET.getCode(), + (byte) 0x00 + } + ); + } + + @Override + public Request setEqualizerPreset(final EqualizerPreset config) { + return new Request( + PayloadTypeV1.EQUALIZER_SET.getMessageType(), + new byte[]{ + PayloadTypeV1.EQUALIZER_SET.getCode(), + (byte) 0x00, + config.getCode(), + (byte) 0x00 + } + ); + } + + @Override + public Request setEqualizerCustomBands(final EqualizerCustomBands config) { + LOG.warn("Equalizer custom bands not implemented for V2"); + return null; + } + + @Override + public Request getSoundPosition() { + LOG.warn("Sound position not implemented for V2"); + return null; + } + + @Override + public Request setSoundPosition(final SoundPosition config) { + LOG.warn("Sound position not implemented for V2"); + return null; + } + + @Override + public Request getSurroundMode() { + LOG.warn("Surround mode not implemented for V2"); + return null; + } + + @Override + public Request setSurroundMode(final SurroundMode config) { + LOG.warn("Surround mode not implemented for V2"); + return null; + } + + @Override + public Request getTouchSensor() { + LOG.warn("Touch sensor not implemented for V2"); + return null; + } + + @Override + public Request setTouchSensor(final TouchSensor config) { + LOG.warn("Touch sensor not implemented for V2"); + return null; + } + + @Override + public Request getVoiceNotifications() { + LOG.warn("Voice notifications not implemented for V2"); + return null; + } + + @Override + public Request setVoiceNotifications(final VoiceNotifications config) { + LOG.warn("Voice notifications not implemented for V2"); + return null; + } + + @Override + public Request startNoiseCancellingOptimizer(final boolean start) { + LOG.warn("Noise cancelling optimizer not implemented for V2"); + return null; + } + + @Override + public Request powerOff() { + LOG.warn("Power off not implemented for V2"); + return null; + } + + @Override + public List handlePayload(final MessageType messageType, final byte[] payload) { + final PayloadTypeV2 payloadType = PayloadTypeV2.fromCode(messageType, payload[0]); + + switch (payloadType) { + case AUDIO_CODEC_NOTIFY: + return handleAudioCodec(payload); + case BATTERY_LEVEL_NOTIFY: + case BATTERY_LEVEL_REPLY: + return handleBattery(payload); + case AUTOMATIC_POWER_OFF_RET: + case AUTOMATIC_POWER_OFF_NOTIFY: + return handleAutomaticPowerOff(payload); + } + + return super.handlePayload(messageType, payload); + } + + @Override + public List handleInitResponse(final byte[] payload) { + return super.handleInitResponse(payload); + } + + @Override + public List handleAmbientSoundControl(final byte[] payload) { + if (payload.length != 8) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x15) { + LOG.warn("Not ambient sound control, ignoring {}", payload[1]); + return Collections.emptyList(); + } + + AmbientSoundControl.Mode mode = null; + + if (payload[3] == (byte) 0x00) { + mode = AmbientSoundControl.Mode.OFF; + } else if (payload[3] == (byte) 0x01) { + // Enabled, determine mode + + if (payload[5] == 0x03 || payload[5] == 0x05) { + mode = AmbientSoundControl.Mode.WIND_NOISE_REDUCTION; + } else if (payload[5] == 0x02) { + if (payload[4] == (byte) 0x00) { + mode = AmbientSoundControl.Mode.NOISE_CANCELLING; + } else if (payload[4] == (byte) 0x01) { + mode = AmbientSoundControl.Mode.AMBIENT_SOUND; + } + } + } + + if (mode == null) { + LOG.warn("Unable to determine ambient sound control mode from {}", GB.hexdump(payload)); + return Collections.emptyList(); + } + + final Boolean focusOnVoice = booleanFromByte(payload[6]); + if (focusOnVoice == null) { + 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); + } + + @Override + public List handleNoiseCancellingOptimizerStatus(final byte[] payload) { + LOG.warn("Touch sensor not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleNoiseCancellingOptimizerState(final byte[] payload) { + LOG.warn("Touch sensor not implemented for V2"); + return Collections.emptyList(); + } + + + @Override + public List handleAudioUpsampling(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x01) { + LOG.warn("Not audio upsampling, ignoring {}", payload[1]); + return Collections.emptyList(); + } + + final Boolean enabled = booleanFromByte(payload[2]); + if (enabled == null) { + LOG.warn("Unknown audio upsampling code {}", String.format("%02x", payload[2])); + return Collections.emptyList(); + } + + LOG.debug("Audio Upsampling: {}", enabled); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new AudioUpsampling(enabled).toPreferences()); + + return Collections.singletonList(event); + } + + @Override + public List handleAutomaticPowerOff(final byte[] payload) { + if (payload.length != 4) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x05) { + LOG.warn("Not automatic power off config, ignoring"); + return Collections.emptyList(); + } + + final AutomaticPowerOff mode = AutomaticPowerOff.fromCode(payload[2], payload[3]); + 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); + } + + @Override + public List handleButtonModes(final byte[] payload) { + if (payload.length != 5) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x03) { + LOG.warn("Not button mode config, ignoring"); + return Collections.emptyList(); + } + + final ButtonModes.Mode modeLeft = ButtonModes.Mode.fromCode(payload[3]); + final ButtonModes.Mode modeRight = ButtonModes.Mode.fromCode(payload[4]); + + if (modeLeft == null || modeRight == null) { + LOG.warn("Unknown button mode codes {}", String.format("%02x %02x", payload[3], payload[4])); + return Collections.emptyList(); + } + + LOG.debug("Button Modes: L: {}, R: {}", modeLeft, modeRight); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new ButtonModes(modeLeft, modeRight).toPreferences()); + + return Collections.singletonList(event); + } + + @Override + public List handlePauseWhenTakenOff(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x01) { + LOG.warn("Not pause when taken off, ignoring"); + return Collections.emptyList(); + } + + final Boolean disabled = booleanFromByte(payload[2]); + if (disabled == null) { + LOG.warn("Unknown pause when taken off code {}", String.format("%02x", payload[2])); + return Collections.emptyList(); + } + + LOG.debug("Pause when taken off: {}", !disabled); + + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences() + .withPreferences(new PauseWhenTakenOff(!disabled).toPreferences()); + + return Collections.singletonList(event); + } + + @Override + public List handleBattery(final byte[] payload) { + return super.handleBattery(payload); + } + + @Override + public List handleAudioCodec(final byte[] payload) { + if (payload.length != 3) { + LOG.warn("Unexpected payload length {}", payload.length); + return Collections.emptyList(); + } + + if (payload[1] != 0x03) { + LOG.warn("Not audio codec, ignoring"); + 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); + } + + @Override + public List handleEqualizer(final byte[] payload) { + return super.handleEqualizer(payload); + } + + @Override + public List handleFirmwareVersion(final byte[] payload) { + return super.handleFirmwareVersion(payload); + } + + @Override + public List handleJson(final byte[] payload) { + LOG.warn("JSON not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleAutomaticPowerOffButtonMode(final byte[] payload) { + switch (payload[1]) { + case 0x01: + return handlePauseWhenTakenOff(payload); + case 0x03: + return handleButtonModes(payload); + } + + return Collections.emptyList(); + } + + @Override + public List handleVirtualSound(final byte[] payload) { + LOG.warn("Virtual sound not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleSoundPosition(final byte[] payload) { + LOG.warn("Sound position not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleSurroundMode(final byte[] payload) { + LOG.warn("Surround mode not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleTouchSensor(final byte[] payload) { + LOG.warn("Touch sensor not implemented for V2"); + return Collections.emptyList(); + } + + @Override + public List handleVoiceNotifications(final byte[] payload) { + LOG.warn("Voice notifications not implemented for V2"); + return Collections.emptyList(); + } + + @Override + protected BatteryType decodeBatteryType(final byte b) { + switch (b) { + case 0x09: + return BatteryType.DUAL; + case 0x0a: + return BatteryType.CASE; + } + + return null; + } + + @Override + protected byte encodeBatteryType(final BatteryType batteryType) { + switch (batteryType) { + case DUAL: + return 0x09; + case CASE: + case SINGLE: // TODO: This is not the code for single, but we need to encode something + return 0x0a; + } + + throw new IllegalArgumentException("Unknown battery type " + batteryType); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index 3e912f7fe..7681bf445 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -129,6 +129,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.watch9.Watch9DeviceCoordinat import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWFSP800NCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM3Coordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Device; @@ -341,6 +342,7 @@ public class DeviceHelper { result.add(new SonyWFSP800NCoordinator()); result.add(new SonyWF1000XM3Coordinator()); result.add(new SonyWH1000XM2Coordinator()); + result.add(new SonyWF1000XM4Coordinator()); result.add(new QC35Coordinator()); result.add(new BinarySensorCoordinator()); result.add(new FlipperZeroCoordinator()); diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index aa345773b..d443550ea 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1149,6 +1149,7 @@ Sony WH-1000XM4 Sony WF-SP800N Sony WF-1000XM3 + Sony WF-1000XM4 Binary sensor Choose export location General diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml index f223aeb96..bf9e6b8da 100644 --- a/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml +++ b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer.xml @@ -1,58 +1,11 @@ - - - - - - - - - - - - - - + android:key="pref_sony_equalizer_mode" + android:summary="%s" + android:title="@string/sony_equalizer" /> diff --git a/app/src/main/res/xml/devicesettings_sony_headphones_equalizer_with_custom_bands.xml b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer_with_custom_bands.xml new file mode 100644 index 000000000..f223aeb96 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_sony_headphones_equalizer_with_custom_bands.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/SonyTestUtils.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/SonyTestUtils.java new file mode 100644 index 000000000..30c82f615 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/SonyTestUtils.java @@ -0,0 +1,54 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +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.Request; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class SonyTestUtils { + public static void assertRequest(final Request request, final String messageHex) { + final Message message = Message.fromBytes(GB.hexStringToByteArray(messageHex.replace(":", ""))); + assertRequest(request, message.getType(), message.getPayload()); + } + + public static void assertRequest(final Request request, final int messageType, final String payloadHex) { + assertRequest( + request, + MessageType.fromCode((byte) messageType), + GB.hexStringToByteArray(payloadHex.replace(":", "")) + ); + } + + public static void assertRequest(final Request request, final MessageType messageType, final byte[] payload) { + assertEquals("Message types should be the same", messageType, request.messageType()); + assertArrayEquals("Payloads should be the same", payload, request.payload()); + } + + public static List handleMessage(final AbstractSonyProtocolImpl protocol, final String messageHex) { + final Message message = Message.fromBytes(GB.hexStringToByteArray(messageHex.replace(":", ""))); + + return protocol.handlePayload(message.getType(), message.getPayload()); + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1Test.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1Test.java new file mode 100644 index 000000000..481877a4a --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v1/SonyProtocolImplV1Test.java @@ -0,0 +1,300 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v1; + +import static org.junit.Assert.*; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequest; + +import org.junit.Test; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.ButtonModes; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.MessageType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class SonyProtocolImplV1Test { + private final SonyProtocolImplV1 protocol = new SonyProtocolImplV1(null); + + @Test + public void getAmbientSoundControl() { + // TODO + final Request request = protocol.getAmbientSoundControl(); + assertRequest(request, 0x0c, "66:02"); + } + + @Test + public void setAmbientSoundControl() { + // TODO + } + + @Test + public void getNoiseCancellingOptimizerState() { + // TODO + } + + @Test + public void getAudioCodec() { + // TODO + } + + @Test + public void getBattery() { + // TODO + } + + @Test + public void getFirmwareVersion() { + // TODO + } + + @Test + public void getAudioUpsampling() { + // TODO + } + + @Test + public void setAudioUpsampling() { + // TODO + } + + @Test + public void getAutomaticPowerOff() { + // TODO + } + + @Test + public void setAutomaticPowerOff() { + // TODO + } + + @Test + public void getButtonModes() { + // TODO + } + + @Test + public void setButtonModes() { + // TODO + final Request request = protocol.setButtonModes(new ButtonModes( + ButtonModes.Mode.AMBIENT_SOUND_CONTROL, + ButtonModes.Mode.PLAYBACK_CONTROL + )); + assertRequest(request, "3e0c0100000005f806020020323c"); + } + + @Test + public void getPauseWhenTakenOff() { + // TODO + } + + @Test + public void setPauseWhenTakenOff() { + // TODO + } + + @Test + public void getEqualizer() { + // TODO + } + + @Test + public void setEqualizerPreset() { + + } + + @Test + public void setEqualizerCustomBands() { + // TODO + } + + @Test + public void getSoundPosition() { + // TODO + } + + @Test + public void setSoundPosition() { + // TODO + } + + @Test + public void getSurroundMode() { + // TODO + } + + @Test + public void setSurroundMode() { + // TODO + } + + @Test + public void getTouchSensor() { + // TODO + } + + @Test + public void setTouchSensor() { + // TODO + } + + @Test + public void getVoiceNotifications() { + // TODO + } + + @Test + public void setVoiceNotifications() { + // TODO + } + + @Test + public void startNoiseCancellingOptimizer() { + // TODO + } + + @Test + public void powerOff() { + // TODO + } + + @Test + public void handlePayload() { + // TODO + } + + @Test + public void validInitPayload() { + // TODO + } + + @Test + public void handleInitResponse() { + // TODO + } + + @Test + public void handleAmbientSoundControl() { + // TODO + } + + @Test + public void handleNoiseCancellingOptimizerStatus() { + // TODO + } + + @Test + public void handleNoiseCancellingOptimizerState() { + // TODO + } + + @Test + public void handleAudioUpsampling() { + // TODO + } + + @Test + public void handleAutomaticPowerOff() { + // TODO + } + + @Test + public void handleButtonModes() { + // TODO + } + + @Test + public void handlePauseWhenTakenOff() { + // TODO + } + + @Test + public void handleBattery() { + // TODO + } + + @Test + public void handleAudioCodec() { + // TODO + final List event = protocol.handlePayload( + MessageType.fromCode((byte) 0x0c), + GB.hexStringToByteArray("1b:00:01".replace(":", "")) + ); + + assertEquals("Expect 2 events", 2, event.size()); + } + + @Test + public void handleEqualizer() { + // TODO + } + + @Test + public void handleFirmwareVersion() { + // TODO + } + + @Test + public void handleJson() { + // TODO + } + + @Test + public void handleAutomaticPowerOffButtonMode() { + // TODO + } + + @Test + public void handleVirtualSound() { + // TODO + } + + @Test + public void handleSoundPosition() { + // TODO + } + + @Test + public void handleSurroundMode() { + // TODO + } + + @Test + public void handleTouchSensor() { + // TODO + } + + @Test + public void handleVoiceNotifications() { + // TODO + } + + @Test + public void booleanFromByte() { + assertEquals(Boolean.FALSE, protocol.booleanFromByte((byte) 0x00)); + assertEquals(Boolean.TRUE, protocol.booleanFromByte((byte) 0x01)); + assertNull(protocol.booleanFromByte((byte) 0x02)); + } + + @Test + public void supportsWindNoiseCancelling() { + // TODO + } +} diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2Test.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2Test.java new file mode 100644 index 000000000..cec7d4092 --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/sony/headphones/protocol/impl/v2/SonyProtocolImplV2Test.java @@ -0,0 +1,371 @@ +/* Copyright (C) 2022 José Rebelo + + This file is part of Gadgetbridge. + + Gadgetbridge is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published + by the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Gadgetbridge is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . */ +package nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.v2; + +import static org.junit.Assert.assertEquals; + +import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.assertRequest; +import static nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.impl.SonyTestUtils.handleMessage; + +import org.junit.Ignore; +import org.junit.Test; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.SonyHeadphonesCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWF1000XM4Coordinator; +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.EqualizerPreset; +import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.PauseWhenTakenOff; +import nodomain.freeyourgadget.gadgetbridge.service.devices.sony.headphones.protocol.Request; + +public class SonyProtocolImplV2Test { + private final SonyProtocolImplV2 protocol = new SonyProtocolImplV2(null) { + @Override + protected SonyHeadphonesCoordinator getCoordinator() { + return new SonyWF1000XM4Coordinator(); + } + }; + + @Test + public void getAmbientSoundControl() { + // TODO + } + + @Test + public void setAmbientSoundControl() { + // TODO + final Request request = protocol.setAmbientSoundControl(new AmbientSoundControl( + AmbientSoundControl.Mode.WIND_NOISE_REDUCTION, + true, + 15 + )); + assertRequest(request, "3e:0c:00:00:00:00:08:68:15:01:01:00:03:01:0f:a6:3c"); + } + + @Test + public void getNoiseCancellingOptimizerState() { + // TODO + } + + @Test + public void getAudioCodec() { + // TODO + } + + @Test + public void getBattery() { + // TODO + } + + @Test + public void getFirmwareVersion() { + // TODO + } + + @Test + public void getAudioUpsampling() { + // TODO + } + + @Test + public void setAudioUpsampling() { + final Request requestEnabled = protocol.setAudioUpsampling(new AudioUpsampling(true)); + assertRequest(requestEnabled, "3e:0c:00:00:00:00:03:e8:01:01:f9:3c"); + + final Request requestDisabled = protocol.setAudioUpsampling(new AudioUpsampling(false)); + assertRequest(requestDisabled, "3e:0c:01:00:00:00:03:e8:01:00:f9:3c"); + } + + @Test + public void getAutomaticPowerOff() { + // TODO + } + + @Test + public void setAutomaticPowerOff() { + // TODO + } + + @Test + public void getButtonModes() { + // TODO + } + + @Test + public void setButtonModes() { + // TODO + } + + @Test + public void getPauseWhenTakenOff() { + // TODO + } + + @Test + public void setPauseWhenTakenOff() { + final Request requestEnabled = protocol.setPauseWhenTakenOff(new PauseWhenTakenOff(true)); + assertRequest(requestEnabled, "3e:0c:01:00:00:00:03:f8:01:00:09:3c"); + + final Request requestDisabled = protocol.setPauseWhenTakenOff(new PauseWhenTakenOff(false)); + assertRequest(requestDisabled, "3e:0c:00:00:00:00:03:f8:01:01:09:3c"); + } + + @Test + public void getEqualizer() { + final Request requestDisabled = protocol.getEqualizer(); + assertRequest(requestDisabled, "3e:0c:00:00:00:00:02:56:00:64:3c"); + } + + @Test + public void setEqualizerPreset() { + final Map commands = new LinkedHashMap() {{ + put(EqualizerPreset.OFF, "3e:0c:01:00:00:00:04:58:00:00:00:69:3c"); + put(EqualizerPreset.BRIGHT, "3e:0c:01:00:00:00:04:58:00:10:00:79:3c"); + put(EqualizerPreset.EXCITED, "3e:0c:01:00:00:00:04:58:00:11:00:7a:3c"); + put(EqualizerPreset.MELLOW, "3e:0c:01:00:00:00:04:58:00:12:00:7b:3c"); + put(EqualizerPreset.RELAXED, "3e:0c:01:00:00:00:04:58:00:13:00:7c:3c"); + put(EqualizerPreset.VOCAL, "3e:0c:01:00:00:00:04:58:00:14:00:7d:3c"); + put(EqualizerPreset.TREBLE_BOOST, "3e:0c:01:00:00:00:04:58:00:15:00:7e:3c"); + put(EqualizerPreset.BASS_BOOST, "3e:0c:01:00:00:00:04:58:00:16:00:7f:3c"); + put(EqualizerPreset.SPEECH, "3e:0c:01:00:00:00:04:58:00:17:00:80:3c"); + put(EqualizerPreset.MANUAL, "3e:0c:01:00:00:00:04:58:00:a0:00:09:3c"); + put(EqualizerPreset.CUSTOM_1, "3e:0c:01:00:00:00:04:58:00:a1:00:0a:3c"); + put(EqualizerPreset.CUSTOM_2, "3e:0c:01:00:00:00:04:58:00:a2:00:0b:3c"); + }}; + + for (Map.Entry entry : commands.entrySet()) { + final Request request = protocol.setEqualizerPreset(entry.getKey()); + assertRequest(request, entry.getValue()); + } + } + + @Test + public void setEqualizerCustomBands() { + // TODO + } + + @Test + @Ignore("Not implemented on V2") + public void getSoundPosition() { + } + + @Test + @Ignore("Not implemented on V2") + public void setSoundPosition() { + } + + @Test + @Ignore("Not implemented on V2") + public void getSurroundMode() { + } + + @Test + @Ignore("Not implemented on V2") + public void setSurroundMode() { + } + + @Test + @Ignore("Not implemented on V2") + public void getTouchSensor() { + } + + @Test + @Ignore("Not implemented on V2") + public void setTouchSensor() { + } + + @Test + @Ignore("Not implemented on V2") + public void getVoiceNotifications() { + } + + @Test + @Ignore("Not implemented on V2") + public void setVoiceNotifications() { + } + + @Test + @Ignore("Not implemented on V2") + public void startNoiseCancellingOptimizer() { + } + + @Test + @Ignore("Not implemented on V2") + public void powerOff() { + } + + @Test + public void handlePayload() { + // TODO + } + + @Test + public void validInitPayload() { + // TODO + } + + @Test + public void handleInitResponse() { + // TODO + } + + @Test + public void handleAmbientSoundControl() { + // TODO + } + + @Test + @Ignore("Not implemented on V2") + public void handleNoiseCancellingOptimizerStatus() { + } + + @Test + @Ignore("Not implemented on V2") + public void handleNoiseCancellingOptimizerState() { + } + + @Test + public void handleAudioUpsampling() { + final Map commands = new LinkedHashMap() {{ + put(new AudioUpsampling(false), "3e:0c:00:00:00:00:03:e9:01:00:f9:3c"); + put(new AudioUpsampling(true), "3e:0c:01:00:00:00:03:e9:01:01:fb:3c"); + }}; + + for (Map.Entry entry : commands.entrySet()) { + final List events = handleMessage(protocol, entry.getValue()); + assertEquals("Expect 1 events", 1, events.size()); + final GBDeviceEventUpdatePreferences event = (GBDeviceEventUpdatePreferences) events.get(0); + final Object modePrefValue = entry.getKey() + .toPreferences() + .get(DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING); + assertEquals(modePrefValue, event.preferences.get(DeviceSettingsPreferenceConst.PREF_SONY_AUDIO_UPSAMPLING)); + } + } + + @Test + public void handleAutomaticPowerOff() { + // TODO + } + + @Test + public void handleButtonModes() { + // TODO + } + + @Test + public void handlePauseWhenTakenOff() { + final Map commands = new LinkedHashMap() {{ + put(new PauseWhenTakenOff(false), "3e:0c:00:00:00:00:03:f9:01:01:0a:3c"); + put(new PauseWhenTakenOff(true), "3e:0c:01:00:00:00:03:f9:01:00:0a:3c"); + }}; + + for (Map.Entry entry : commands.entrySet()) { + final List events = handleMessage(protocol, entry.getValue()); + assertEquals("Expect 1 events", 1, events.size()); + final GBDeviceEventUpdatePreferences event = (GBDeviceEventUpdatePreferences) events.get(0); + final Object modePrefValue = entry.getKey() + .toPreferences() + .get(DeviceSettingsPreferenceConst.PREF_SONY_PAUSE_WHEN_TAKEN_OFF); + assertEquals(modePrefValue, event.preferences.get(DeviceSettingsPreferenceConst.PREF_SONY_PAUSE_WHEN_TAKEN_OFF)); + } + } + + @Test + public void handleBattery() { + // TODO + } + + @Test + public void handleAudioCodec() { + // TODO + } + + @Test + public void handleEqualizer() { + final Map commands = new LinkedHashMap() {{ + put(EqualizerPreset.OFF, "3e:0c:01:00:00:00:0a:59:00:00:06:0a:0a:0a:0a:0a:0a:b2:3c"); + put(EqualizerPreset.BRIGHT, "3e:0c:01:00:00:00:0a:59:00:10:06:09:0a:0f:11:11:13:dd:3c"); + put(EqualizerPreset.EXCITED, "3e:0c:01:00:00:00:0a:59:00:11:06:12:09:0b:0a:0d:0f:d3:3c"); + put(EqualizerPreset.MELLOW, "3e:0c:01:00:00:00:0a:59:00:12:06:07:09:08:07:06:04:b1:3c"); + put(EqualizerPreset.RELAXED, "3e:0c:01:00:00:00:0a:59:00:13:06:01:07:09:07:05:02:a8:3c"); + put(EqualizerPreset.VOCAL, "3e:0c:01:00:00:00:0a:59:00:14:06:0a:10:0e:0c:0d:09:d4:3c"); + put(EqualizerPreset.TREBLE_BOOST, "3e:0c:01:00:00:00:0a:59:00:15:06:0a:0a:0a:0c:10:14:d9:3c"); + put(EqualizerPreset.BASS_BOOST, "3e:0c:01:00:00:00:0a:59:00:16:06:11:0a:0a:0a:0a:0a:cf:3c"); + put(EqualizerPreset.SPEECH, "3e:0c:01:00:00:00:0a:59:00:17:06:00:0e:0d:0b:0c:00:bf:3c"); + put(EqualizerPreset.MANUAL, "3e:0c:01:00:00:00:0a:59:00:a0:06:0a:0a:0a:0a:0a:0a:52:3c"); + put(EqualizerPreset.CUSTOM_1, "3e:0c:01:00:00:00:0a:59:00:a1:06:0a:0a:0a:0a:0a:0a:53:3c"); + put(EqualizerPreset.CUSTOM_2, "3e:0c:01:00:00:00:0a:59:00:a2:06:0a:0a:0a:0a:0a:0a:54:3c"); + }}; + + for (Map.Entry entry : commands.entrySet()) { + final List events = handleMessage(protocol, entry.getValue()); + assertEquals("Expect 1 events", 1, events.size()); + final GBDeviceEventUpdatePreferences event = (GBDeviceEventUpdatePreferences) events.get(0); + final Object modePrefValue = entry.getKey() + .toPreferences() + .get(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE); + assertEquals(modePrefValue, event.preferences.get(DeviceSettingsPreferenceConst.PREF_SONY_EQUALIZER_MODE)); + } + } + + @Test + public void handleFirmwareVersion() { + // TODO + } + + @Test + @Ignore("Not implemented on V2") + public void handleJson() { + } + + @Test + public void handleAutomaticPowerOffButtonMode() { + // TODO + } + + @Test + @Ignore("Not implemented on V2") + public void handleVirtualSound() { + } + + @Test + @Ignore("Not implemented on V2") + public void handleSoundPosition() { + } + + @Test + @Ignore("Not implemented on V2") + public void handleSurroundMode() { + } + + @Test + @Ignore("Not implemented on V2") + public void handleTouchSensor() { + } + + @Test + @Ignore("Not implemented on V2") + public void handleVoiceNotifications() { + } +}