New Device Soundcore Liberty 3 Pro (#3753)

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/3753
Co-authored-by: ahormann <ahormann@gmx.net>
Co-committed-by: ahormann <ahormann@gmx.net>
This commit is contained in:
ahormann 2024-05-07 22:39:13 +00:00 committed by José Rebelo
parent 508a86b8ed
commit ac8d1ed6a0
15 changed files with 845 additions and 0 deletions

View File

@ -343,6 +343,25 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SONY_PROTOCOL_VERSION = "pref_protocol_version";
public static final String PREF_SONY_ACTUAL_PROTOCOL_VERSION = "pref_actual_protocol_version";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL = "pref_sony_ambient_sound_control";
public static final String PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL = "pref_soundcore_ambient_sound_control";
public static final String PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING = "pref_adaptive_noise_cancelling";
public static final String PREF_SOUNDCORE_WIND_NOISE_REDUCTION= "pref_soundcore_wind_noise_reduction";
public static final String PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE = "pref_soundcore_transparency_vocal_mode";
public static final String PREF_SOUNDCORE_WEARING_DETECTION = "pref_soundcore_wearing_detection";
public static final String PREF_SOUNDCORE_WEARING_TONE = "pref_soundcore_wearing_tone";
public static final String PREF_SOUNDCORE_TOUCH_TONE = "pref_soundcore_touch_tone";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED = "pref_soundcore_control_single_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED = "pref_soundcore_control_double_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED = "pref_soundcore_control_triple_tap_disabled";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED = "pref_soundcore_control_long_press_disabled";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT = "pref_soundcore_control_single_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT = "pref_soundcore_control_double_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT = "pref_soundcore_control_triple_tap_action_left";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT = "pref_soundcore_control_long_press_action_left";
public static final String PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT = "pref_soundcore_control_single_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT = "pref_soundcore_control_double_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT = "pref_soundcore_control_triple_tap_action_right";
public static final String PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT = "pref_soundcore_control_long_press_action_right";
public static final String PREF_SONY_AMBIENT_SOUND_CONTROL_BUTTON_MODE = "pref_sony_ambient_sound_control_button_mode";
public static final String PREF_SONY_FOCUS_VOICE = "pref_sony_focus_voice";
public static final String PREF_SONY_AMBIENT_SOUND_LEVEL = "pref_sony_ambient_sound_level";

View File

@ -563,6 +563,27 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SONY_CONNECT_TWO_DEVICES);
addPreferenceHandlerFor(PREF_SONY_ADAPTIVE_VOLUME_CONTROL);
addPreferenceHandlerFor(PREF_SONY_WIDE_AREA_TAP);
addPreferenceHandlerFor(PREF_SOUNDCORE_AMBIENT_SOUND_CONTROL);
addPreferenceHandlerFor(PREF_SOUNDCORE_WIND_NOISE_REDUCTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_TRANSPARENCY_VOCAL_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_NOISE_CANCELLING);
addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_LEFT);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_LONG_PRESS_ACTION_RIGHT);
addPreferenceHandlerFor(PREF_FEMOMETER_MEASUREMENT_MODE);
addPreferenceHandlerFor(PREF_QC35_NOISE_CANCELLING_LEVEL);

View File

@ -34,6 +34,7 @@ public enum DeviceSpecificSettingsScreen {
DATE_TIME("pref_screen_date_time", R.xml.devicesettings_root_date_time),
WORKOUT("pref_screen_workout", R.xml.devicesettings_root_workout),
HEALTH("pref_screen_health", R.xml.devicesettings_root_health),
TOUCH_OPTIONS("pref_screen_touch_options", R.xml.devicesettings_root_touch_options),
;
private final String key;

View File

@ -0,0 +1,83 @@
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore;
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.SoundcoreLiberty3ProDeviceSupport;
public class SoundcoreLiberty3ProCoordinator extends AbstractDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_soundcore_liberty3_pro;
}
@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 3 Pro");
}
@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() {
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 SoundcoreLiberty3ProDeviceSupport.class;
}
}

View File

@ -168,6 +168,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWH1000XM5Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.SoundcoreLiberty3ProCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.supercars.SuperCarsCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.test.TestDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.tlw64.TLW64Coordinator;
@ -348,6 +349,7 @@ public enum DeviceType {
SONY_LINKBUDS_S(SonyLinkBudsSCoordinator.class),
SONY_WH_1000XM5(SonyWH1000XM5Coordinator.class),
SONY_WF_1000XM5(SonyWF1000XM5Coordinator.class),
SOUNDCORE_LIBERTY3_PRO(SoundcoreLiberty3ProCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
HONORBAND3(HonorBand3Coordinator.class),
HONORBAND4(HonorBand4Coordinator.class),

View File

@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
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 SoundcoreLiberty3ProDeviceSupport 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(),
SoundcoreLiberty3ProDeviceSupport.this, getBluetoothAdapter());
}
}

View File

