mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-26 20:06:52 +01:00
Oppo Enco Air: Initial support
This commit is contained in:
parent
7a0e43a4de
commit
a72de07d2a
@ -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();
|
||||
}
|
||||
}
|
@ -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)
|
||||
);
|
||||
}
|
||||
}
|
@ -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) {
|
||||
}
|
||||
}
|
@ -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),
|
||||
|
@ -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) ?
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
Loading…
Reference in New Issue
Block a user