1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-12-27 19:15:50 +01:00

Oppo Enco Air: Initial support

This commit is contained in:
José Rebelo 2024-11-10 22:18:41 +00:00
parent 7a0e43a4de
commit a72de07d2a
15 changed files with 980 additions and 1 deletions

View File

@ -0,0 +1,108 @@
/* 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.oppo;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.OppoHeadphonesSupport;
public class OppoEncoAirCoordinator extends AbstractBLClassicDeviceCoordinator {
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("OPPO Enco Air", Pattern.LITERAL);
}
@Override
protected void deleteDevice(@NonNull final GBDevice gbDevice, @NonNull final Device device, @NonNull final DaoSession session) throws GBException {
}
@Override
public String getManufacturer() {
return "Oppo";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return OppoHeadphonesSupport.class;
}
@Override
public int getDeviceNameResource() {
return R.string.devicetype_oppo_enco_air;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_nothingear;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_nothingear_disabled;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
public int getBatteryCount() {
return 3;
}
@Override
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
final BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
final BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
final BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_tws_case, R.string.battery_case);
return new BatteryConfig[]{battery1, battery2, battery3};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings settings = new DeviceSpecificSettings();
settings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
settings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_oppo_headphones_touch_options);
settings.addRootScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS);
settings.addSubScreen(DeviceSpecificSettingsScreen.CALLS_AND_NOTIFICATIONS, R.xml.devicesettings_headphones);
return settings;
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new OppoHeadphonesSettingsCustomizer();
}
}

View File

@ -0,0 +1,33 @@
/* 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.oppo;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
public class OppoHeadphonesPreferences {
public static String getKey(final TouchConfigSide side, final TouchConfigType type) {
return String.format(
Locale.ROOT,
"oppo_touch__%s__%s",
side.name().toLowerCase(Locale.ROOT),
type.name().toLowerCase(Locale.ROOT)
);
}
}

View File

@ -0,0 +1,71 @@
/* 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.oppo;
import android.os.Parcel;
import androidx.preference.Preference;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigSide;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.TouchConfigType;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class OppoHeadphonesSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
public static final Creator<OppoHeadphonesSettingsCustomizer> CREATOR = new Creator<OppoHeadphonesSettingsCustomizer>() {
@Override
public OppoHeadphonesSettingsCustomizer createFromParcel(final Parcel in) {
return new OppoHeadphonesSettingsCustomizer();
}
@Override
public OppoHeadphonesSettingsCustomizer[] newArray(final int size) {
return new OppoHeadphonesSettingsCustomizer[size];
}
};
@Override
public void onPreferenceChange(final Preference preference, final DeviceSpecificSettingsHandler handler) {
}
@Override
public void customizeSettings(final DeviceSpecificSettingsHandler handler, final Prefs prefs, final String rootKey) {
for (final TouchConfigSide side : TouchConfigSide.values()) {
for (final TouchConfigType type : TouchConfigType.values()) {
handler.addPreferenceHandlerFor(OppoHeadphonesPreferences.getKey(side, type));
}
}
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(final Parcel dest, final int flags) {
}
}

View File

@ -224,6 +224,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear2Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.EarStickCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoEncoAirCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.qc35.QC35Coordinator;
@ -538,6 +539,7 @@ public enum DeviceType {
FLIPPER_ZERO(FlipperZeroCoordinator.class),
SUPER_CARS(SuperCarsCoordinator.class),
ASTEROIDOS(AsteroidOSDeviceCoordinator.class),
OPPO_ENCO_AIR(OppoEncoAirCoordinator.class),
SOFLOW_SO6(SoFlowCoordinator.class),
WITHINGS_STEEL_HR(WithingsSteelHRDeviceCoordinator.class),
SONY_WENA_3(SonyWena3Coordinator.class),

View File

@ -108,7 +108,6 @@ public abstract class AbstractHeadphoneDeviceSupport extends AbstractSerialDevic
@Override
public void onSendConfiguration(String config) {
LOG.warn("ONSENDCONFIGURATION");
if (PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE.equals(config)) {
final SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
gbTextToSpeech.setAudioFocus(prefs.getBoolean(PREF_SPEAK_NOTIFICATIONS_FOCUS_EXCLUSIVE, false) ?

View File

@ -0,0 +1,75 @@
/* 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;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
public class OppoHeadphonesIoThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesIoThread.class);
private final OppoHeadphonesProtocol mProtocol;
public OppoHeadphonesIoThread(final GBDevice gbDevice,
final Context context,
final OppoHeadphonesProtocol deviceProtocol,
final AbstractSerialDeviceSupport deviceSupport,
final BluetoothAdapter btAdapter) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
this.mProtocol = deviceProtocol;
}
@NonNull
@Override
protected UUID getUuidToConnect(@NonNull final ParcelUuid[] uuids) {
return UUID.fromString("0000079a-d102-11e1-9b23-00025b00a5a5");
}
@Override
protected void initialize() {
write(mProtocol.encodeFirmwareVersionReq());
write(mProtocol.encodeConfigurationReq());
write(mProtocol.encodeBatteryReq());
setUpdateState(GBDevice.State.INITIALIZED);
}
@Override
protected byte[] parseIncoming(final InputStream inStream) throws IOException {
final byte[] buffer = new byte[1048576]; //HUGE read
final int bytes = inStream.read(buffer);
// FIXME: We should buffer this and handle partial commands
LOG.debug("Read {} bytes: {}", bytes, hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
}

View File

@ -0,0 +1,343 @@
/* 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;
import org.apache.commons.lang3.ArrayUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.devices.oppo.OppoHeadphonesPreferences;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions;
import nodomain.freeyourgadget.gadgetbridge.service.devices.oppo.commands.OppoCommand;
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.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
import nodomain.freeyourgadget.gadgetbridge.util.preferences.DevicePrefs;
public class OppoHeadphonesProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(OppoHeadphonesProtocol.class);
public static final byte CMD_PREAMBLE = (byte) 0xaa;
private int seqNum = 0;
protected OppoHeadphonesProtocol(final GBDevice device) {
super(device);
}
@Override
public GBDeviceEvent[] decodeResponse(final byte[] responseData) {
final List<GBDeviceEvent> events = new ArrayList<>();
int i = 0;
while (i < responseData.length) {
if (responseData[i] != CMD_PREAMBLE) {
LOG.warn("Unexpected preamble {}", responseData[i]);
i++;
continue;
}
final int totalLength = BLETypeConversions.toUint16(responseData, i + 1);
if (responseData.length - i < totalLength + 2) {
LOG.error("Got partial response with {} bytes, expected {}", responseData.length - i, totalLength + 2);
break;
}
final byte[] singleResponse = ArrayUtils.subarray(responseData, i, i + totalLength + 3);
events.addAll(handleSingleResponse(singleResponse));
i += totalLength + 2;
}
return events.toArray(new GBDeviceEvent[0]);
}
private static List<GBDeviceEvent> handleSingleResponse(final byte[] responseData) {
final List<GBDeviceEvent> events = new ArrayList<>();
final ByteBuffer responseBuf = ByteBuffer.wrap(responseData).order(ByteOrder.LITTLE_ENDIAN);
final byte preamble = responseBuf.get();
if (preamble != CMD_PREAMBLE) {
LOG.error("Unexpected preamble {}", preamble);
return Collections.emptyList();
}
final byte totalLength = responseBuf.get();
if (responseData.length != totalLength + 2) {
LOG.error("Invalid number of bytes {}, expected {}", responseData.length, totalLength + 2);
return Collections.emptyList();
}
final short zero = responseBuf.getShort();
if (zero != 0) {
LOG.error("Unexpected bytes: {}, expected 0", zero);
return Collections.emptyList();
}
final short code = responseBuf.getShort();
final OppoCommand command = OppoCommand.fromCode(code);
if (command == null) {
LOG.warn("Unknown command code {}", String.format(Locale.ROOT, "0x%04x", code));
return Collections.emptyList();
}
final int seq = responseBuf.get();
final short payloadLength = responseBuf.getShort();
final byte[] payload = new byte[payloadLength];
responseBuf.get(payload);
switch (command) {
case BATTERY_RET: {
if (payload[0] != 0) {
LOG.error("Unknown battery ret {}", payload[0]);
break;
}
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]);
}
break;
}
case FIRMWARE_RET: {
if (payload[0] != 0) {
LOG.warn("Unexpected firmware ret {}", payload[0]);
break;
}
final String fwString = StringUtils.untilNullTerminator(payload, 1);
if (fwString == null) {
LOG.warn("Failed to get firmware string");
break;
}
final String[] parts = fwString.split(",");
if (parts.length % 3 != 0) {
LOG.warn("Fw parts length {} from '{}' is not divisible by 3", parts.length, fwString);
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 String fwVersion = String.join(".", fwVersionParts);
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;
}
case FIND_DEVICE_ACK: {
LOG.debug("Got find device ack, status={}", payload[0]);
break;
}
case TOUCH_CONFIG_RET: {
if (payload[0] != 0) {
LOG.warn("Unknown config ret {}", payload[0]);
break;
}
if ((payload.length - 2) % 4 != 0) {
LOG.warn("Unexpected 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 side code {}", sideCode);
continue;
}
if (type == null) {
LOG.warn("Unknown type code {}", typeCode);
continue;
}
if (value == null) {
LOG.warn("Unknown 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;
}
case TOUCH_CONFIG_ACK: {
LOG.debug("Got config ack, status={}", payload[0]);
break;
}
default:
LOG.warn("Unhandled command {}", command);
}
return events;
}
private static List<GBDeviceEvent> parseBattery(final byte[] payload) {
final List<GBDeviceEvent> events = new ArrayList<>();
final int numBatteries = payload[1] & 0xff;
for (int i = 2; i < payload.length; i += 2) {
if ((payload[i] & 0xff) == 0xff) {
continue;
}
final int batteryIndex = payload[i] - 1;
if (batteryIndex < 0 || batteryIndex > 2) {
LOG.error("Unknown battery index {}", payload[i]);
break;
}
final int batteryLevel = payload[i + 1] & 0x7f;
final BatteryState batteryState = (payload[i + 1] & 0x80) != 0 ? BatteryState.BATTERY_CHARGING : BatteryState.BATTERY_NORMAL;
LOG.debug("Got battery {}: {}%, {}", batteryIndex, batteryLevel, batteryState);
final GBDeviceEventBatteryInfo eventBatteryInfo = new GBDeviceEventBatteryInfo();
eventBatteryInfo.batteryIndex = batteryIndex;
eventBatteryInfo.level = batteryLevel;
eventBatteryInfo.state = batteryState;
events.add(eventBatteryInfo);
}
return events;
}
@Override
public byte[] encodeFirmwareVersionReq() {
return encodeMessage(OppoCommand.FIRMWARE_GET, new byte[0]);
}
@Override
public byte[] encodeFindDevice(final boolean start) {
return encodeMessage(OppoCommand.FIND_DEVICE_REQ, new byte[]{(byte) (start ? 0x01 : 0x00)});
}
@Override
public byte[] encodeSendConfiguration(final String config) {
final DevicePrefs prefs = getDevicePrefs();
if (config.startsWith("oppo_touch__")) {
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);
if (valueCode == null) {
LOG.warn("Failed to get touch option value for {}/{}", side, type);
return super.encodeSendConfiguration(config);
}
final TouchConfigValue value = TouchConfigValue.valueOf(valueCode.toUpperCase(Locale.ROOT));
LOG.debug("Sending {} {} = {}", 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());
return encodeMessage(OppoCommand.TOUCH_CONFIG_SET, buf.array());
}
return super.encodeSendConfiguration(config);
}
public byte[] encodeBatteryReq() {
return encodeMessage(OppoCommand.BATTERY_REQ, new byte[0]);
}
public byte[] encodeConfigurationReq() {
return encodeMessage(OppoCommand.TOUCH_CONFIG_REQ, new byte[]{0x02, 0x03, 0x01});
}
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);
buf.put((byte) (buf.limit() - 2));
buf.put((byte) 0);
buf.put((byte) 0);
buf.putShort(command.getCode());
buf.put((byte) seqNum++);
buf.putShort((short) payload.length);
buf.put(payload);
return buf.array();
}
}

View File

@ -0,0 +1,44 @@
/* 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;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class OppoHeadphonesSupport extends AbstractHeadphoneDeviceSupport {
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new OppoHeadphonesProtocol(getDevice());
}
@Override
protected GBDeviceIoThread createDeviceIOThread() {
return new OppoHeadphonesIoThread(
getDevice(),
getContext(),
(OppoHeadphonesProtocol) getDeviceProtocol(),
OppoHeadphonesSupport.this,
getBluetoothAdapter()
);
}
@Override
public boolean useAutoConnect() {
return false;
}
}

View File

@ -0,0 +1,55 @@
/* 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 OppoCommand {
BATTERY_REQ(0x0106),
BATTERY_RET(0x8106),
DEVICE_INFO(0x0204),
FIRMWARE_GET(0x0105),
FIRMWARE_RET(0x8105),
TOUCH_CONFIG_REQ(0x0108),
TOUCH_CONFIG_SET(0x0401),
TOUCH_CONFIG_RET(0x8108),
TOUCH_CONFIG_ACK(0x8401),
FIND_DEVICE_REQ(0x0400),
FIND_DEVICE_ACK(0x8400),
;
private final short code;
OppoCommand(final int code) {
this.code = (short) code;
}
public short getCode() {
return code;
}
@Nullable
public static OppoCommand fromCode(final short code) {
for (final OppoCommand cmd : OppoCommand.values()) {
if (cmd.code == code) {
return cmd;
}
}
return null;
}
}

View File

@ -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 TouchConfigSide {
LEFT(0x01),
RIGHT(0x02),
;
private final int code;
TouchConfigSide(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
@Nullable
public static TouchConfigSide fromCode(final int code) {
for (final TouchConfigSide param : TouchConfigSide.values()) {
if (param.code == code) {
return param;
}
}
return null;
}
}

View File

@ -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 TouchConfigType {
UNK_1(0x0101),
TAP_2(0x0201),
TAP_3(0x0301),
HOLD(0x0401),
;
private final int code;
TouchConfigType(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
@Nullable
public static TouchConfigType fromCode(final int code) {
for (final TouchConfigType param : TouchConfigType.values()) {
if (param.code == code) {
return param;
}
}
return null;
}
}

View File

@ -0,0 +1,52 @@
/* 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 TouchConfigValue {
OFF(0x00),
PLAY_PAUSE(0x01),
VOICE_ASSISTANT(0x03),
PREVIOUS(0x05),
NEXT(0x06),
GAME_MODE(0x11),
VOLUME_UP(0x0B),
VOLUME_DOWN(0x0C),
;
private final int code;
TouchConfigValue(final int code) {
this.code = code;
}
public int getCode() {
return code;
}
@Nullable
public static TouchConfigValue fromCode(final int code) {
for (final TouchConfigValue param : TouchConfigValue.values()) {
if (param.code == code) {
return param;
}
}
return null;
}
}

View File

@ -3566,6 +3566,46 @@
<item>as_off</item>
</string-array>
<string-array name="oppo_touch_tap_2_names">
<item>@string/sony_button_mode_off</item>
<item>@string/moondrop_touch_action_play_pause</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_next</item>
<item>@string/pref_title_touch_voice_assistant</item>
</string-array>
<string-array name="oppo_touch_tap_2_values">
<item>off</item>
<item>play_pause</item>
<item>previous</item>
<item>next</item>
<item>voice_assistant</item>
</string-array>
<string-array name="oppo_touch_tap_3_names">
<item>@string/sony_button_mode_off</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/prefs_game_mode</item>
</string-array>
<string-array name="oppo_touch_tap_3_values">
<item>off</item>
<item>voice_assistant</item>
<item>game_mode</item>
</string-array>
<string-array name="oppo_touch_hold_names">
<item>@string/sony_button_mode_off</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_volumedown</item>
</string-array>
<string-array name="oppo_touch_hold_values">
<item>off</item>
<item>volume_up</item>
<item>volume_down</item>
</string-array>
<string-array name="soundcore_button_function_names">
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_volumeup</item>

View File

@ -2484,6 +2484,7 @@
<string name="devicetype_nothing_cmf_buds_pro_2">CMF Buds Pro 2</string>
<string name="devicetype_nothing_cmf_watch_pro">CMF Watch Pro</string>
<string name="devicetype_nothing_cmf_watch_pro_2">CMF Watch Pro 2</string>
<string name="devicetype_oppo_enco_air">Oppo Enco Air</string>
<string name="devicetype_galaxybuds">Galaxy Buds</string>
<string name="devicetype_galaxybuds_live">Galaxy Buds Live</string>
<string name="devicetype_galaxybuds_pro">Galaxy Buds Pro</string>

View File

@ -0,0 +1,62 @@
<?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:title="@string/left_earbud"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_tap_2_names"
android:entryValues="@array/oppo_touch_tap_2_values"
android:icon="@drawable/ic_filter_2"
android:key="oppo_touch__left__tap_2"
android:summary="%s"
android:title="@string/double_tap" />
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_tap_3_names"
android:entryValues="@array/oppo_touch_tap_3_values"
android:icon="@drawable/ic_filter_3"
android:key="oppo_touch__left__tap_3"
android:summary="%s"
android:title="@string/triple_tap" />
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_hold_names"
android:entryValues="@array/oppo_touch_hold_values"
android:icon="@drawable/ic_horizontal_rule"
android:key="oppo_touch__left__hold"
android:summary="%s"
android:title="@string/long_press" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/right_earbud"
app:iconSpaceReserved="false">
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_tap_2_names"
android:entryValues="@array/oppo_touch_tap_2_values"
android:icon="@drawable/ic_filter_2"
android:key="oppo_touch__right__tap_2"
android:summary="%s"
android:title="@string/double_tap" />
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_tap_3_names"
android:entryValues="@array/oppo_touch_tap_3_values"
android:icon="@drawable/ic_filter_3"
android:key="oppo_touch__right__tap_3"
android:summary="%s"
android:title="@string/triple_tap" />
<ListPreference
android:defaultValue="none"
android:entries="@array/oppo_touch_hold_names"
android:entryValues="@array/oppo_touch_hold_values"
android:icon="@drawable/ic_horizontal_rule"
android:key="oppo_touch__right__hold"
android:summary="%s"
android:title="@string/long_press" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>