Merge pull request 'add support ldac, multipoint and game mode options for realme Buds T200' (#6305)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/6305
This commit is contained in:
José Rebelo
2026-06-20 17:22:37 +02:00
19 changed files with 1035 additions and 132 deletions
@@ -76,12 +76,39 @@ public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceC
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS); settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones); 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; return settings;
} }
@Override @Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) { 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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions(); protected abstract Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions();
@@ -90,4 +117,20 @@ public abstract class OppoHeadphonesCoordinator extends AbstractBLClassicDeviceC
public final DeviceKind getDeviceKind(@NonNull GBDevice device) { public final DeviceKind getDeviceKind(@NonNull GBDevice device) {
return DeviceKind.EARBUDS; 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;
}
} }
@@ -22,10 +22,19 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchC
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
public class OppoHeadphonesPreferences { 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( return String.format(
Locale.ROOT, Locale.ROOT,
"oppo_touch__%s__%s", "%s%s__%s",
TOUCH_PREFIX,
side.name().toLowerCase(Locale.ROOT), side.name().toLowerCase(Locale.ROOT),
type.name().toLowerCase(Locale.ROOT) type.name().toLowerCase(Locale.ROOT)
); );
@@ -18,9 +18,13 @@ package nodomain.freeyourgadget.gadgetbridge.devices.oppo;
import android.os.Parcel; import android.os.Parcel;
import android.util.Pair; import android.util.Pair;
import android.content.Context;
import androidx.preference.ListPreference; import androidx.preference.ListPreference;
import androidx.preference.Preference; import androidx.preference.Preference;
import androidx.preference.MultiSelectListPreference;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
@@ -40,10 +44,19 @@ import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer { public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
private final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions; private final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions;
private final boolean supportsLdac;
private final boolean supportsMultipoint;
private final boolean supportsGameMode;
private final boolean supportsAnc;
public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() { public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() {
@Override @Override
public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) { 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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions = new LinkedHashMap<>(); final Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> touchOptions = new LinkedHashMap<>();
final int numOptions = in.readInt(); final int numOptions = in.readInt();
for (int i = 0; i < numOptions; i++) { for (int i = 0; i < numOptions; i++) {
@@ -53,7 +66,8 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
in.readList(values, TouchConfigValue.class.getClassLoader()); in.readList(values, TouchConfigValue.class.getClassLoader());
touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values); touchOptions.put(Pair.create(touchConfigSide, touchConfigType), values);
} }
return new OppoHeadphonesSettingsCustomizer(touchOptions);
return new OppoHeadphonesSettingsCustomizer(touchOptions, supportsLdac, supportsMultipoint, supportsGameMode, supportsAnc);
} }
@Override @Override
@@ -62,12 +76,32 @@ 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, final boolean supportsAnc) {
this.touchOptions = touchOptions; this.touchOptions = touchOptions;
this.supportsLdac = supportsLdac;
this.supportsMultipoint = supportsMultipoint;
this.supportsGameMode = supportsGameMode;
this.supportsAnc = supportsAnc;
} }
@Override @Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) { 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<String> 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 @Override
@@ -75,6 +109,12 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
final Set<TouchConfigSide> knownSides = new HashSet<>(); final Set<TouchConfigSide> knownSides = new HashSet<>();
final Set<TouchConfigType> knownTypes = 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);
this.addPreferenceHandler(handler, OppoHeadphonesPreferences.ANC_SELECTOR, supportsAnc);
this.addPreferenceHandler(handler, OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES, supportsAnc);
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) { for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
final TouchConfigSide side = e.getKey().first; final TouchConfigSide side = e.getKey().first;
final TouchConfigType type = e.getKey().second; final TouchConfigType type = e.getKey().second;
@@ -83,7 +123,7 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
knownSides.add(side); knownSides.add(side);
knownTypes.add(type); knownTypes.add(type);
final String key = OppoHeadphonesPreferences.getKey(side, type); final String key = OppoHeadphonesPreferences.getTouchKey(side, type);
final ListPreference pref = handler.findPreference(key); final ListPreference pref = handler.findPreference(key);
if (pref == null) { if (pref == null) {
continue; continue;
@@ -120,7 +160,7 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
for (final TouchConfigType type : TouchConfigType.values()) { for (final TouchConfigType type : TouchConfigType.values()) {
if (!knownTypes.contains(type)) { if (!knownTypes.contains(type)) {
final String key = OppoHeadphonesPreferences.getKey(side, type); final String key = OppoHeadphonesPreferences.getTouchKey(side, type);
final Preference pref = handler.findPreference(key); final Preference pref = handler.findPreference(key);
if (pref != null) { if (pref != null) {
pref.setVisible(false); 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 @Override
public Set<String> getPreferenceKeysWithSummary() { public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet(); return Collections.emptySet();
@@ -142,6 +192,11 @@ public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsC
@Override @Override
public void writeToParcel(final Parcel dest, final int flags) { 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()); dest.writeInt(touchOptions.size());
for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) { for (final Map.Entry<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
dest.writeString(e.getKey().first.name()); dest.writeString(e.getKey().first.name());
@@ -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 <https://www.gnu.org/licenses/>. */
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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
return new LinkedHashMap<>() {{
final List<TouchConfigValue> 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);
}};
}
}
@@ -67,6 +67,26 @@ public class RealmeBudsT200Coordinator extends OppoHeadphonesCoordinator {
return true; 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 @Override
protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() { protected Map<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
return new LinkedHashMap<>() {{ return new LinkedHashMap<>() {{
@@ -399,6 +399,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.polar.PolarH9DeviceCoordinat
import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.qhybrid.QHybridCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsAir5ProCoordinator; 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.RealmeBudsT100Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT110Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT200Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.realme.RealmeBudsT200Coordinator;
@@ -922,6 +923,7 @@ public enum DeviceType {
REALME_BUDS_T200(RealmeBudsT200Coordinator.class), REALME_BUDS_T200(RealmeBudsT200Coordinator.class),
REALME_BUDS_T300(RealmeBudsT300Coordinator.class), REALME_BUDS_T300(RealmeBudsT300Coordinator.class),
REALME_BUDS_AIR_5_PRO(RealmeBudsAir5ProCoordinator.class), REALME_BUDS_AIR_5_PRO(RealmeBudsAir5ProCoordinator.class),
REALME_BUDS_AIR_6_PRO(RealmeBudsAir6ProCoordinator.class),
SOFLOW_SO6(SoFlowCoordinator.class), SOFLOW_SO6(SoFlowCoordinator.class),
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class), WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
SONY_WENA_3(SonyWena3Coordinator.class), SONY_WENA_3(SonyWena3Coordinator.class),
@@ -26,7 +26,13 @@ import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.Locale; 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.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent; 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.TouchConfigSide;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType; 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.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.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs; import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
@@ -137,19 +147,11 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
events.addAll(parseBattery(payload)); events.addAll(parseBattery(payload));
break; break;
} }
case DEVICE_INFO: { case SUBSCRIPTION_ACK:
switch (payload[0]) { LOG.debug("Got subscription ack, return={}", StringUtils.bytesToHex(payload));
case 1: // battery break;
events.addAll(parseBattery(payload)); case SUBSCRIPTION_RET: {
break; events.addAll(parseSubscription(payload));
case 2: // status
LOG.debug("Got status");
// TODO handle
break;
default:
LOG.warn("Unknown device info {}", payload[0]);
}
break; break;
} }
case FIRMWARE_RET: { case FIRMWARE_RET: {
@@ -157,126 +159,67 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
LOG.warn("Unexpected firmware ret {}", payload[0]); LOG.warn("Unexpected firmware ret {}", payload[0]);
break; break;
} }
events.addAll(parseFirmware(payload));
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<String> 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);
break; break;
} }
case FIND_DEVICE_ACK: { case FIND_DEVICE_ACK: {
LOG.debug("Got find device ack, status={}", payload[0]); LOG.debug("Got find device ack, status={}", payload[0]);
break; 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: { case TOUCH_CONFIG_RET: {
if (payload[0] != 0) { if (payload[0] != 0) {
LOG.warn("Unknown config ret {}", payload[0]); LOG.warn("Unknown touch config ret {}", payload[0]);
break; break;
} }
if ((payload.length - 2) % 4 != 0) { 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; break;
} }
final GBDeviceEventUpdatePreferences eventUpdatePreferences = new GBDeviceEventUpdatePreferences(); events.add(parseTouchConfig(payload));
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);
break; break;
} }
case TOUCH_CONFIG_ACK: { case TOUCH_CONFIG_ACK: {
LOG.debug("Got config ack, status={}", payload[0]); LOG.debug("Got touch config ack, status={}", payload[0]);
break; 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); LOG.warn("Unhandled command {}", command);
}
} }
return events; return events;
@@ -297,6 +240,10 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
} }
final int batteryLevel = payload[i + 1] & 0x7f; 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; final BatteryState batteryState = (payload[i + 1] & 0x80) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState); LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState);
@@ -308,9 +255,264 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
events.add(eventBatteryInfo); events.add(eventBatteryInfo);
} }
List<Integer> 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; return events;
} }
private static List<GBDeviceEvent> parseFirmware(final byte[] payload) {
final List<GBDeviceEvent> 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<String> 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<GBDeviceEvent> parseSubscription(final byte[] payload) {
final List<GBDeviceEvent> 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<AncConfigValue> modes = AncConfigValue.fromMask(valueCode);
if (!modes.isEmpty()) {
final Set<String> 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 @Override
public byte[] encodeFirmwareVersionReq() { public byte[] encodeFirmwareVersionReq() {
return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]); return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]);
@@ -325,11 +527,11 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
public byte[] encodeSendConfiguration(final String config) { public byte[] encodeSendConfiguration(final String config) {
final DevicePrefs prefs = getDevicePrefs(); final DevicePrefs prefs = getDevicePrefs();
if (config.startsWith("oppo_touch__")) { if (config.startsWith(OppoHeadphonesPreferences.TOUCH_PREFIX)) {
final String[] parts = config.split("__"); final String[] parts = config.split("__");
final TouchConfigSide side = TouchConfigSide.valueOf(parts[1].toUpperCase(Locale.ROOT)); final TouchConfigSide side = TouchConfigSide.valueOf(parts[1].toUpperCase(Locale.ROOT));
final TouchConfigType type = TouchConfigType.valueOf(parts[2].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) { if (valueCode == null) {
LOG.warn("Failed to get touch option value for {}/{}", side, type); LOG.warn("Failed to get touch option value for {}/{}", side, type);
return super.encodeSendConfiguration(config); return super.encodeSendConfiguration(config);
@@ -338,14 +540,48 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT)); final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT));
LOG.debug("Sending {} {} = {}", side, type, value); LOG.debug("Sending {} {} = {}", side, type, value);
return encodeTouchConfigSet(side, type, value);
}
final ByteBuffer buf = ByteBuffer.allocate(5).order(ByteOrder.LITTLE_ENDIAN); if (config.equals(OppoHeadphonesPreferences.LDAC)) {
buf.put((byte) 0x01); final boolean value = prefs.getBoolean(OppoHeadphonesPreferences.LDAC, false);
buf.put((byte) side.getCode()); LOG.debug("Sending ldac = {}", value);
buf.putShort((short) type.getCode()); return encodeLdacSet(value);
buf.put((byte) value.getCode()); }
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<String> prefIds = prefs.getStringSet(OppoHeadphonesPreferences.ANC_TOUCH_CYCLE_MODES, Collections.emptySet());
final EnumSet<AncConfigValue> 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); return super.encodeSendConfiguration(config);
@@ -355,10 +591,104 @@ public class OppoHeadphonesProtocol extends GBDeviceProtocol {
return encodeMessage(OppoCommand.BATTERY_REQ, new byte[0]); 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}); 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<MiscConfigType> 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<AncConfigValue> 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<SubscriptionType> 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) { private byte[] encodeMessage(final OppoCommand command, final byte[] payload) {
final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN); final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN);
buf.put(CMD_PREAMBLE); buf.put(CMD_PREAMBLE);
@@ -22,11 +22,18 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import java.util.UUID; 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.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder; import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractHeadphoneSerialDeviceSupportV2; 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<OppoHeadphonesProtocol> { public class OppoHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupportV2<OppoHeadphonesProtocol> {
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesSupport.class); private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesSupport.class);
@@ -69,8 +76,36 @@ public class OppoHeadphonesSupport extends AbstractHeadphoneSerialDeviceSupportV
@Override @Override
protected TransactionBuilder initializeDevice(final TransactionBuilder builder) { protected TransactionBuilder initializeDevice(final TransactionBuilder builder) {
final OppoHeadphonesCoordinator coordinator = (OppoHeadphonesCoordinator) getDevice().getDeviceCoordinator();
final List<MiscConfigType> supportedMiscConfigs = new ArrayList<>();
final EnumSet<SubscriptionType> 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.encodeFirmwareVersionReq());
builder.write(mDeviceProtocol.encodeConfigurationReq());
builder.write(mDeviceProtocol.encodeBatteryReq()); builder.write(mDeviceProtocol.encodeBatteryReq());
builder.setDeviceState(GBDevice.State.INITIALIZED); builder.setDeviceState(GBDevice.State.INITIALIZED);
scheduleBatteryRequestRetry(); scheduleBatteryRequestRetry();
@@ -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 <https://www.gnu.org/licenses/>. */
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;
}
}
@@ -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 <https://www.gnu.org/licenses/>. */
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<AncConfigValue> modes) {
int mask = 0;
for (AncConfigValue mode : modes) {
mask |= mode.getCode();
}
return mask;
}
public static EnumSet<AncConfigValue> fromMask(int mask) {
EnumSet<AncConfigValue> modes = EnumSet.noneOf(AncConfigValue.class);
for (AncConfigValue mode : AncConfigValue.values()) {
if ((mask & mode.getCode()) == mode.getCode()) {
modes.add(mode);
}
}
return modes;
}
public static Set<String> toPrefIds(Iterable<AncConfigValue> modes) {
Set<String> prefIds = new HashSet<>();
for (AncConfigValue mode : modes) {
prefIds.add(mode.getPrefId());
}
return prefIds;
}
public static EnumSet<AncConfigValue> fromPrefIds(Iterable<String> prefIds) {
EnumSet<AncConfigValue> modes = EnumSet.noneOf(AncConfigValue.class);
for (String prefId : prefIds) {
AncConfigValue mode = fromPrefId(prefId);
if (mode != null) {
modes.add(mode);
}
}
return modes;
}
}
@@ -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 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;
}
}
@@ -21,7 +21,9 @@ import androidx.annotation.Nullable;
public enum OppoCommand { public enum OppoCommand {
BATTERY_REQ(0x0106), BATTERY_REQ(0x0106),
BATTERY_RET(0x8106), BATTERY_RET(0x8106),
DEVICE_INFO(0x0204), SUBSCRIPTION_SET(0x0205),
SUBSCRIPTION_ACK(0x8205),
SUBSCRIPTION_RET(0x0204),
FIRMWARE_GET(0x0105), FIRMWARE_GET(0x0105),
FIRMWARE_RET(0x8105), FIRMWARE_RET(0x8105),
TOUCH_CONFIG_REQ(0x0108), TOUCH_CONFIG_REQ(0x0108),
@@ -30,6 +32,14 @@ public enum OppoCommand {
TOUCH_CONFIG_ACK(0x8401), TOUCH_CONFIG_ACK(0x8401),
FIND_DEVICE_REQ(0x0400), FIND_DEVICE_REQ(0x0400),
FIND_DEVICE_ACK(0x8400), 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; 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 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;
}
}
+1 -1
View File
@@ -4523,7 +4523,7 @@
<item>@string/pref_title_touch_voice_assistant</item> <item>@string/pref_title_touch_voice_assistant</item>
<item>@string/pref_title_touch_voice_assistant</item> <item>@string/pref_title_touch_voice_assistant</item>
<item>@string/prefs_game_mode</item> <item>@string/prefs_game_mode</item>
<item>@string/prefs_noise_control</item> <item>@string/moondrop_touch_action_anc_mode</item>
</string-array> </string-array>
<string-array name="oppo_touch_tap_values"> <string-array name="oppo_touch_tap_values">
+2
View File
@@ -3229,6 +3229,7 @@
<string name="devicetype_realme_buds_t200" translatable="false">Realme Buds T200</string> <string name="devicetype_realme_buds_t200" translatable="false">Realme Buds T200</string>
<string name="devicetype_realme_buds_t300" translatable="false">Realme Buds T300</string> <string name="devicetype_realme_buds_t300" translatable="false">Realme Buds T300</string>
<string name="devicetype_realme_buds_air_5_pro" translatable="false">Realme Buds Air 5 Pro</string> <string name="devicetype_realme_buds_air_5_pro" translatable="false">Realme Buds Air 5 Pro</string>
<string name="devicetype_realme_buds_air_6_pro" translatable="false">Realme Buds Air 6 Pro</string>
<string name="devicetype_galaxybuds" translatable="false">Galaxy Buds</string> <string name="devicetype_galaxybuds" translatable="false">Galaxy Buds</string>
<string name="devicetype_galaxybuds_live" translatable="false">Galaxy Buds Live</string> <string name="devicetype_galaxybuds_live" translatable="false">Galaxy Buds Live</string>
<string name="devicetype_galaxybuds_pro" translatable="false">Galaxy Buds Pro</string> <string name="devicetype_galaxybuds_pro" translatable="false">Galaxy Buds Pro</string>
@@ -5042,4 +5043,5 @@
<string name="pref_category_endurain" translatable="false">Endurain</string> <string name="pref_category_endurain" translatable="false">Endurain</string>
<string name="pref_category_wanderer" translatable="false">Wanderer</string> <string name="pref_category_wanderer" translatable="false">Wanderer</string>
<string name="activity_detail_upload_to_wanderer">Upload GPX to Wanderer</string> <string name="activity_detail_upload_to_wanderer">Upload GPX to Wanderer</string>
<string name="select_at_least_option">Please select at least %1$d option</string>
</resources> </resources>
@@ -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>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceCategory
android:key="oppo_touch_header_anc"
android:title="@string/prefs_active_noise_cancelling"
app:iconSpaceReserved="false">
<MultiSelectListPreference
android:key="oppo_touch_anc_cycle_modes"
android:title="@string/prefs_noise_modes_to_cycle"
android:icon="@drawable/ic_sync"
android:entries="@array/onemore_noise_control_modes"
android:entryValues="@array/onemore_noise_control_values"
android:defaultValue="@array/onemore_noise_control_values" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -164,6 +164,7 @@ public class BluetoothNameTest extends TestBase {
put("realme Buds T110", DeviceType.REALME_BUDS_T110); put("realme Buds T110", DeviceType.REALME_BUDS_T110);
put("realme Buds T200", DeviceType.REALME_BUDS_T200); // #6258 put("realme Buds T200", DeviceType.REALME_BUDS_T200); // #6258
put("realme Buds T300", DeviceType.REALME_BUDS_T300); // #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("Redmi Smart Band 2 604D", DeviceType.REDMISMARTBAND2); // #3274
put("vívosmart 5", DeviceType.GARMIN_VIVOSMART_5); // #3269 put("vívosmart 5", DeviceType.GARMIN_VIVOSMART_5); // #3269
put("Instinct Crossover", DeviceType.GARMIN_INSTINCT_CROSSOVER); // #3252 put("Instinct Crossover", DeviceType.GARMIN_INSTINCT_CROSSOVER); // #3252