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 index b487941b40..84b8744162 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesCoordinator.java @@ -76,12 +76,39 @@ public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceC settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones); + if (this.supportsLdac(device) || this.supportsAnc(device)) { + settings.addRootScreen(DeviceSpecificSettingsScreen.AUDIO); + if (this.supportsLdac(device)) { + settings.addSubScreen(DeviceSpecificSettingsScreen.AUDIO, R.xml.devicesettings_ldac_toggle); + } + if (this.supportsAnc(device)) { + settings.addSubScreen(DeviceSpecificSettingsScreen.AUDIO, R.xml.devicesettings_onemore_noise_control_selector); + settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options_anc); + } + } + + if (this.supportsMultipoint(device) || this.supportsGameMode(device)) { + settings.addRootScreen(DeviceSpecificSettingsScreen.CONNECTION); + if (this.supportsMultipoint(device)) { + settings.addSubScreen(DeviceSpecificSettingsScreen.CONNECTION, R.xml.devicesettings_oppo_headphones_multipoint); + } + if (this.supportsGameMode(device)) { + settings.addSubScreen(DeviceSpecificSettingsScreen.CONNECTION, R.xml.devicesettings_oppo_headphones_game_mode); + } + } + return settings; } @Override public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { - return new OppoHeadphonesSettingsCustomizer(getTouchOptions()); + return new OppoHeadphonesSettingsCustomizer( + getTouchOptions(), + supportsLdac(device), + supportsMultipoint(device), + supportsGameMode(device), + supportsAnc(device) + ); } protected abstract Map, List> getTouchOptions(); @@ -90,4 +117,20 @@ public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceC public final DeviceKind getDeviceKind(@NonNull GBDevice device) { return DeviceKind.EARBUDS; } + + public boolean supportsLdac(@NonNull GBDevice device) { + return false; + } + + public boolean supportsMultipoint(@NonNull GBDevice device) { + return false; + } + + public boolean supportsGameMode(@NonNull GBDevice device) { + return false; + } + + public boolean supportsAnc(@NonNull GBDevice device) { + return false; + } } diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesPreferences.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesPreferences.java index c5538e8cd9..a73bbdb31b 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesPreferences.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/oppo/OppoHeadphonesPreferences.java @@ -22,10 +22,19 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchC import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; public class OppoHeadphonesPreferences { - public static String getKey(final TouchConfigSide side, final TouchConfigType type) { + public static final String TOUCH_PREFIX = "oppo_touch__"; + + public static final String LDAC = "pref_soundcore_ldac_mode"; + public static final String MULTIPOINT = "oppo_multipoint"; + public static final String GAME_MODE = "oppo_game_mode"; + public static final String ANC_SELECTOR = "noise_control_selector"; + public static final String ANC_TOUCH_CYCLE_MODES = "oppo_touch_anc_cycle_modes"; + + public static String getTouchKey(final TouchConfigSide side, final TouchConfigType type) { return String.format( Locale.ROOT, - "oppo_touch__%s__%s", + "%s%s__%s", + TOUCH_PREFIX, side.name().toLowerCase(Locale.ROOT), type.name().toLowerCase(Locale.ROOT) ); 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 c081027a16..045d445a93 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 @@ -18,9 +18,13 @@ package nodomain.freeyourgadget.gadgetbridge.devices.oppo; import android.os.Parcel; import android.util.Pair; +import android.content.Context; import androidx.preference.ListPreference; import androidx.preference.Preference; +import androidx.preference.MultiSelectListPreference; + +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import java.util.ArrayList; import java.util.Collections; @@ -40,10 +44,19 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs; public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer { private final Map, List> touchOptions; + private final boolean supportsLdac; + private final boolean supportsMultipoint; + private final boolean supportsGameMode; + private final boolean supportsAnc; public static final Creator CREATOR = new Creator() { @Override public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) { + final boolean supportsLdac = in.readByte() == 1; + final boolean supportsMultipoint = in.readByte() == 1; + final boolean supportsGameMode = in.readByte() == 1; + final boolean supportsAnc = in.readByte() == 1; + final Map, List> touchOptions = new LinkedHashMap<>(); final int numOptions = in.readInt(); for (int i = 0; i < numOptions; i++) { @@ -53,7 +66,8 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC in.readList(values, TouchConfigValue.class.getClassLoader()); touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values); } - return new OppoHeadphonesSettingsCustomizer(touchOptions); + + return new OppoHeadphonesSettingsCustomizer(touchOptions, supportsLdac, supportsMultipoint, supportsGameMode, supportsAnc); } @Override @@ -62,12 +76,32 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC } }; - public OppoHeadphonesSettingsCustomizer(final Map, List> touchOptions) { + public OppoHeadphonesSettingsCustomizer(final Map, List> touchOptions, final boolean supportsLdac, final boolean supportsMultipoint, final boolean supportsGameMode, final boolean supportsAnc) { this.touchOptions = touchOptions; + this.supportsLdac = supportsLdac; + this.supportsMultipoint = supportsMultipoint; + this.supportsGameMode = supportsGameMode; + this.supportsAnc = supportsAnc; } @Override public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { + if (OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES.equals(preference.getKey())) { + if (preference instanceof MultiSelectListPreference) { + final MultiSelectListPreference ancCycleModesPref = (MultiSelectListPreference) preference; + final Set selectedValues = ancCycleModesPref.getValues(); + + if (selectedValues == null || selectedValues.size() < 2) { + final Context context = preference.getContext(); + final String message = context.getString(nodomain.freeyourgadget.gadgetbridge.R.string.select_at_least_option, 2); + new MaterialAlertDialogBuilder(context) + .setTitle(nodomain.freeyourgadget.gadgetbridge.R.string.warning) + .setMessage(message) + .setPositiveButton(android.R.string.ok, null) + .show(); + } + } + } } @Override @@ -75,6 +109,12 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC final Set knownSides = new HashSet<>(); final Set knownTypes = new HashSet<>(); + this.addPreferenceHandler(handler, OppoHeadphonesPreferences.LDAC, supportsLdac); + this.addPreferenceHandler(handler, OppoHeadphonesPreferences.MULTIPOINT, supportsMultipoint); + this.addPreferenceHandler(handler, OppoHeadphonesPreferences.GAME_MODE, supportsGameMode); + this.addPreferenceHandler(handler, OppoHeadphonesPreferences.ANC_SELECTOR, supportsAnc); + this.addPreferenceHandler(handler, OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES, supportsAnc); + for (final Map.Entry, List> e : touchOptions.entrySet()) { final TouchConfigSide side = e.getKey().first; final TouchConfigType type = e.getKey().second; @@ -83,7 +123,7 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC knownSides.add(side); knownTypes.add(type); - final String key = OppoHeadphonesPreferences.getKey(side, type); + final String key = OppoHeadphonesPreferences.getTouchKey(side, type); final ListPreference pref = handler.findPreference(key); if (pref == null) { continue; @@ -120,7 +160,7 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC for (final TouchConfigType type : TouchConfigType.values()) { if (!knownTypes.contains(type)) { - final String key = OppoHeadphonesPreferences.getKey(side, type); + final String key = OppoHeadphonesPreferences.getTouchKey(side, type); final Preference pref = handler.findPreference(key); if (pref != null) { pref.setVisible(false); @@ -130,6 +170,16 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC } } + private void addPreferenceHandler(DeviceSpecificSettingsHandler handler, String key, boolean isSupported) { + Preference pref = handler.findPreference(key); + if (pref != null) { + pref.setVisible(isSupported); + if (isSupported) { + handler.addPreferenceHandlerFor(key); + } + } + } + @Override public Set getPreferenceKeysWithSummary() { return Collections.emptySet(); @@ -142,6 +192,11 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC @Override public void writeToParcel(final Parcel dest, final int flags) { + dest.writeByte((byte) (supportsLdac ? 1 : 0)); + dest.writeByte((byte) (supportsMultipoint ? 1 : 0)); + dest.writeByte((byte) (supportsGameMode ? 1 : 0)); + dest.writeByte((byte) (supportsAnc ? 1 : 0)); + dest.writeInt(touchOptions.size()); for (final Map.Entry, List> e : touchOptions.entrySet()) { dest.writeString(e.getKey().first.name()); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsAir6ProCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsAir6ProCoordinator.java new file mode 100644 index 0000000000..a81a85c424 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsAir6ProCoordinator.java @@ -0,0 +1,112 @@ +/* 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 androidx.annotation.NonNull; + +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.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig; +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 RealmeBudsAir6ProCoordinator extends OppoHeadphonesCoordinator { + @Override + protected Pattern getSupportedDeviceName() { + return Pattern.compile("realme Buds Air6 Pro", Pattern.LITERAL); + } + + @Override + public String getManufacturer() { + return "Realme"; + } + + @Override + public int getDeviceNameResource() { + return R.string.devicetype_realme_buds_air_6_pro; + } + + @Override + public BatteryConfig[] getBatteryConfig(final GBDevice device) { + final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_realme_buds_t300_l, R.string.left_earbud); + final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_realme_buds_t300_r, R.string.right_earbud); + final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_realme_buds_t300_case, R.string.battery_case); + return new BatteryConfig[]{battery1, battery2, battery3}; + } + + @Override + public int getDefaultIconResource() { + return R.drawable.ic_realme_buds_t300; + } + + @Override + public boolean supportsFindDevice(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsLdac(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsMultipoint(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsGameMode(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsAnc(@NonNull GBDevice device) { + return true; + } + + @Override + protected Map, List> getTouchOptions() { + return new LinkedHashMap<>() {{ + final List options = Arrays.asList( + TouchConfigValue.OFF, + TouchConfigValue.PLAY_PAUSE, + TouchConfigValue.PREVIOUS, + TouchConfigValue.NEXT, + TouchConfigValue.VOLUME_UP, + TouchConfigValue.VOLUME_DOWN, + TouchConfigValue.VOICE_ASSISTANT_REALME, + TouchConfigValue.GAME_MODE, + TouchConfigValue.NOISE_CONTROL + ); + 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); + }}; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT200Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT200Coordinator.java index 2dc7731f54..f998da2dc2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT200Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/realme/RealmeBudsT200Coordinator.java @@ -67,6 +67,26 @@ public class RealmeBudsT200Coordinator extends OppoHeadphonesCoordinator { return true; } + @Override + public boolean supportsLdac(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsMultipoint(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsGameMode(@NonNull GBDevice device) { + return true; + } + + @Override + public boolean supportsAnc(@NonNull GBDevice device) { + return true; + } + @Override protected Map, List> getTouchOptions() { return new LinkedHashMap<>() {{ 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 713c13dec4..1bc75e00a2 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -399,6 +399,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.polar.PolarH9DeviceCoordinat import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsAir5ProCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsAir6ProCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT100Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT200Coordinator; @@ -922,6 +923,7 @@ public enum DeviceType { REALME_BUDS_T200(RealmeBudsT200Coordinator.class), REALME_BUDS_T300(RealmeBudsT300Coordinator.class), REALME_BUDS_AIR_5_PRO(RealmeBudsAir5ProCoordinator.class), + REALME_BUDS_AIR_6_PRO(RealmeBudsAir6ProCoordinator.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 2c2bd7ed46..8fa4d4e4e4 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 @@ -26,7 +26,13 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; +import java.util.EnumSet; +import java.util.Set; +import java.util.HashSet; +import androidx.annotation.NonNull; + +import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; @@ -41,6 +47,10 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.OppoCo 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.service.devices.oppo.commands.MiscConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.AncConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.AncConfigValue; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.SubscriptionType; import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol; import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs; @@ -137,19 +147,11 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { events.addAll(parseBattery(payload)); break; } - case DEVICE_INFO: { - switch (payload[0]) { - case 1: // battery - events.addAll(parseBattery(payload)); - break; - case 2: // status - LOG.debug("Got status"); - // TODO handle - break; - default: - LOG.warn("Unknown device info {}", payload[0]); - } - + case SUBSCRIPTION_ACK: + LOG.debug("Got subscription ack, return={}", StringUtils.bytesToHex(payload)); + break; + case SUBSCRIPTION_RET: { + events.addAll(parseSubscription(payload)); break; } case FIRMWARE_RET: { @@ -157,126 +159,67 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { LOG.warn("Unexpected firmware ret {}", payload[0]); 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)).strip(); - } - final String[] parts = fwString.split(","); - if (parts.length % 3 != 0) { - LOG.warn("Fw parts length {} from '{}' is not divisible by 3", parts.length, fwString); - - // We need to persist something, otherwise Gb misbehaves - final GBDeviceEventVersionInfo eventVersionInfo = new GBDeviceEventVersionInfo(); - eventVersionInfo.fwVersion = fwString; - eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a); - events.add(eventVersionInfo); - - break; - } - final String[] fwVersionParts = new String[3]; - for (int i = 0; i < parts.length; i += 3) { - final String versionPart = parts[i]; - final String versionType = parts[i + 1]; - final String version = parts[i + 2]; - if (!"2".equals(versionType)) { - continue; // not fw - } - - switch (versionPart) { - case "1": - fwVersionParts[0] = version; - break; - case "2": - fwVersionParts[1] = version; - break; - case "3": - fwVersionParts[2] = version; - break; - default: - LOG.warn("Unknown firmware version part {}", versionPart); - } - } - - 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; - eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a); - events.add(eventVersionInfo); - - LOG.debug("Got fw version: {}", fwVersion); - + events.addAll(parseFirmware(payload)); break; } case FIND_DEVICE_ACK: { LOG.debug("Got find device ack, status={}", payload[0]); break; } + case MISC_CONFIG_RET: { + if (payload[0] != 0) { + LOG.warn("Unknown misc config ret {}", payload[0]); + break; + } + if (payload.length < 3) { + LOG.warn("Unexpected misc config ret payload size {}", payload.length); + break; + } + + events.add(parseMiscConfig(payload)); + break; + } + case MISC_CONFIG_ACK: { + LOG.debug("Got misc config ack, status={}", payload[0]); + break; + } case TOUCH_CONFIG_RET: { if (payload[0] != 0) { - LOG.warn("Unknown config ret {}", payload[0]); + LOG.warn("Unknown touch config ret {}", payload[0]); break; } if ((payload.length - 2) % 4 != 0) { - LOG.warn("Unexpected config ret payload size {}", payload.length); + LOG.warn("Unexpected touch config ret payload size {}", payload.length); break; } - final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(); - - for (int i = 2; i < payload.length; i += 4) { - final int sideCode = payload[i] & 0xff; - final int typeCode = BLETypeConversions.toUint16(payload, i + 1); - final int valueCode = payload[i + 3] & 0xff; - final TouchConfigSide side = TouchConfigSide.fromCode(sideCode); - final TouchConfigType type = TouchConfigType.fromCode(typeCode); - final TouchConfigValue value = TouchConfigValue.fromCode(valueCode); - - if (side == null) { - LOG.warn("Unknown touch side code {}", sideCode); - continue; - } - if (type == null) { - LOG.warn("Unknown touch type code {}", typeCode); - continue; - } - if (value == null) { - LOG.warn("Unknown touch value code {}", valueCode); - continue; - } - - LOG.debug("Got touch config for {} {} = {}", side, type, value); - - eventUpdatePreferences.withPreference( - OppoHeadphonesPreferences.getKey(side, type), - value.name().toLowerCase(Locale.ROOT) - ); - } - - events.add(eventUpdatePreferences); - + events.add(parseTouchConfig(payload)); break; } case TOUCH_CONFIG_ACK: { - LOG.debug("Got config ack, status={}", payload[0]); + LOG.debug("Got touch config ack, status={}", payload[0]); break; } - default: + case ANC_CONFIG_RET: { + if (payload[0] != 0) { + LOG.warn("Unknown anc config ret {}", payload[0]); + break; + } + if (payload.length < 3) { + LOG.warn("Unexpected anc config ret payload size {}", payload.length); + break; + } + + events.add(parseAncConfig(payload)); + break; + } + case ANC_CONFIG_ACK: { + LOG.debug("Got anc config ack, status={}", payload[0]); + break; + } + default: { LOG.warn("Unhandled command {}", command); + } } return events; @@ -297,6 +240,10 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { } final int batteryLevel = payload[i + 1] & 0x7f; + if (batteryIndex == 2 && batteryLevel == 0) { + continue; + } + final BatteryState batteryState = (payload[i + 1] & 0x80) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL; LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState); @@ -308,9 +255,264 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { events.add(eventBatteryInfo); } + List processedBatteries = events.stream() + .map(event -> ((GBDeviceEventBatteryInfo) event).batteryIndex) + .toList(); + + for (int i = 0; i < 3; i++) { + if (processedBatteries.contains(i)) { + continue; + } + + final GBDeviceEventBatteryInfo eventBatteryInfo = new GBDeviceEventBatteryInfo(); + eventBatteryInfo.batteryIndex = i; + eventBatteryInfo.level = -1; + eventBatteryInfo.state = BatteryState.UNKNOWN; + events.add(eventBatteryInfo); + } + return events; } + private static List parseFirmware(final byte[] payload) { + final List events = new ArrayList<>(); + + 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)).strip(); + } + final String[] parts = fwString.split(","); + if (parts.length % 3 != 0) { + LOG.warn("Fw parts length {} from '{}' is not divisible by 3", parts.length, fwString); + + // We need to persist something, otherwise Gb misbehaves + final GBDeviceEventVersionInfo eventVersionInfo = new GBDeviceEventVersionInfo(); + eventVersionInfo.fwVersion = fwString; + eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a); + events.add(eventVersionInfo); + + return events; + } + final String[] fwVersionParts = new String[3]; + for (int i = 0; i < parts.length; i += 3) { + final String versionPart = parts[i]; + final String versionType = parts[i + 1]; + final String version = parts[i + 2]; + if (!"2".equals(versionType)) { + continue; // not fw + } + + switch (versionPart) { + case "1": + fwVersionParts[0] = version; + break; + case "2": + fwVersionParts[1] = version; + break; + case "3": + fwVersionParts[2] = version; + break; + default: + LOG.warn("Unknown firmware version part {}", versionPart); + } + } + + 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; + eventVersionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a); + events.add(eventVersionInfo); + + LOG.debug("Got fw version: {}", fwVersion); + return events; + } + + + private static List parseSubscription(final byte[] payload) { + final List events = new ArrayList<>(); + final int typeCode = payload[0] & 0xff; + final SubscriptionType type = SubscriptionType.fromCode(typeCode); + if (type == null) { + LOG.warn("Unknown subcription type {}", String.format(Locale.ROOT, "0x%02x", typeCode)); + return events; + } + + final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(); + switch (type) { + case BATTERY: { + events.addAll(parseBattery(payload)); + break; + } + case STATUS: { + LOG.debug("Got status"); + // TODO handle + break; + } + case ANC_SELECTOR: { + final int valueCode = payload[2] & 0xFF; + final AncConfigValue mode = AncConfigValue.fromCode(valueCode); + if (mode == null) { + LOG.warn("Unknown anc mode code {}", valueCode); + } + LOG.debug("Got anc config for MODE = {}", mode); + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.ANC_SELECTOR, + mode.getPrefId() + ); + events.add(eventUpdatePreferences); + break; + } + case GAME_MODE: { + final boolean isEnabled = ((payload[1] & 0xFF) == 0x01); + LOG.debug("Got misc config for GAME_MODE = {}", isEnabled); + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.GAME_MODE, + isEnabled + ); + events.add(eventUpdatePreferences); + break; + } + default: { + LOG.warn("Unhandled subscription type {}", type); + break; + } + } + return events; + } + + private static GBDeviceEvent parseMiscConfig(final byte[] payload) { + final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(); + for (int i = 2; i + 1 < payload.length; i += 2) { + final int typeCode = payload[i] & 0xff; + final int valueCode = payload[i + 1] & 0xff; + + final MiscConfigType type = MiscConfigType.fromCode(typeCode); + if (type == null) { + LOG.warn("Unknown misc config type code {}", typeCode); + continue; + } + + final boolean isEnabled = (valueCode == 1); + switch (type) { + case LDAC: + LOG.debug("Got misc config for LDAC = {}", isEnabled); + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.LDAC, + isEnabled + ); + break; + case MULTIPOINT: + LOG.debug("Got misc config for MULTIPOINT = {}", isEnabled); + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.MULTIPOINT, + isEnabled + ); + break; + case GAME_MODE: + LOG.debug("Got misc config for GAME_MODE = {}", isEnabled); + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.GAME_MODE, + isEnabled + ); + break; + default: + LOG.warn("Unknown misc config type code {}", typeCode); + break; + } + } + return eventUpdatePreferences; + } + + private static GBDeviceEvent parseTouchConfig(final byte[] payload) { + final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(); + + for (int i = 2; i < payload.length; i += 4) { + final int sideCode = payload[i] & 0xff; + final int typeCode = BLETypeConversions.toUint16(payload, i + 1); + final int valueCode = payload[i + 3] & 0xff; + final TouchConfigSide side = TouchConfigSide.fromCode(sideCode); + final TouchConfigType type = TouchConfigType.fromCode(typeCode); + final TouchConfigValue value = TouchConfigValue.fromCode(valueCode); + + if (side == null) { + LOG.warn("Unknown touch side code {}", sideCode); + continue; + } + if (type == null) { + LOG.warn("Unknown touch type code {}", typeCode); + continue; + } + if (value == null) { + LOG.warn("Unknown touch value code {}", valueCode); + continue; + } + + LOG.debug("Got touch config for {} {} = {}", side, type, value); + + eventUpdatePreferences.withPreference( + OppoHeadphonesPreferences.getTouchKey(side, type), + value.name().toLowerCase(Locale.ROOT) + ); + } + return eventUpdatePreferences; + } + + private static GBDeviceEvent parseAncConfig(final byte[] payload) { + final GBDeviceEventUpdatePreferences event = new GBDeviceEventUpdatePreferences(); + + final int typeCode = payload[1] & 0xff; + final int valueCode = payload[3] & 0xff; + + final AncConfigType type = AncConfigType.fromCode(typeCode); + if (type == null) { + LOG.warn("Unknown anc type code {}", typeCode); + return event; + } + + switch (type) { + case MODE: { + final AncConfigValue mode = AncConfigValue.fromCode(valueCode); + if (mode == null) { + LOG.warn("Unknown anc mode code {}", valueCode); + break; + } + + LOG.debug("Got anc config for {} = {}", type, mode); + event.withPreference(OppoHeadphonesPreferences.ANC_SELECTOR, mode.getPrefId()); + break; + } + case TOUCH_CYCLE_MODES: { + final EnumSet modes = AncConfigValue.fromMask(valueCode); + if (!modes.isEmpty()) { + final Set prefIds = AncConfigValue.toPrefIds(modes); + + LOG.debug("Got anc config for {} = {}", type, prefIds); + event.withPreference(OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES, prefIds); + } + break; + } + default: { + LOG.debug("Unknown anc type code {}", typeCode); + break; + } + } + return event; + } + @Override public byte[] encodeFirmwareVersionReq() { return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]); @@ -325,11 +527,11 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { public byte[] encodeSendConfiguration(final String config) { final DevicePrefs prefs = getDevicePrefs(); - if (config.startsWith("oppo_touch__")) { + if (config.startsWith(OppoHeadphonesPreferences.TOUCH_PREFIX)) { final String[] parts = config.split("__"); final TouchConfigSide side = TouchConfigSide.valueOf(parts[1].toUpperCase(Locale.ROOT)); final TouchConfigType type = TouchConfigType.valueOf(parts[2].toUpperCase(Locale.ROOT)); - final String valueCode = prefs.getString(OppoHeadphonesPreferences.getKey(side, type), null); + final String valueCode = prefs.getString(OppoHeadphonesPreferences.getTouchKey(side, type), null); if (valueCode == null) { LOG.warn("Failed to get touch option value for {}/{}", side, type); return super.encodeSendConfiguration(config); @@ -338,14 +540,48 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT)); LOG.debug("Sending {} {} = {}", side, type, value); + return encodeTouchConfigSet(side, type, value); + } - final ByteBuffer buf = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN); - buf.put((byte) 0x01); - buf.put((byte) side.getCode()); - buf.putShort((short) type.getCode()); - buf.put((byte) value.getCode()); + if (config.equals(OppoHeadphonesPreferences.LDAC)) { + final boolean value = prefs.getBoolean(OppoHeadphonesPreferences.LDAC, false); + LOG.debug("Sending ldac = {}", value); + return encodeLdacSet(value); + } - return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array()); + if (config.equals(OppoHeadphonesPreferences.MULTIPOINT)) { + final boolean value = prefs.getBoolean(OppoHeadphonesPreferences.MULTIPOINT, false); + LOG.debug("Sending multipoint = {}", value); + return encodeMultipointSet(value); + } + + if (config.equals(OppoHeadphonesPreferences.GAME_MODE)) { + final boolean value = prefs.getBoolean(OppoHeadphonesPreferences.GAME_MODE, false); + LOG.debug("Sending game mode = {}", value); + return encodeGameModeSet(value); + } + + if (config.equals(OppoHeadphonesPreferences.ANC_SELECTOR)) { + final String prefId = prefs.getString(OppoHeadphonesPreferences.ANC_SELECTOR, "0"); + AncConfigValue mode = AncConfigValue.fromPrefId(prefId); + if (mode == null) { + LOG.debug("Unknown ANC prefId = \"{}\"", prefId); + mode = AncConfigValue.OFF; + } + LOG.debug("Sending ANC mode = {}", mode); + return encodeAncModeSet(mode); + } + + if (config.equals(OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES)) { + final Set prefIds = prefs.getStringSet(OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES, Collections.emptySet()); + final EnumSet modes = AncConfigValue.fromPrefIds(prefIds); + if (modes.size() < 2) { + LOG.warn("ANC cycle must contain at least 2 modes. Current selection: {}", modes); + return super.encodeSendConfiguration(config); + } + + LOG.debug("Sending ANC touch cycle modes = {}", modes); + return encodeAncTouchCycleModesSet(modes); } return super.encodeSendConfiguration(config); @@ -355,10 +591,104 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol { return encodeMessage(OppoCommand.BATTERY_REQ, new byte[0]); } - public byte[] encodeConfigurationReq() { + private byte[] encodeTouchConfigSet(final TouchConfigSide side, final TouchConfigType type, final TouchConfigValue value) { + final ByteBuffer buf = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 0x01); + buf.put((byte) side.getCode()); + buf.putShort((short) type.getCode()); + buf.put((byte) value.getCode()); + return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array()); + } + + public byte[] encodeTouchConfigReq() { return encodeMessage(OppoCommand.TOUCH_CONFIG_REQ, new byte[]{0x02, 0x03, 0x01}); } + public byte[] encodeLdacSet(final boolean enable) { + final byte[] payload = new byte[] { + (byte) (enable ? 0x01 : 0x00) + }; + return encodeMiscConfigSet(MiscConfigType.LDAC, payload); + } + + public byte[] encodeGameModeSet(final boolean enable) { + final byte[] payload = new byte[] { + (byte) (enable ? 0x01 : 0x00) + }; + return encodeMiscConfigSet(MiscConfigType.GAME_MODE, payload); + } + + public byte[] encodeMultipointSet(final boolean enable) { + final byte[] payload = new byte[] { + (byte) (enable ? 0x01 : 0x00) + }; + return encodeMiscConfigSet(MiscConfigType.MULTIPOINT, payload); + } + + private byte[] encodeMiscConfigSet(final MiscConfigType type, final byte[] value) { + final byte[] payload = new byte[1 + value.length]; + payload[0] = (byte) type.getCode(); + System.arraycopy(value, 0, payload, 1, value.length); + return encodeMessage(OppoCommand.MISC_CONFIG_SET, payload); + } + + public byte[] encodeMiscConfigReq(List types) { + if (types == null || types.isEmpty()) { + return encodeMessage(OppoCommand.MISC_CONFIG_REQ, new byte[]{0x00}); + } + + byte[] payload = new byte[1 + types.size()]; + payload[0] = (byte) types.size(); + + for (int i = 0; i < types.size(); i++) { + payload[i + 1] = (byte) types.get(i).getCode(); + } + return encodeMessage(OppoCommand.MISC_CONFIG_REQ, payload); + } + + public byte[] encodeAncModeSet(final AncConfigValue mode) { + final byte[] payload = new byte[] { + (byte) AncConfigType.MODE.getCode(), + (byte) 0x01, + (byte) mode.getCode() + }; + return encodeMessage(OppoCommand.ANC_CONFIG_SET, payload); + } + + public byte[] encodeAncTouchCycleModesSet(final EnumSet modes) { + final int mask = AncConfigValue.toMask(modes); + final byte[] payload = new byte[] { + (byte) AncConfigType.TOUCH_CYCLE_MODES.getCode(), + (byte) 0x01, + (byte) mask + }; + return encodeMessage(OppoCommand.ANC_CONFIG_SET, payload); + } + + public byte[] encodeAncConfigReq(final AncConfigType type) { + final byte[] payload = new byte[] { + (byte) type.getCode(), + (byte) 0x01, + }; + return encodeMessage(OppoCommand.ANC_CONFIG_REQ, payload); + } + + public byte[] encodeSubscriptionSet(@NonNull final EnumSet subscriptions) { + if (subscriptions.isEmpty()) { + throw new IllegalArgumentException("Subscription list cannot be empty"); + } + + byte[] payload = new byte[1 + subscriptions.size()]; + payload[0] = (byte) 0x09; + + int i = 1; + for (SubscriptionType type : subscriptions) { + payload[i++] = (byte) type.getCode(); + } + + return encodeMessage(OppoCommand.SUBSCRIPTION_SET, payload); + } + private byte[] encodeMessage(final OppoCommand command, final byte[] payload) { final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN); buf.put(CMD_PREAMBLE); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesSupport.java index 1def3b16d0..d5573eb98d 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesSupport.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/OppoHeadphonesSupport.java @@ -22,11 +22,18 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.UUID; +import java.util.List; +import java.util.ArrayList; +import java.util.EnumSet; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractHeadphoneSerialDeviceSupportV2; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.MiscConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.AncConfigType; +import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.SubscriptionType; +import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesCoordinator; public class OppoHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupportV2 { private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesSupport.class); @@ -69,8 +76,36 @@ public class OppoHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupportV @Override protected TransactionBuilder initializeDevice(final TransactionBuilder builder) { + + final OppoHeadphonesCoordinator coordinator = (OppoHeadphonesCoordinator) getDevice().getDeviceCoordinator(); + final List supportedMiscConfigs = new ArrayList<>(); + final EnumSet supportedSubscriptions = EnumSet.of(SubscriptionType.BATTERY); + if (coordinator.supportsLdac(getDevice())) { + supportedMiscConfigs.add(MiscConfigType.LDAC); + } + + if (coordinator.supportsMultipoint(getDevice())) { + supportedMiscConfigs.add(MiscConfigType.MULTIPOINT); + } + + if (coordinator.supportsGameMode(getDevice())) { + supportedMiscConfigs.add(MiscConfigType.GAME_MODE); + supportedSubscriptions.add(SubscriptionType.GAME_MODE); + } + + if (coordinator.supportsAnc(getDevice())) { + builder.write(mDeviceProtocol.encodeAncConfigReq(AncConfigType.MODE)); + builder.write(mDeviceProtocol.encodeAncConfigReq(AncConfigType.TOUCH_CYCLE_MODES)); + supportedSubscriptions.add(SubscriptionType.ANC_SELECTOR); + } + + if (!supportedMiscConfigs.isEmpty()) { + builder.write(mDeviceProtocol.encodeMiscConfigReq(supportedMiscConfigs)); + } + + builder.write(mDeviceProtocol.encodeSubscriptionSet(supportedSubscriptions)); + builder.write(mDeviceProtocol.encodeTouchConfigReq()); builder.write(mDeviceProtocol.encodeFirmwareVersionReq()); - builder.write(mDeviceProtocol.encodeConfigurationReq()); builder.write(mDeviceProtocol.encodeBatteryReq()); builder.setDeviceState(GBDevice.State.INITIALIZED); scheduleBatteryRequestRetry(); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigType.java new file mode 100644 index 0000000000..b176da6966 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigType.java @@ -0,0 +1,46 @@ +/* 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.service.devices.oppo.commands; + +import androidx.annotation.Nullable; + +public enum AncConfigType { + MODE(0x01), + TOUCH_CYCLE_MODES(0x02), + ; + + private final int code; + + AncConfigType(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + @Nullable + public static AncConfigType fromCode(final int code) { + for (final AncConfigType param : AncConfigType.values()) { + if (param.code == code) { + return param; + } + } + + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigValue.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigValue.java new file mode 100644 index 0000000000..54dd7f5a47 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/AncConfigValue.java @@ -0,0 +1,107 @@ +/* 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.service.devices.oppo.commands; + +import java.lang.Iterable; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Set; + +import androidx.annotation.Nullable; + +public enum AncConfigValue { + OFF(0x01, "0"), + TRANSPARENCY(0x02, "2"), + ON(0x08, "1"); + ; + + private final int code; + private final String prefId; + + AncConfigValue(final int code, final String prefId) { + this.code = code; + this.prefId = prefId; + } + + public int getCode() { + return code; + } + + @Nullable + public static AncConfigValue fromCode(final int code) { + for (final AncConfigValue param : AncConfigValue.values()) { + if (param.code == code) { + return param; + } + } + + return null; + } + + public String getPrefId() { + return prefId; + } + + @Nullable + public static AncConfigValue fromPrefId(final String prefId) { + for (final AncConfigValue param : AncConfigValue.values()) { + if (prefId.equals(param.prefId)) { + return param; + } + } + + return null; + } + + public static int toMask(Iterable modes) { + int mask = 0; + for (AncConfigValue mode : modes) { + mask |= mode.getCode(); + } + return mask; + } + + public static EnumSet fromMask(int mask) { + EnumSet modes = EnumSet.noneOf(AncConfigValue.class); + for (AncConfigValue mode : AncConfigValue.values()) { + if ((mask & mode.getCode()) == mode.getCode()) { + modes.add(mode); + } + } + + return modes; + } + + public static Set toPrefIds(Iterable modes) { + Set prefIds = new HashSet<>(); + for (AncConfigValue mode : modes) { + prefIds.add(mode.getPrefId()); + } + return prefIds; + } + + public static EnumSet fromPrefIds(Iterable prefIds) { + EnumSet modes = EnumSet.noneOf(AncConfigValue.class); + for (String prefId : prefIds) { + AncConfigValue mode = fromPrefId(prefId); + if (mode != null) { + modes.add(mode); + } + } + return modes; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/MiscConfigType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/MiscConfigType.java new file mode 100644 index 0000000000..3c245fcd3b --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/MiscConfigType.java @@ -0,0 +1,48 @@ +/* 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.service.devices.oppo.commands; + +import androidx.annotation.Nullable; + +public enum MiscConfigType { + LDAC(0x18), + GAME_MODE(0x06), + MULTIPOINT(0x11), + FIND_PHONE(0x26), + ; + + private final int code; + + MiscConfigType(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + @Nullable + public static MiscConfigType fromCode(final int code) { + for (final MiscConfigType param : MiscConfigType.values()) { + if (param.code == code) { + return param; + } + } + + return null; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/OppoCommand.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/OppoCommand.java index 0d594c835e..30d5ff293e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/OppoCommand.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/OppoCommand.java @@ -21,7 +21,9 @@ import androidx.annotation.Nullable; public enum OppoCommand { BATTERY_REQ(0x0106), BATTERY_RET(0x8106), - DEVICE_INFO(0x0204), + SUBSCRIPTION_SET(0x0205), + SUBSCRIPTION_ACK(0x8205), + SUBSCRIPTION_RET(0x0204), FIRMWARE_GET(0x0105), FIRMWARE_RET(0x8105), TOUCH_CONFIG_REQ(0x0108), @@ -30,6 +32,14 @@ public enum OppoCommand { TOUCH_CONFIG_ACK(0x8401), FIND_DEVICE_REQ(0x0400), FIND_DEVICE_ACK(0x8400), + MISC_CONFIG_SET(0x0403), + MISC_CONFIG_REQ(0x010d), + MISC_CONFIG_ACK(0x8403), + MISC_CONFIG_RET(0x810d), + ANC_CONFIG_SET(0x0404), + ANC_CONFIG_REQ(0x010c), + ANC_CONFIG_ACK(0x8404), + ANC_CONFIG_RET(0x810c), ; private final short code; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/SubscriptionType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/SubscriptionType.java new file mode 100644 index 0000000000..17b76a65cd --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/oppo/commands/SubscriptionType.java @@ -0,0 +1,48 @@ +/* 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.service.devices.oppo.commands; + +import androidx.annotation.Nullable; + +public enum SubscriptionType { + BATTERY(0x01), + STATUS(0x02), + ANC_SELECTOR(0x03), + GAME_MODE(0x05), + ; + + private final int code; + + SubscriptionType(final int code) { + this.code = code; + } + + public int getCode() { + return code; + } + + @Nullable + public static SubscriptionType fromCode(final int code) { + for (final SubscriptionType param : SubscriptionType.values()) { + if (param.code == code) { + return param; + } + } + + return null; + } +} diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 8e716198a4..071f45c6d5 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -4523,7 +4523,7 @@ @string/pref_title_touch_voice_assistant @string/pref_title_touch_voice_assistant @string/prefs_game_mode - @string/prefs_noise_control + @string/moondrop_touch_action_anc_mode diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e2dd01ff3c..05e7f99dbe 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -3229,6 +3229,7 @@ Realme Buds T200 Realme Buds T300 Realme Buds Air 5 Pro + Realme Buds Air 6 Pro Galaxy Buds Galaxy Buds Live Galaxy Buds Pro @@ -5042,4 +5043,5 @@ Endurain Wanderer Upload GPX to Wanderer + Please select at least %1$d option diff --git a/app/src/main/res/xml/devicesettings_oppo_headphones_game_mode.xml b/app/src/main/res/xml/devicesettings_oppo_headphones_game_mode.xml new file mode 100644 index 0000000000..aa8b0430ec --- /dev/null +++ b/app/src/main/res/xml/devicesettings_oppo_headphones_game_mode.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_oppo_headphones_multipoint.xml b/app/src/main/res/xml/devicesettings_oppo_headphones_multipoint.xml new file mode 100644 index 0000000000..2fb23735ec --- /dev/null +++ b/app/src/main/res/xml/devicesettings_oppo_headphones_multipoint.xml @@ -0,0 +1,9 @@ + + + + diff --git a/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options_anc.xml b/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options_anc.xml new file mode 100644 index 0000000000..cdbf5de2d8 --- /dev/null +++ b/app/src/main/res/xml/devicesettings_oppo_headphones_touch_options_anc.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/BluetoothNameTest.java b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/BluetoothNameTest.java index 0b1186cd62..11a8689b80 100644 --- a/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/BluetoothNameTest.java +++ b/app/src/test/java/nodomain/freeyourgadget/gadgetbridge/devices/BluetoothNameTest.java @@ -164,6 +164,7 @@ public class BluetoothNameTest extends TestBase { put("realme Buds T110", DeviceType.REALME_BUDS_T110); put("realme Buds T200", DeviceType.REALME_BUDS_T200); // #6258 put("realme Buds T300", DeviceType.REALME_BUDS_T300); // #6258 + put("realme Buds Air6 Pro", DeviceType.REALME_BUDS_AIR_6_PRO); put("Redmi Smart Band 2 604D", DeviceType.REDMISMARTBAND2); // #3274 put("vívosmart 5", DeviceType.GARMIN_VIVOSMART_5); // #3269 put("Instinct Crossover", DeviceType.GARMIN_INSTINCT_CROSSOVER); // #3252