From 5f91715c89a5a05f8269b48ff86b1a7e26fc4f5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Rebelo?= Date: Sun, 17 Nov 2024 17:23:23 +0000 Subject: [PATCH] Realme Buds T110: Initial support --- .idea/dictionaries/t.xml | 1 + .../devices/oppo/OppoEncoAirCoordinator.java | 99 ++++++----------- .../oppo/OppoHeadphonesCoordinator.java | 101 ++++++++++++++++++ .../OppoHeadphonesSettingsCustomizer.java | 85 ++++++++++++++- .../realme/RealmeBudsT110Coordinator.java | 73 +++++++++++++ .../gadgetbridge/model/DeviceType.java | 2 + .../devices/oppo/OppoHeadphonesProtocol.java | 30 ++++-- .../oppo/commands/TouchConfigSide.java | 1 + .../oppo/commands/TouchConfigValue.java | 3 +- app/src/main/res/values/arrays.xml | 38 +++---- app/src/main/res/values/strings.xml | 1 + ...settings_oppo_headphones_touch_options.xml | 52 +++++---- .../oppo/OppoHeadphonesProtocolTest.java | 27 +++++ 13 files changed, 391 insertions(+), 122 deletions(-) create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java create mode 100644 app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT110Coordinator.java create mode 100644 app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocolTest.java diff --git a/.idea/dictionaries/t.xml b/.idea/dictionaries/t.xml index e2a60bb75..c82ca81bd 100644 --- a/.idea/dictionaries/t.xml +++ b/.idea/dictionaries/t.xml @@ -124,6 +124,7 @@ protomors qhybrid quallenauge + realme rebelo roidmi romanization diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoEncoAirCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoEncoAirCoordinator.java index 5546e24fd..937a8d832 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoEncoAirCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoEncoAirCoordinator.java @@ -16,93 +16,60 @@ along with this program. If not, see . */ package nodomain.freeyourgadget.gadgetbridge.devices.oppo; -import androidx.annotation.NonNull; +import android.util.Pair; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; import java.util.regex.Pattern; -import nodomain.freeyourgadget.gadgetbridge.GBException; import nodomain.freeyourgadget.gadgetbridge.R; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; -import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; -import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator; -import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; -import nodomain.freeyourgadget.gadgetbridge.entities.Device; -import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; -import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; -import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; -import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.OppoHeadphonesSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue; -public class OppoEncoAirCoordinator extends AbstractBLClassicDeviceCoordinator { +public class OppoEncoAirCoordinator extends OppoHeadphonesCoordinator { @Override protected Pattern getSupportedDeviceName() { return Pattern.compile("OPPO Enco Air", Pattern.LITERAL); } - @Override - protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException { - - } - - @Override - public String getManufacturer() { - return "Oppo"; - } - - @NonNull - @Override - public Class getDeviceSupportClass() { - return OppoHeadphonesSupport.class; - } - @Override public int getDeviceNameResource() { return R.string.devicetype_oppo_enco_air; } - @Override - public int getDefaultIconResource() { - return R.drawable.ic_device_nothingear; - } - - @Override - public int getDisabledIconResource() { - return R.drawable.ic_device_nothingear_disabled; - } - @Override public boolean supportsFindDevice() { return true; } @Override - public int getBatteryCount() { - return 3; - } + protected Map, List> getTouchOptions() { + return new LinkedHashMap, List>() {{ + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.PLAY_PAUSE, + TouchConfigValue.PREVIOUS, + TouchConfigValue.NEXT, + TouchConfigValue.VOICE_ASSISTANT + )); + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.VOICE_ASSISTANT, + TouchConfigValue.GAME_MODE + )); + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.VOLUME_UP, + TouchConfigValue.VOLUME_DOWN + )); - @Override - public BatteryConfig[] getBatteryConfig(final GBDevice device) { - final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud); - final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud); - final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case); - return new BatteryConfig[]{battery1, battery2, battery3}; - } - - @Override - public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { - final DeviceSpecificSettings settings = new DeviceSpecificSettings(); - - settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS); - settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options); - - settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); - settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones); - - return settings; - } - - @Override - public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { - return new OppoHeadphonesSettingsCustomizer(); + // Right side is the same + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2))); + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3))); + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), get(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD))); + }}; } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java new file mode 100644 index 000000000..a05297dc7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java @@ -0,0 +1,101 @@ +/* Copyright (C) 2024 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.oppo; + +import android.util.Pair; + +import androidx.annotation.NonNull; + +import java.util.List; +import java.util.Map; + +import nodomain.freeyourgadget.gadgetbridge.GBException; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; +import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen; +import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator; +import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession; +import nodomain.freeyourgadget.gadgetbridge.entities.Device; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.OppoHeadphonesSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue; + +public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceCoordinator { + @Override + protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException { + + } + + @Override + public String getManufacturer() { + return "Oppo"; + } + + @NonNull + @Override + public Class getDeviceSupportClass() { + return OppoHeadphonesSupport.class; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_device_nothingear; + } + + @Override + public int getDisabledIconResource() { + return R.drawable.ic_device_nothingear_disabled; + } + + @Override + public int getBatteryCount() { + return 3; + } + + @Override + public BatteryConfig[] getBatteryConfig(final GBDevice device) { + final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud); + final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud); + final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case); + return new BatteryConfig[]{battery1, battery2, battery3}; + } + + @Override + public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) { + final DeviceSpecificSettings settings = new DeviceSpecificSettings(); + + settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS); + settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options); + + settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); + settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones); + + return settings; + } + + @Override + public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { + return new OppoHeadphonesSettingsCustomizer(getTouchOptions()); + } + + protected abstract Map, List> getTouchOptions(); +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesSettingsCustomizer.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesSettingsCustomizer.java index 693a356e7..c081027a1 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesSettingsCustomizer.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesSettingsCustomizer.java @@ -17,23 +17,43 @@ package nodomain.freeyourgadget.gadgetbridge.devices.oppo; import android.os.Parcel; +import android.util.Pair; +import androidx.preference.ListPreference; import androidx.preference.Preference; +import java.util.ArrayList; import java.util.Collections; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; import java.util.Set; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler; import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide; import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer { + private final Map, List> touchOptions; + public static final Creator CREATOR = new Creator() { @Override public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) { - return new OppoHeadphonesSettingsCustomizer(); + final Map, List> touchOptions = new LinkedHashMap<>(); + final int numOptions = in.readInt(); + for (int i = 0; i < numOptions; i++) { + final TouchConfigSide touchConfigSide = TouchConfigSide.valueOf(in.readString()); + final TouchConfigType touchConfigType = TouchConfigType.valueOf(in.readString()); + final List values = new ArrayList<>(); + in.readList(values, TouchConfigValue.class.getClassLoader()); + touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values); + } + return new OppoHeadphonesSettingsCustomizer(touchOptions); } @Override @@ -42,15 +62,70 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC } }; + public OppoHeadphonesSettingsCustomizer(final Map, List> touchOptions) { + this.touchOptions = touchOptions; + } + @Override public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { } @Override public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) { + final Set knownSides = new HashSet<>(); + final Set knownTypes = new HashSet<>(); + + for (final Map.Entry, List> e : touchOptions.entrySet()) { + final TouchConfigSide side = e.getKey().first; + final TouchConfigType type = e.getKey().second; + final Set possibleValues = new HashSet<>(e.getValue()); + + knownSides.add(side); + knownTypes.add(type); + + final String key = OppoHeadphonesPreferences.getKey(side, type); + final ListPreference pref = handler.findPreference(key); + if (pref == null) { + continue; + } + + final CharSequence[] originalEntries = pref.getEntries(); + final CharSequence[] originalValues = pref.getEntryValues(); + final CharSequence[] entries = new CharSequence[possibleValues.size()]; + final CharSequence[] values = new CharSequence[possibleValues.size()]; + int j = 0; + for (int i = 0; i < originalValues.length; i++) { + if (possibleValues.contains(TouchConfigValue.valueOf(originalValues[i].toString().toUpperCase(Locale.ROOT)))) { + entries[j] = originalEntries[i]; + values[j] = originalValues[i]; + j++; + } + } + + pref.setEntries(entries); + pref.setEntryValues(values); + + handler.addPreferenceHandlerFor(key); + } + for (final TouchConfigSide side : TouchConfigSide.values()) { + if (!knownSides.contains(side)) { + // Side not configurable, hide it completely + final Preference header = handler.findPreference("oppo_touch_header_" + side.name().toLowerCase(Locale.ROOT)); + if (header != null) { + header.setVisible(false); + continue; + } + } + for (final TouchConfigType type : TouchConfigType.values()) { - handler.addPreferenceHandlerFor(OppoHeadphonesPreferences.getKey(side, type)); + if (!knownTypes.contains(type)) { + final String key = OppoHeadphonesPreferences.getKey(side, type); + final Preference pref = handler.findPreference(key); + if (pref != null) { + pref.setVisible(false); + } + } } } } @@ -67,5 +142,11 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC @Override public void writeToParcel(final Parcel dest, final int flags) { + dest.writeInt(touchOptions.size()); + for (final Map.Entry, List> e : touchOptions.entrySet()) { + dest.writeString(e.getKey().first.name()); + dest.writeString(e.getKey().second.name()); + dest.writeList(e.getValue()); + } } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT110Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT110Coordinator.java new file mode 100644 index 000000000..9ffea1b39 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT110Coordinator.java @@ -0,0 +1,73 @@ +/* Copyright (C) 2024 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.realme; + +import android.util.Pair; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesCoordinator; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigValue; + +public class RealmeBudsT110Coordinator extends OppoHeadphonesCoordinator { + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("realme Buds T110", Pattern.LITERAL); + } + + @Override + public String getManufacturer() { + return "Realme"; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_realme_buds_t110; + } + + @Override + protected Map, List> getTouchOptions() { + return new LinkedHashMap, List>() {{ + final List options = Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.PLAY_PAUSE, + TouchConfigValue.PREVIOUS, + TouchConfigValue.NEXT, + TouchConfigValue.VOLUME_UP, + TouchConfigValue.VOLUME_DOWN, + TouchConfigValue.VOICE_ASSISTANT_REALME + ); + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_2), options); + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.TAP_3), options); + put(Pair.create(TouchConfigSide.LEFT, TouchConfigType.HOLD), options); + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_2), options); + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.TAP_3), options); + put(Pair.create(TouchConfigSide.RIGHT, TouchConfigType.HOLD), options); + put(Pair.create(TouchConfigSide.BOTH, TouchConfigType.HOLD), Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.GAME_MODE + )); + }}; + } +} 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 c97a90d52..d4ccfd5ae 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -231,6 +231,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.scannable.ScannableDeviceCoordinator; @@ -544,6 +545,7 @@ public enum DeviceType { SUPER_CARS(SuperCarsCoordinator.class), ASTEROIDOS(AsteroidOSDeviceCoordinator.class), OPPO_ENCO_AIR(OppoEncoAirCoordinator.class), + REALME_BUDS_T110(RealmeBudsT110Coordinator.class), SOFLOW_SO6(SoFlowCoordinator.class), WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class), SONY_WENA_3(SonyWena3Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocol.java index 89d9c0cca..2dc87b72c 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocol.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocol.java @@ -66,7 +66,7 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { i++; continue; } - final int totalLength = BLETypeConversions.toUint16(responseData, i + 1); + final int totalLength = responseData[i + 1] & 0xff; if (responseData.length - i < totalLength + 2) { LOG.error("Got partial response with {} bytes, expected {}", responseData.length - i, totalLength + 2); break; @@ -99,9 +99,9 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { } final short zero = responseBuf.getShort(); - if (zero != 0) { - LOG.error("Unexpected bytes: {}, expected 0", zero); - return Collections.emptyList(); + if (zero != 0 && zero != 4) { + // 0 on oppo, 4 on realme? + LOG.warn("Unexpected bytes: {}, expected 0 or 4", zero); } final short code = responseBuf.getShort(); @@ -146,10 +146,11 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { break; } - final String fwString = StringUtils.untilNullTerminator(payload, 1); - if (fwString == null) { - LOG.warn("Failed to get firmware string"); - break; + final String fwString; + if (payload[payload.length - 1] == 0) { + fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 1)).strip(); + } else { + fwString = new String(ArrayUtils.subarray(payload, 2, payload.length - 2)).strip(); } final String[] parts = fwString.split(","); if (parts.length % 3 != 0) { @@ -180,7 +181,18 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { } } - final String fwVersion = String.join(".", fwVersionParts); + final List nonNullParts = new ArrayList<>(fwVersionParts.length); + for (int i = 0; i < fwVersionParts.length; i++) { + if (fwVersionParts[i] == null) { + continue; + } + nonNullParts.add(fwVersionParts[i]); + if (fwVersionParts[i].contains(".")) { + // Realme devices have the version already with the dots, repeated multiple times + break; + } + } + final String fwVersion = String.join(".", nonNullParts); final GBDeviceEventVersionInfo eventVersionInfo = new GBDeviceEventVersionInfo(); eventVersionInfo.fwVersion = fwVersion; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigSide.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigSide.java index 32c962b39..b717567b7 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigSide.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigSide.java @@ -21,6 +21,7 @@ import androidx.annotation.Nullable; public enum TouchConfigSide { LEFT(0x01), RIGHT(0x02), + BOTH(0x04), ; private final int code; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigValue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigValue.java index ce49e1e46..ec496ec86 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigValue.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/TouchConfigValue.java @@ -21,7 +21,8 @@ import androidx.annotation.Nullable; public enum TouchConfigValue { OFF(0x00), PLAY_PAUSE(0x01), - VOICE_ASSISTANT(0x03), + VOICE_ASSISTANT(0x03), // oppo + VOICE_ASSISTANT_REALME(0x04), PREVIOUS(0x05), NEXT(0x06), GAME_MODE(0x11), diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 945ad0a8c..4c0cb8cef 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -3566,44 +3566,30 @@ as_off - + + @string/sony_button_mode_off @string/moondrop_touch_action_play_pause @string/pref_media_previous @string/pref_media_next + @string/pref_media_volumeup + @string/pref_media_volumedown @string/pref_title_touch_voice_assistant - - - - off - play_pause - previous - next - voice_assistant - - - - @string/sony_button_mode_off @string/pref_title_touch_voice_assistant @string/prefs_game_mode - - off - voice_assistant - game_mode - - - - @string/sony_button_mode_off - @string/pref_media_volumeup - @string/pref_media_volumedown - - - + + off + play_pause + previous + next volume_up volume_down + voice_assistant + voice_assistant_realme + game_mode diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 14d798665..350de55c4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2489,6 +2489,7 @@ CMF Watch Pro CMF Watch Pro 2 Oppo Enco Air + Realme Buds T110 Galaxy Buds Galaxy Buds Live Galaxy Buds Pro diff --git a/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options.xml b/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options.xml index 7c8c59b4f..bae22d44c 100644 --- a/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options.xml +++ b/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options.xml @@ -3,28 +3,29 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocolTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocolTest.java new file mode 100644 index 000000000..37133b85f --- /dev/null +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesProtocolTest.java @@ -0,0 +1,27 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.oppo; + +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.test.TestBase; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +public class OppoHeadphonesProtocolTest extends TestBase { + @Test + public void testHandleFirmware() { + final OppoHeadphonesProtocol protocol = new OppoHeadphonesProtocol(null); + GBDeviceEvent[] oppoEvents = protocol.decodeResponse(GB.hexStringToByteArray("aa4f00000581f34800000a312c312c312c312c322c3136302c312c332c3838382c312c342c302c322c312c312c322c322c3136302c322c332c3838382c322c342c302c332c312c312c332c322c38323700")); + Assert.assertEquals(1, oppoEvents.length); + GBDeviceEventVersionInfo oppoEvent = (GBDeviceEventVersionInfo) oppoEvents[0]; + Assert.assertEquals("160.160.827", oppoEvent.fwVersion); + + GBDeviceEvent[] realme = protocol.decodeResponse(GB.hexStringToByteArray("aa2a040005810e23000003312c322c312e312e302e37352c322c322c312e312e302e37352c332c322c303031")); + Assert.assertEquals(1, realme.length); + GBDeviceEventVersionInfo realmeEvent = (GBDeviceEventVersionInfo) realme[0]; + Assert.assertEquals("1.1.0.75", realmeEvent.fwVersion); + } +}