mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2026-07-31 07:44:24 +02:00
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:
+44
-1
@@ -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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> 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;
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -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)
|
||||
);
|
||||
|
||||
+59
-4
@@ -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<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>() {
|
||||
@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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> 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<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.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<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
|
||||
@@ -75,6 +109,12 @@ 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);
|
||||
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()) {
|
||||
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<String> 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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> e : touchOptions.entrySet()) {
|
||||
dest.writeString(e.getKey().first.name());
|
||||
|
||||
+112
@@ -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);
|
||||
}};
|
||||
}
|
||||
}
|
||||
+20
@@ -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<Pair<TouchConfigSide, TouchConfigType>, List<TouchConfigValue>> getTouchOptions() {
|
||||
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.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),
|
||||
|
||||
+452
-122
@@ -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<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);
|
||||
|
||||
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<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;
|
||||
}
|
||||
|
||||
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
|
||||
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<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);
|
||||
@@ -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<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) {
|
||||
final ByteBuffer buf = ByteBuffer.allocate(9 + payload.length).order(ByteOrder.LITTLE_ENDIAN);
|
||||
buf.put(CMD_PREAMBLE);
|
||||
|
||||
+36
-1
@@ -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<OppoHeadphonesProtocol> {
|
||||
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<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.encodeConfigurationReq());
|
||||
builder.write(mDeviceProtocol.encodeBatteryReq());
|
||||
builder.setDeviceState(GBDevice.State.INITIALIZED);
|
||||
scheduleBatteryRequestRetry();
|
||||
|
||||
+46
@@ -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;
|
||||
}
|
||||
}
|
||||
+107
@@ -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;
|
||||
}
|
||||
}
|
||||
+48
@@ -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;
|
||||
}
|
||||
}
|
||||
+11
-1
@@ -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;
|
||||
|
||||
+48
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -4523,7 +4523,7 @@
|
||||
<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_noise_control</item>
|
||||
<item>@string/moondrop_touch_action_anc_mode</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="oppo_touch_tap_values">
|
||||
|
||||
@@ -3229,6 +3229,7 @@
|
||||
<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_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_live" translatable="false">Galaxy Buds Live</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_wanderer" translatable="false">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>
|
||||
|
||||
@@ -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 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
|
||||
|
||||
Reference in New Issue
Block a user