mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge
synced 2024-11-28 04:46:51 +01:00
Initial support for Anker Soundcore Liberty 4 NC earbuds
This commit is contained in:
parent
227f0a9d80
commit
648212bdc1
@ -0,0 +1,83 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc;
|
||||||
|
|
||||||
|
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.DeviceSpecificSettingsScreen;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||||
|
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.soundcore.liberty4_nc.SoundcoreLiberty4NCDeviceSupport;
|
||||||
|
|
||||||
|
public class SoundcoreLiberty4NCCoordinator extends AbstractDeviceCoordinator {
|
||||||
|
@Override
|
||||||
|
public int getDeviceNameResource() {
|
||||||
|
return R.string.devicetype_soundcore_liberty4_nc;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDefaultIconResource() {
|
||||||
|
return R.drawable.ic_device_galaxy_buds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getDisabledIconResource() {
|
||||||
|
return R.drawable.ic_device_galaxy_buds_disabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getManufacturer() {
|
||||||
|
return "Anker";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Pattern getSupportedDeviceName() {
|
||||||
|
return Pattern.compile("soundcore Liberty 4 NC");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBondingStyle(){
|
||||||
|
return BONDING_STYLE_NONE;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getBatteryCount() {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
|
||||||
|
BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_buds_pro_case, R.string.battery_case);
|
||||||
|
BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
|
||||||
|
BatteryConfig battery3 = new BatteryConfig(2, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
|
||||||
|
return new BatteryConfig[]{battery1, battery2, battery3};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
|
||||||
|
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
|
||||||
|
deviceSpecificSettings.addRootScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS);
|
||||||
|
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_sony_headphones_ambient_sound_control_button_modes);
|
||||||
|
deviceSpecificSettings.addSubScreen(DeviceSpecificSettingsScreen.TOUCH_OPTIONS, R.xml.devicesettings_soundcore_touch_options);
|
||||||
|
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_soundcore_headphones);
|
||||||
|
return deviceSpecificSettings;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Class<? extends DeviceSupport> getDeviceSupportClass() {
|
||||||
|
return SoundcoreLiberty4NCDeviceSupport.class;
|
||||||
|
}
|
||||||
|
}
|
@ -226,6 +226,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
|
|||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty3_pro.SoundcoreLiberty3ProCoordinator;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.liberty4_nc.SoundcoreLiberty4NCCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.motion300.SoundcoreMotion300Coordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
|
||||||
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
|
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
|
||||||
@ -451,6 +452,7 @@ public enum DeviceType {
|
|||||||
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
|
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
|
||||||
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
|
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
|
||||||
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
|
||||||
|
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
|
||||||
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
|
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
|
||||||
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
|
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
|
||||||
BOSE_QC35(QC35Coordinator.class),
|
BOSE_QC35(QC35Coordinator.class),
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty4_nc;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.btbr.TransactionBuilder;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||||
|
|
||||||
|
public class SoundcoreLiberty4NCDeviceSupport extends AbstractSerialDeviceSupport {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean connect() {
|
||||||
|
getDeviceIOThread().start();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean useAutoConnect() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected GBDeviceProtocol createDeviceProtocol() {
|
||||||
|
return new SoundcoreLibertyProtocol(getDevice());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected synchronized GBDeviceIoThread createDeviceIOThread() {
|
||||||
|
return new SoundcoreLibertyIOThread(getDevice(), getContext(),
|
||||||
|
(SoundcoreLibertyProtocol) getDeviceProtocol(),
|
||||||
|
SoundcoreLiberty4NCDeviceSupport.this, getBluetoothAdapter());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty4_nc;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class SoundcoreLibertyIOThread extends BtClassicIoThread {
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyIOThread.class);
|
||||||
|
private final SoundcoreLibertyProtocol mSoundcoreProtocol;
|
||||||
|
|
||||||
|
public SoundcoreLibertyIOThread(GBDevice gbDevice, Context context, SoundcoreLibertyProtocol deviceProtocol, SoundcoreLiberty4NCDeviceSupport deviceSupport, BluetoothAdapter btAdapter) {
|
||||||
|
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
|
||||||
|
mSoundcoreProtocol = deviceProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void initialize() {
|
||||||
|
write(mSoundcoreProtocol.encodeDeviceInfoRequest());
|
||||||
|
setUpdateState(GBDevice.State.INITIALIZED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
|
||||||
|
return mSoundcoreProtocol.UUID_DEVICE_CTRL;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected byte[] parseIncoming(InputStream inStream) throws IOException {
|
||||||
|
byte[] buffer = new byte[1048576]; //HUGE read
|
||||||
|
int bytes = inStream.read(buffer);
|
||||||
|
LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
|
||||||
|
return Arrays.copyOf(buffer, bytes);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,350 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty4_nc;
|
||||||
|
|
||||||
|
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.prefs.AmbientSoundControlButtonMode;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
|
||||||
|
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||||
|
|
||||||
|
public class SoundcoreLibertyProtocol extends GBDeviceProtocol {
|
||||||
|
|
||||||
|
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreLibertyProtocol.class);
|
||||||
|
|
||||||
|
private static final int battery_case = 0;
|
||||||
|
private static final int battery_earphone_left = 1;
|
||||||
|
private static final int battery_earphone_right = 2;
|
||||||
|
|
||||||
|
final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3947");
|
||||||
|
protected SoundcoreLibertyProtocol(GBDevice device) {
|
||||||
|
super(device);
|
||||||
|
}
|
||||||
|
|
||||||
|
private GBDeviceEventBatteryInfo buildBatteryInfo(int batteryIndex, int level) {
|
||||||
|
GBDeviceEventBatteryInfo info = new GBDeviceEventBatteryInfo();
|
||||||
|
info.batteryIndex = batteryIndex;
|
||||||
|
info.level = level;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private GBDeviceEventVersionInfo buildVersionInfo(String firmware1, String firmware2, String serialNumber) {
|
||||||
|
GBDeviceEventVersionInfo info = new GBDeviceEventVersionInfo();
|
||||||
|
info.hwVersion = serialNumber;
|
||||||
|
info.fwVersion = firmware1;
|
||||||
|
info.fwVersion2 = firmware2;
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String readString(byte[] data, int position, int size) {
|
||||||
|
if (position + size > data.length) throw new IllegalStateException();
|
||||||
|
return new String(data, position, size, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
|
||||||
|
ByteBuffer buf = ByteBuffer.wrap(responseData);
|
||||||
|
SoundcorePacket packet = SoundcorePacket.decode(buf);
|
||||||
|
|
||||||
|
if (packet == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
List<GBDeviceEvent> devEvts = new ArrayList<>();
|
||||||
|
short cmd = packet.getCommand();
|
||||||
|
byte[] payload = packet.getPayload();
|
||||||
|
|
||||||
|
if (cmd == (short) 0x0101) {
|
||||||
|
// a lot of other data is in here, anything interesting?
|
||||||
|
String firmware1 = readString(payload, 6, 5);
|
||||||
|
String firmware2 = readString(payload, 11, 5);
|
||||||
|
String serialNumber = readString(payload, 16, 16);
|
||||||
|
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
|
||||||
|
} else if (cmd == (short) 0x8d01) {
|
||||||
|
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
|
||||||
|
} else if (cmd == (short) 0x8205) {
|
||||||
|
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
|
||||||
|
} else if (cmd == (short) 0x0105) {
|
||||||
|
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
|
||||||
|
} else if (cmd == (short) 0x0106) { //Sound Mode Update
|
||||||
|
decodeAudioMode(payload);
|
||||||
|
} else if (cmd == (short) 0x0301) { // Battery Update
|
||||||
|
int batteryLeft = payload[0] * 20;
|
||||||
|
int batteryRight = payload[1] * 20;
|
||||||
|
int batteryCase = payload[2] * 20;
|
||||||
|
|
||||||
|
devEvts.add(buildBatteryInfo(battery_case, batteryCase));
|
||||||
|
devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeft));
|
||||||
|
devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRight));
|
||||||
|
} else {
|
||||||
|
// see https://github.com/gmallios/SoundcoreManager/blob/master/soundcore-lib/src/models/packet_kind.rs
|
||||||
|
// for a mapping for other soundcore devices (similar protocol?)
|
||||||
|
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
|
||||||
|
}
|
||||||
|
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void decodeAudioMode(byte[] payload) {
|
||||||
|
SharedPreferences prefs = getDevicePrefs().getPreferences();
|
||||||
|
SharedPreferences.Editor editor = prefs.edit();
|
||||||
|
String soundmode = "off";
|
||||||
|
int anc_strength = 0;
|
||||||
|
|
||||||
|
if (payload[0] == 0x00) {
|
||||||
|
soundmode = "noise_cancelling";
|
||||||
|
} else if (payload[0] == 0x01) {
|
||||||
|
soundmode = "ambient_sound";
|
||||||
|
} else if (payload[0] == 0x02) {
|
||||||
|
soundmode = "off";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (payload[1] == 0x10) {
|
||||||
|
anc_strength = 0;
|
||||||
|
} else if (payload[1] == 0x20) {
|
||||||
|
anc_strength = 1;
|
||||||
|
} else if (payload[1] == 0x30) {
|
||||||
|
anc_strength = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean vocal_mode = (payload[2] == 0x01);
|
||||||
|
boolean adaptive_anc = (payload[3] == 0x01);
|
||||||
|
boolean windnoiseReduction = (payload[4] == 0x01);
|
||||||
|
|
||||||
|
editor.putString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, soundmode);
|
||||||
|
editor.putInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, anc_strength);
|
||||||
|
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, vocal_mode);
|
||||||
|
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, adaptive_anc);
|
||||||
|
editor.putBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, windnoiseReduction);
|
||||||
|
editor.apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] encodeSendConfiguration(String config) {
|
||||||
|
Prefs prefs = getDevicePrefs();
|
||||||
|
String pref_string;
|
||||||
|
|
||||||
|
switch (config) {
|
||||||
|
// Ambient Sound Modes
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL:
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION:
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE:
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING:
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL:
|
||||||
|
return encodeAudioMode();
|
||||||
|
|
||||||
|
// Control
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED:
|
||||||
|
return encodeControlTouchLockMessage(TapAction.SINGLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED, false));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED:
|
||||||
|
return encodeControlTouchLockMessage(TapAction.DOUBLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED, false));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED:
|
||||||
|
return encodeControlTouchLockMessage(TapAction.TRIPLE_TAP, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED, false));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED:
|
||||||
|
return encodeControlTouchLockMessage(TapAction.LONG_PRESS, prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED, false));
|
||||||
|
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, false, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.SINGLE_TAP, true, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, false, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.DOUBLE_TAP, true, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, false, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.TRIPLE_TAP, true, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.LONG_PRESS, false, TapFunction.valueOf(pref_string));
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT:
|
||||||
|
pref_string = prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT, "");
|
||||||
|
return encodeControlFunctionMessage(TapAction.LONG_PRESS, true, TapFunction.valueOf(pref_string));
|
||||||
|
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE:
|
||||||
|
AmbientSoundControlButtonMode modes = AmbientSoundControlButtonMode.fromPreferences(prefs.getPreferences());
|
||||||
|
switch (modes) {
|
||||||
|
case NC_AS_OFF:
|
||||||
|
return encodeControlAmbientModeMessage(true, true, true);
|
||||||
|
case NC_AS:
|
||||||
|
return encodeControlAmbientModeMessage(true, true, false);
|
||||||
|
case NC_OFF:
|
||||||
|
return encodeControlAmbientModeMessage(true, false, true);
|
||||||
|
case AS_OFF:
|
||||||
|
return encodeControlAmbientModeMessage(false, true, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Miscellaneous Settings
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION:
|
||||||
|
boolean wearingDetection = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_DETECTION, false);
|
||||||
|
return new SoundcorePacket((short) 0x8101, new byte[]{encodeBoolean(wearingDetection)}).encode();
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE:
|
||||||
|
boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false);
|
||||||
|
return new SoundcorePacket((short) 0x8c01, new byte[]{encodeBoolean(wearingTone)}).encode();
|
||||||
|
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE:
|
||||||
|
boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false);
|
||||||
|
return new SoundcorePacket((short) 0x8301, new byte[]{encodeBoolean(touchTone)}).encode();
|
||||||
|
default:
|
||||||
|
LOG.debug("Unsupported CONFIG: " + config);
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.encodeSendConfiguration(config);
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encodeDeviceInfoRequest() {
|
||||||
|
return new SoundcorePacket((short) 0x0101).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] encodeMysteryDataRequest1() {
|
||||||
|
byte[] payload = new byte[]{0x00};
|
||||||
|
return new SoundcorePacket((short) 0x8d01, payload).encode();
|
||||||
|
}
|
||||||
|
byte[] encodeMysteryDataRequest2() {
|
||||||
|
return new SoundcorePacket((short) 0x0105).encode();
|
||||||
|
}
|
||||||
|
byte[] encodeMysteryDataRequest3() {
|
||||||
|
byte[] payload = new byte[]{0x00};
|
||||||
|
return new SoundcorePacket((short) 0x8205, payload).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes the following settings to a payload to set the audio-mode on the headphones:
|
||||||
|
* PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL If ANC, Transparent or neither should be active
|
||||||
|
* PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING If the strenght of the ANC should be set manual or adaptively according to ambient noise
|
||||||
|
* PREF_SONY_AMBIENT_SOUND_LEVEL How strong the ANC should be in manual mode
|
||||||
|
* PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE If the Transparency should focus on vocals or should be fully transparent
|
||||||
|
* PREF_SOUNDCORE_WIND_NOISE_REDUCTION If Transparency or ANC should reduce Wind Noise
|
||||||
|
* @return The payload
|
||||||
|
*/
|
||||||
|
private byte[] encodeAudioMode() {
|
||||||
|
Prefs prefs = getDevicePrefs();
|
||||||
|
|
||||||
|
byte anc_mode;
|
||||||
|
switch (prefs.getString(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL, "off")) {
|
||||||
|
case "noise_cancelling":
|
||||||
|
anc_mode = 0x00;
|
||||||
|
break;
|
||||||
|
case "ambient_sound":
|
||||||
|
anc_mode = 0x01;
|
||||||
|
break;
|
||||||
|
case "off":
|
||||||
|
anc_mode = 0x02;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.error("Invalid Audio Mode selected");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte anc_strength;
|
||||||
|
switch (prefs.getInt(DeviceSettingsPreferenceConst.PREF_SONY_AMBIENT_SOUND_LEVEL, 0)) {
|
||||||
|
case 0:
|
||||||
|
anc_strength = 0x10;
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
anc_strength = 0x20;
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
anc_strength = 0x30;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.error("Invalid ANC Strength selected");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte adaptive_anc = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING, true));
|
||||||
|
byte vocal_mode = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE, false));
|
||||||
|
byte windnoise_reduction = encodeBoolean(prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WIND_NOISE_REDUCTION, false));
|
||||||
|
|
||||||
|
byte[] payload = new byte[]{anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
|
||||||
|
return new SoundcorePacket((short) 0x8106, payload).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables or disables a tap-action
|
||||||
|
* @param action The byte that encodes the action (single/double/triple or long tap)
|
||||||
|
* @param disabled If the action should be enabled or disabled
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private byte[] encodeControlTouchLockMessage(TapAction action, boolean disabled) {
|
||||||
|
boolean enabled = !disabled;
|
||||||
|
byte enabled_byte;
|
||||||
|
byte[] payload;
|
||||||
|
switch (action) {
|
||||||
|
case SINGLE_TAP:
|
||||||
|
case TRIPLE_TAP:
|
||||||
|
enabled_byte = encodeBoolean(enabled);
|
||||||
|
break;
|
||||||
|
case DOUBLE_TAP:
|
||||||
|
case LONG_PRESS:
|
||||||
|
enabled_byte = enabled?(byte) 0x11: (byte) 0x10;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.error("Invalid Tap action");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
payload = new byte[]{0x00, action.getCode(), enabled_byte};
|
||||||
|
return new SoundcorePacket((short) 0x8304, payload).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Assigns a function (eg play/pause) to an action (eg single tap on right bud)
|
||||||
|
* @param action The byte that encodes the action (single/double/triple or long tap)
|
||||||
|
* @param right If the right or left earbud is meant
|
||||||
|
* @param function The byte that encodes the triggered function (eg play/pause)
|
||||||
|
* @return The encoded message
|
||||||
|
*/
|
||||||
|
private byte[] encodeControlFunctionMessage(TapAction action, boolean right, TapFunction function) {
|
||||||
|
byte function_byte;
|
||||||
|
switch (action) {
|
||||||
|
case SINGLE_TAP:
|
||||||
|
case DOUBLE_TAP:
|
||||||
|
function_byte = (byte) (16*6 + function.getCode());
|
||||||
|
break;
|
||||||
|
case TRIPLE_TAP:
|
||||||
|
function_byte = (byte) (16*4 + function.getCode());
|
||||||
|
break;
|
||||||
|
case LONG_PRESS:
|
||||||
|
function_byte = (byte) (16*5 + function.getCode());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
LOG.error("Invalid Tap action");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
byte[] payload = new byte[] {encodeBoolean(right), action.getCode(), function_byte};
|
||||||
|
return new SoundcorePacket((short) 0x8104, payload).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encodes between which Audio Modes a tap should switch, if it is set to switch the Audio Mode.
|
||||||
|
* Zb ANC -> -> Transparency -> Normal -> ANC -> ....
|
||||||
|
*/
|
||||||
|
private byte[] encodeControlAmbientModeMessage(boolean anc, boolean transparency, boolean normal) {
|
||||||
|
// Original app does not allow only one true flag. Unsure if Earbuds accept this state.
|
||||||
|
byte ambientModes = (byte) (4 * (normal?1:0) + 2 * (transparency?1:0) + (anc?1:0));
|
||||||
|
return new SoundcorePacket((short) 0x8206, new byte[] {ambientModes}).encode();
|
||||||
|
}
|
||||||
|
|
||||||
|
private byte encodeBoolean(boolean bool) {
|
||||||
|
if (bool) return 0x01;
|
||||||
|
else return 0x00;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,19 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty4_nc;
|
||||||
|
|
||||||
|
enum TapAction {
|
||||||
|
SINGLE_TAP((byte) 0x02),
|
||||||
|
DOUBLE_TAP((byte) 0x00),
|
||||||
|
TRIPLE_TAP((byte) 0x05),
|
||||||
|
LONG_PRESS((byte) 0x01)
|
||||||
|
;
|
||||||
|
|
||||||
|
private final byte code;
|
||||||
|
|
||||||
|
TapAction(final byte code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.liberty4_nc;
|
||||||
|
enum TapFunction {
|
||||||
|
VOLUME_DOWN(1),
|
||||||
|
VOLUME_UP(0),
|
||||||
|
MEDIA_NEXT( 3),
|
||||||
|
MEDIA_PREV(2),
|
||||||
|
PLAYPAUSE(6),
|
||||||
|
VOICE_ASSISTANT(5),
|
||||||
|
AMBIENT_SOUND_CONTROL(4)
|
||||||
|
;
|
||||||
|
|
||||||
|
private final int code;
|
||||||
|
|
||||||
|
TapFunction(final int code) {
|
||||||
|
this.code = code;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCode() {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
}
|
@ -1781,6 +1781,7 @@
|
|||||||
<string name="devicetype_sony_linkbuds">Sony LinkBuds</string>
|
<string name="devicetype_sony_linkbuds">Sony LinkBuds</string>
|
||||||
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
|
<string name="devicetype_sony_linkbuds_s">Sony LinkBuds S</string>
|
||||||
<string name="devicetype_soundcore_liberty3_pro">Soundcore Liberty 3 Pro</string>
|
<string name="devicetype_soundcore_liberty3_pro">Soundcore Liberty 3 Pro</string>
|
||||||
|
<string name="devicetype_soundcore_liberty4_nc">Soundcore Liberty 4 NC</string>
|
||||||
<string name="devicetype_soundcore_motion300">Soundcore Motion 300</string>
|
<string name="devicetype_soundcore_motion300">Soundcore Motion 300</string>
|
||||||
<string name="devicetype_moondrop_space_travel">Moondrop Space Travel</string>
|
<string name="devicetype_moondrop_space_travel">Moondrop Space Travel</string>
|
||||||
<string name="devicetype_binary_sensor">Binary sensor</string>
|
<string name="devicetype_binary_sensor">Binary sensor</string>
|
||||||
|
@ -56,6 +56,7 @@
|
|||||||
|
|
||||||
<SwitchPreferenceCompat
|
<SwitchPreferenceCompat
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
android:key="pref_soundcore_wearing_detection"
|
||||||
android:layout="@layout/preference_checkbox"
|
android:layout="@layout/preference_checkbox"
|
||||||
android:summary="@string/nothing_prefs_inear_summary"
|
android:summary="@string/nothing_prefs_inear_summary"
|
||||||
android:title="@string/nothing_prefs_inear_title"/>
|
android:title="@string/nothing_prefs_inear_title"/>
|
||||||
|
Loading…
Reference in New Issue
Block a user