@ -0,0 +1,49 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
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, SoundcoreLiberty3ProDeviceSupport 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);
}
}

View File

@ -0,0 +1,377 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
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.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
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.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-d6832e7b3952");
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) {
// Byte 0-4: Header
// Byte 5-6: Command (Audio-Mode)
// Byte 7: Size of data
// Byte 8-(x-1): Data
// Byte x: Checksum
if (responseData.length == 0) return null;
List<GBDeviceEvent> devEvts = new ArrayList<>();
byte[] command = Arrays.copyOfRange(responseData, 5, 7);
byte[] data = Arrays.copyOfRange(responseData, 8, responseData.length-1);
if (Arrays.equals(command, new byte[]{0x01, 0x01})) {
// a lot of other data is in here, anything interesting?
String firmware1 = readString(data, 7, 5);
String firmware2 = readString(data, 12, 5);
String serialNumber = readString(data, 17, 16);
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
} else if (Arrays.equals(command, new byte[]{0x01, (byte) 0x8d})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, (byte) 0x82})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x05, 0x01})) {
LOG.debug("Unknown incoming message - command: " + hexdump(command) + ", dump: " + hexdump(responseData));
} else if (Arrays.equals(command, new byte[]{0x06, 0x01})) { //Sound Mode Update
decodeAudioMode(data);
} else if (Arrays.equals(command, new byte[]{0x01, 0x03})) { // Battery Update
int batteryLeft = data[1] * 20;
int batteryRight = data[2] * 20;
int batteryCase = data[3] * 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: " + hexdump(command) + ", 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[1] == 0x00) {
soundmode = "noise_cancelling";
} else if (payload[1] == 0x01) {
soundmode = "ambient_sound";
} else if (payload[1] == 0x02) {
soundmode = "off";
}
if (payload[2] == 0x10) {
anc_strength = 0;
} else if (payload[2] == 0x20) {
anc_strength = 1;
} else if (payload[2] == 0x30) {
anc_strength = 2;
}
boolean vocal_mode = (payload[3] == 0x01);
boolean adaptive_anc = (payload[4] == 0x01);
boolean windnoiseReduction = (payload[5] == 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 encodeMessage((byte) 0x01, (byte) 0x81, new byte[]{0x00, encodeBoolean(wearingDetection)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE:
boolean wearingTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_WEARING_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x8c, new byte[]{0x00, encodeBoolean(wearingTone)});
case DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE:
boolean touchTone = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE, false);
return encodeMessage((byte) 0x01, (byte) 0x83, new byte[]{0x00, encodeBoolean(touchTone)});
default:
LOG.debug("Unsupported CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
byte[] encodeDeviceInfoRequest() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x01, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest1() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x01, (byte) 0x8d, payload);
}
byte[] encodeMysteryDataRequest2() {
byte[] payload = new byte[]{0x00};
return encodeMessage((byte) 0x05, (byte) 0x01, payload);
}
byte[] encodeMysteryDataRequest3() {
byte[] payload = new byte[]{0x00, 0x00};
return encodeMessage((byte) 0x05, (byte) 0x82, payload);
}
/**
* 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[]{0x00, anc_mode, anc_strength, vocal_mode, adaptive_anc, windnoise_reduction, 0x01};
return encodeMessage((byte) 0x06, (byte) 0x81, payload);
}
/**
* 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, 0x00, action.getCode(), enabled_byte};
return encodeMessage((byte) 0x04, (byte) 0x83, payload);
}
/**
* 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[] {0x00, encodeBoolean(right), action.getCode(), function_byte};
return encodeMessage((byte) 0x04, (byte) 0x81, payload);
}
/**
* 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 encodeMessage((byte) 0x06, (byte) 0x82, new byte[] {0x00, ambientModes});
}
private byte encodeBoolean(boolean bool) {
if (bool) return 0x01;
else return 0x00;
}
private byte[] encodeMessage(byte command1, byte command2, byte[] payload) {
int size = 8 + payload.length + 1;
ByteBuffer msgBuf = ByteBuffer.allocate(size);
msgBuf.order(ByteOrder.BIG_ENDIAN);
msgBuf.put(new byte[] {0x08, (byte) 0xee, 0x00, 0x00, 0x00}); // header
msgBuf.put(command1);
msgBuf.put(command2);
msgBuf.put((byte) size);
msgBuf.put(payload);
byte checksum = -10;
checksum += command1 + command2 + size;
for (int b : payload) {
checksum += b;
}
msgBuf.put(checksum);
return msgBuf.array();
}
}

View File

@ -0,0 +1,19 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
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;
}
}

View File

@ -0,0 +1,21 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore;
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;
}
}

View File

@ -3441,6 +3441,27 @@
<item>as_off</item>
</string-array>
<string-array name="soundcore_button_function_names">
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_next</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_playpause</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/sony_button_mode_ambient_sound_control</item>
</string-array>
<string-array name="soundcore_button_function_values">
<item>VOLUME_DOWN</item>
<item>VOLUME_UP</item>
<item>MEDIA_NEXT</item>
<item>MEDIA_PREV</item>
<item>PLAYPAUSE</item>
<item>VOICE_ASSISTANT</item>
<item>AMBIENT_SOUND_CONTROL</item>
</string-array>
<string-array name="fitness_tracking_apps_package_names">
<item>de.dennisguse.opentracks</item>
<item>de.dennisguse.opentracks.playStore</item>

View File

@ -520,6 +520,10 @@
<string name="pref_gps_satellite_search">Satellite Search</string>
<string name="pref_crown_vibration">Crown Vibration</string>
<string name="pref_alert_tone">Alert Tone</string>
<string name="pref_touch_tone">Touch Tone</string>
<string name="pref_touch_tone_summary">Plays a tone when the earbud is touched</string>
<string name="pref_wearing_tone">Wearing Tone</string>
<string name="pref_wearing_tone_summary">Plays a tone when the earbud is inserted</string>
<string name="pref_cover_to_mute">Cover to Mute</string>
<string name="pref_vibrate_for_alert">Vibrate for Alert</string>
<string name="pref_text_to_speech">Text to Speech</string>
@ -1533,6 +1537,7 @@
<string name="devicetype_sony_wi_sp600n">Sony WI-SP600N</string>
<string name="devicetype_sony_linkbuds">Sony LinkBuds</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_binary_sensor">Binary sensor</string>
<string name="devicetype_honor_band3">Honor Band 3</string>
<string name="devicetype_honor_band4">Honor Band 4</string>
@ -2284,6 +2289,8 @@
<string name="pref_wide_area_tap_title">Wide area tap</string>
<string name="pref_adaptive_volume_control_summary">Increase volume automatically when ambient sound is loud</string>
<string name="pref_adaptive_volume_control_title">Adaptive volume control</string>
<string name="pref_adaptive_noise_cancelling_title">Adaptive ANC</string>
<string name="pref_adaptive_noise_cancelling_summary">Set the strength of the ANC automatically depending on the ambient sound level</string>
<string name="sony_speak_to_chat">Speak-to-chat</string>
<string name="sony_speak_to_chat_summary">Turn off noise cancelling automatically when you start talking.</string>
<string name="sony_speak_to_chat_sensitivity">Voice Detection Sensitivity</string>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen
android:icon="@drawable/ic_touch"
android:key="pref_screen_touch_options"
android:persistent="false"
android:title="@string/prefs_galaxy_touch_options">
</PreferenceScreen>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_key_header_soundcore_ambient_sound_control"
android:title="@string/pref_header_sony_ambient_sound_control">
<ListPreference
android:defaultValue="noise_cancelling"
android:entries="@array/sony_ambient_sound_control_names"
android:entryValues="@array/sony_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_soundcore_ambient_sound_control"
android:summary="%s"
android:title="@string/sony_ambient_sound" />
<SwitchPreferenceCompat
android:defaultValue="true"
android:disableDependentsState="true"
android:icon="@drawable/ic_hearing"
android:key="pref_adaptive_noise_cancelling"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_adaptive_noise_cancelling_summary"
android:title="@string/pref_adaptive_noise_cancelling_title" />
<!-- [0, 2], low moderate and high -->
<SeekBarPreference
android:dependency="pref_adaptive_noise_cancelling"
android:defaultValue="0"
android:icon="@drawable/ic_hearing"
android:key="pref_sony_ambient_sound_level"
android:max="2"
android:title="@string/prefs_active_noise_cancelling_level" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_block"
android:key="pref_soundcore_wind_noise_reduction"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_wind_noise_reduction" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_voice"
android:key="pref_soundcore_transparency_vocal_mode"
android:layout="@layout/preference_checkbox"
android:title="@string/sony_ambient_sound_focus_voice" />
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_header_soundcore_other"
android:title="@string/pref_header_other">
<SwitchPreferenceCompat
android:defaultValue="false"
android:layout="@layout/preference_checkbox"
android:summary="@string/nothing_prefs_inear_summary"
android:title="@string/nothing_prefs_inear_title"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_wearing_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_wearing_tone_summary"
android:title="@string/pref_wearing_tone"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_touch_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/pref_touch_tone_summary"
android:title="@string/pref_touch_tone"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>

View File

@ -0,0 +1,107 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="@string/single_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_single_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_single_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_single_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/double_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_double_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_double_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_double_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/triple_tap">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_triple_tap_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_triple_tap_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_triple_tap_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/long_press">
<SwitchPreferenceCompat
android:defaultValue="false"
android:disableDependentsState="true"
android:icon="@drawable/ic_lock_open"
android:key="pref_soundcore_control_long_press_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_left"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:dependency="pref_soundcore_control_long_press_disabled"
android:entries="@array/soundcore_button_function_names"
android:entryValues="@array/soundcore_button_function_values"
android:icon="@drawable/ic_touch"
android:key="pref_soundcore_control_long_press_action_right"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>