diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCCoordinator.java new file mode 100644 index 000000000..3719d2423 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCCoordinator.java @@ -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 getDeviceSupportClass() { + return SoundcoreLiberty4NCDeviceSupport.class; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index 5272f9cb0..4956569c8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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.sonyswr12.SonySWR12DeviceCoordinator; 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.supercars.SuperCarsCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator; @@ -451,6 +452,7 @@ public enum DeviceType { SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class), SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class), SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class), + SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class), SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class), MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class), BOSE_QC35(QC35Coordinator.class), diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCDeviceSupport.java new file mode 100644 index 000000000..b822dcc72 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLiberty4NCDeviceSupport.java @@ -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()); + } + +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyIOThread.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyIOThread.java new file mode 100644 index 000000000..bf7dd701f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyIOThread.java @@ -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); + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyProtocol.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyProtocol.java new file mode 100644 index 000000000..f8582ff2a --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/SoundcoreLibertyProtocol.java @@ -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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapAction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapAction.java new file mode 100644 index 000000000..06d056b11 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapAction.java @@ -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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapFunction.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapFunction.java new file mode 100644 index 000000000..3a3b79b59 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/soundcore/liberty4_nc/TapFunction.java @@ -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; + } +} diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7434aedad..567e00859 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1781,6 +1781,7 @@ Sony LinkBuds Sony LinkBuds S Soundcore Liberty 3 Pro + Soundcore Liberty 4 NC Soundcore Motion 300 Moondrop Space Travel Binary sensor diff --git a/app/src/main/res/xml/devicesettings_soundcore_headphones.xml b/app/src/main/res/xml/devicesettings_soundcore_headphones.xml index 8c5ba11b2..6a280e922 100644 --- a/app/src/main/res/xml/devicesettings_soundcore_headphones.xml +++ b/app/src/main/res/xml/devicesettings_soundcore_headphones.xml @@ -56,6 +56,7 @@