oppo: add minimal support ldac, multipoint and gamemode

This commit is contained in:
NTeditor
2026-06-20 03:21:54 +03:00
parent beb81e0d65
commit 54f0949f84
8 changed files with 186 additions and 10 deletions
@@ -76,12 +76,25 @@ public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceC
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones);
settings.addRootScreen(DeviceSpecificSettingsScreen.DEVELOPER);
if (this.supportsLdac(device)) {
settings.addSubScreen(DeviceSpecificSettingsScreen.DEVELOPER, R.xml.devicesettings_ldac_toggle);
}
if (this.supportsMultipoint(device)) {
settings.addSubScreen(DeviceSpecificSettingsScreen.DEVELOPER, R.xml.devicesettings_oppo_headphones_multipoint);
}
if (this.supportsGameMode(device)) {
settings.addSubScreen(DeviceSpecificSettingsScreen.DEVELOPER, 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));
}
protected abstract Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions();
@@ -90,4 +103,16 @@ 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;
}
}
@@ -22,10 +22,17 @@ 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 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)
);
@@ -40,10 +40,17 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
private final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions;
private final boolean supportsLdac;
private final boolean supportsMultipoint;
private final boolean supportsGameMode;
public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() {
@Override
public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) {
final boolean supportsLdac = in.readByte() != 0;
final boolean supportsMultipoint = in.readByte() != 0;
final boolean supportsGameMode = in.readByte() != 0;
final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions = new LinkedHashMap<>();
final int numOptions = in.readInt();
for (int i = 0; i < numOptions; i++) {
@@ -53,7 +60,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);
}
@Override
@@ -62,8 +70,11 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
}
};
public OppoHeadphonesSettingsCustomizer(final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions) {
public OppoHeadphonesSettingsCustomizer(final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions, final boolean supportsLdac, final boolean supportsMultipoint, final boolean supportsGameMode) {
this.touchOptions = touchOptions;
this.supportsLdac = supportsLdac;
this.supportsMultipoint = supportsMultipoint;
this.supportsGameMode = supportsGameMode;
}
@Override
@@ -75,6 +86,10 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
final Set<TouchConfigSide> knownSides = new HashSet<>();
final Set<TouchConfigType> knownTypes = new HashSet<>();
this.addPreferenceHandler(handler, OppoHeadphonesPreferences.LDAC, supportsLdac);
this.addPreferenceHandler(handler, OppoHeadphonesPreferences.MULTIPOINT, supportsMultipoint);
this.addPreferenceHandler(handler, OppoHeadphonesPreferences.GAME_MODE, supportsGameMode);
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
final TouchConfigSide side = e.getKey().first;
final TouchConfigType type = e.getKey().second;
@@ -83,7 +98,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 +135,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 +145,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<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
@@ -142,6 +167,10 @@ 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.writeInt(touchOptions.size());
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
dest.writeString(e.getKey().first.name());
@@ -41,6 +41,7 @@ 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.PrefType;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
@@ -262,7 +263,7 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
LOG.debug("Got touch config for {} {} = {}", side, type, value);
eventUpdatePreferences.withPreference(
OppoHeadphonesPreferences.getKey(side, type),
OppoHeadphonesPreferences.getTouchKey(side, type),
value.name().toLowerCase(Locale.ROOT)
);
}
@@ -325,11 +326,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);
@@ -348,6 +349,24 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array());
}
if (config.equals(OppoHeadphonesPreferences.LDAC)) {
final boolean value = prefs.getBoolean(OppoHeadphonesPreferences.LDAC, false);
LOG.debug("Sending ldac = {}", value);
return encodeLdacSet(value);
}
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);
}
return super.encodeSendConfiguration(config);
}
@@ -359,6 +378,34 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
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 encodePrefSet(PrefType.LDAC, payload);
}
public byte[] encodeGameModeSet(final boolean enable) {
final byte[] payload = new byte[] {
(byte) (enable ? 0x01 : 0x00)
};
return encodePrefSet(PrefType.GAME_MODE, payload);
}
public byte[] encodeMultipointSet(final boolean enable) {
final byte[] payload = new byte[] {
(byte) (enable ? 0x01 : 0x00)
};
return encodePrefSet(PrefType.MULTIPOINT, payload);
}
public byte[] encodePrefSet(final PrefType 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.PREF_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);
@@ -30,6 +30,8 @@ public enum OppoCommand {
TOUCH_CONFIG_ACK(0x8401),
FIND_DEVICE_REQ(0x0400),
FIND_DEVICE_ACK(0x8400),
PREF_SET(0x0403),
PREF_ACK(0x8403),
;
private final short code;
@@ -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 <https://www.gnu.org/licenses/>. */
package nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands;
import androidx.annotation.Nullable;
public enum PrefType {
LDAC(0x18),
GAME_MODE(0x06),
MULTIPOINT(0x11),
FIND_PHONE(0x26),
;
private final int code;
PrefType(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
@Nullable
public static PrefType fromCode(final int code) {
for (final PrefType param : PrefType.values()) {
if (param.code == code) {
return param;
}
}
return null;
}
}
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:key="oppo_game_mode"
android:title="@string/prefs_game_mode"
android:icon="@drawable/ic_videogame"
android:defaultValue="false"
android:layout="@layout/preference_checkbox" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreferenceCompat
android:key="oppo_multipoint"
android:title="@string/bluetooth_multipoint_pairing"
android:icon="@drawable/ic_bluetooth_searching"
android:defaultValue="false"
android:layout="@layout/preference_checkbox" />
</androidx.preference.PreferenceScreen>