Soundcore AeroFit 2: Initial Support

This commit is contained in:
ahormann
2025-08-25 20:27:58 +02:00
committed by José Rebelo
parent 1a58401fbf
commit ec01ac70aa
11 changed files with 372 additions and 2 deletions
@@ -445,6 +445,8 @@ public class DeviceSettingsPreferenceConst {
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_BATTERY_LOW_TONE = "pref_soundcore_battery_low_tone";
public static final String PREF_SOUNDCORE_CONTROL_TOUCH_DISABLED = "pref_soundcore_control_touch_disabled";
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";
@@ -461,6 +463,7 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_SOUNDCORE_BUTTON_BRIGHTNESS = "pref_soundcore_button_brightness";
public static final String PREF_SOUNDCORE_AUTO_POWER_OFF = "pref_soundcore_auto_power_off";
public static final String PREF_SOUNDCORE_LDAC_MODE = "pref_soundcore_ldac_mode";
public static final String PREF_SOUNDCORE_GAMING_MODE = "pref_soundcore_gaming_mode";
public static final String PREF_SOUNDCORE_ADAPTIVE_DIRECTION = "pref_soundcore_adaptive_direction";
public static final String PREF_SOUNDCORE_EQUALIZER_PRESET = "pref_soundcore_equalizer_preset";
public static final String PREF_SOUNDCORE_EQUALIZER_CUSTOM = "pref_soundcore_equalizer_custom";
@@ -853,7 +853,9 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SOUNDCORE_ANC_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_TOUCH_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_BATTERY_LOW_TONE);
addPreferenceHandlerFor(PREF_SOUNDCORE_WEARING_DETECTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TOUCH_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_SINGLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_DOUBLE_TAP_DISABLED);
addPreferenceHandlerFor(PREF_SOUNDCORE_CONTROL_TRIPLE_TAP_DISABLED);
@@ -870,6 +872,7 @@ public class DeviceSpecificSettingsFragment extends AbstractPreferenceFragment i
addPreferenceHandlerFor(PREF_SOUNDCORE_BUTTON_BRIGHTNESS);
addPreferenceHandlerFor(PREF_SOUNDCORE_AUTO_POWER_OFF);
addPreferenceHandlerFor(PREF_SOUNDCORE_LDAC_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_GAMING_MODE);
addPreferenceHandlerFor(PREF_SOUNDCORE_ADAPTIVE_DIRECTION);
addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_PRESET);
addPreferenceHandlerFor(PREF_SOUNDCORE_EQUALIZER_DIRECTION);
@@ -0,0 +1,72 @@
package nodomain.freeyourgadget.gadgetbridge.devices.soundcore.aerofit;
import androidx.annotation.NonNull;
import java.util.regex.Pattern;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsScreen;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractBLClassicDeviceCoordinator;
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.aeroFit.SoundcoreAeroFit2DeviceSupport;
public class SoundcoreAeroFit2Coordinator extends AbstractBLClassicDeviceCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_soundcore_aerofit2;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_sony_wf_800n;
}
@Override
public String getManufacturer() {
return "Anker";
}
@Override
protected Pattern getSupportedDeviceName() {
return Pattern.compile("soundcore AeroFit 2");
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_NONE;
}
@Override
public int getBatteryCount(final GBDevice device) {
return 3;
}
@Override
public boolean supportsFindDevice(@NonNull final GBDevice device) {
return true;
}
@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(R.xml.devicesettings_soundcore_aerofit);
return deviceSpecificSettings;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass(final GBDevice device) {
return SoundcoreAeroFit2DeviceSupport.class;
}
}
@@ -346,6 +346,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators
import nodomain.freeyourgadget.gadgetbridge.devices.sony.headphones.coordinators.SonyWISP600NCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sony.wena3.SonyWena3Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.sonyswr12.SonySWR12DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.soundcore.aerofit.SoundcoreAeroFit2Coordinator;
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;
@@ -682,6 +683,7 @@ public enum DeviceType {
SOUNDCORE_LIBERTY4_NC(SoundcoreLiberty4NCCoordinator.class),
SOUNDCORE_MOTION300(SoundcoreMotion300Coordinator.class),
SOUNDCORE_Q30(SoundcoreQ30Coordinator.class),
SOUNDCORE_AEROFIT2(SoundcoreAeroFit2Coordinator.class),
MOONDROP_SPACE_TRAVEL(MoondropSpaceTravelCoordinator.class),
BOSE_QC35(QC35Coordinator.class),
ONEMORE_SONOFLOW(OneMoreSonoFlowCoordinator.class),
@@ -0,0 +1,33 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.aeroFit;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class SoundcoreAeroFit2DeviceSupport extends AbstractSerialDeviceSupport {
public static final UUID UUID_DEVICE_CTRL = UUID.fromString("0cf12d31-fac3-4553-bd80-d6832e7b3874");
@Override
public boolean useAutoConnect() {
return false;
}
@Override
protected GBDeviceProtocol createDeviceProtocol() {
return new SoundcoreAeroFitProtocol(getDevice());
}
@Override
protected synchronized GBDeviceIoThread createDeviceIOThread() {
return new SoundcoreAeroFitIOThread(
getDevice(),
getContext(),
(SoundcoreAeroFitProtocol) getDeviceProtocol(),
UUID_DEVICE_CTRL,
this,
getBluetoothAdapter()
);
}
}
@@ -0,0 +1,57 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.aeroFit;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import android.bluetooth.BluetoothAdapter;
import android.content.Context;
import android.os.ParcelUuid;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.btclassic.BtClassicIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
public class SoundcoreAeroFitIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreAeroFitIOThread.class);
private final SoundcoreAeroFitProtocol mSoundcoreProtocol;
private final UUID mUuidToConnect;
public SoundcoreAeroFitIOThread(final GBDevice gbDevice,
final Context context,
final SoundcoreAeroFitProtocol deviceProtocol,
final UUID uuidToConnect,
final AbstractSerialDeviceSupport deviceSupport,
final BluetoothAdapter btAdapter) {
super(gbDevice, context, deviceProtocol, deviceSupport, btAdapter);
mSoundcoreProtocol = deviceProtocol;
mUuidToConnect = uuidToConnect;
}
@Override
protected void initialize() {
write(mSoundcoreProtocol.encodeDeviceInfoRequest());
setUpdateState(GBDevice.State.INITIALIZED);
}
@Override
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return mUuidToConnect;
}
@Override
protected byte[] parseIncoming(InputStream inStream) throws IOException {
byte[] buffer = new byte[1048576]; //HUGE read
int bytes = inStream.read(buffer);
return Arrays.copyOf(buffer, bytes);
}
}
@@ -0,0 +1,138 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.aeroFit;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDCORE_AUTO_POWER_OFF;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDCORE_BATTERY_LOW_TONE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDCORE_CONTROL_TOUCH_DISABLED;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDCORE_GAMING_MODE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SOUNDCORE_TOUCH_TONE;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.AbstractSoundcoreProtocol;
import nodomain.freeyourgadget.gadgetbridge.service.devices.soundcore.SoundcorePacket;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class SoundcoreAeroFitProtocol extends AbstractSoundcoreProtocol {
private static final Logger LOG = LoggerFactory.getLogger(SoundcoreAeroFitProtocol.class);
private static final int battery_case = 0;
private static final int battery_earphone_left = 1;
private static final int battery_earphone_right = 2;
protected SoundcoreAeroFitProtocol(GBDevice device) {
super(device);
}
@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) {
String firmware1 = readString(payload, 4, 5);
String firmware2 = readString(payload, 9, 5);
String serialNumber = readString(payload, 14, 16);
handleBatteryInfo(devEvts, null, payload[2], payload[3]);
devEvts.add(buildVersionInfo(firmware1, firmware2, serialNumber));
} else if (cmd == (short) 0x010b) {
// shows connected devices from 50 onwards
// readString(payload, 50, 112)
// maybe also other settings
LOG.debug("Incoming Information about connected devices, dump: " + hexdump(responseData));
} else if (cmd == (short) 0x0301) { // Battery Update
handleBatteryInfo(devEvts, payload[2], payload[0], payload[1]);
} else if (cmd == (short) 0x0401) {
boolean leftInCase = payload[0] == 0x01;
boolean rightInCase = payload[1] == 0x01;
LOG.info("Left Earbud in Charging Case: " + leftInCase + ", Right Earbud in Charging Case: " + rightInCase);
} else {
LOG.debug("Unknown incoming message - command: " + cmd + ", dump: " + hexdump(responseData));
}
return devEvts.toArray(new GBDeviceEvent[devEvts.size()]);
}
private void handleBatteryInfo(List<GBDeviceEvent> devEvts, Byte batteryCase, byte batteryLeft, byte batteryRight) {
int batteryLeftLevel = (batteryLeft + 1) * 10;
int batteryRightLevel = (batteryRight + 1) * 10;
devEvts.add(buildBatteryInfo(battery_earphone_left, batteryLeftLevel));
devEvts.add(buildBatteryInfo(battery_earphone_right, batteryRightLevel));
if (batteryCase != null) {
int batteryCaseLevel = (batteryCase + 1) * 10;
devEvts.add(buildBatteryInfo(battery_case, batteryCaseLevel));
}
}
@Override
public byte[] encodeSendConfiguration(String config) {
Prefs prefs = getDevicePrefs();
switch (config) {
// Control
case PREF_SOUNDCORE_CONTROL_TOUCH_DISABLED:
boolean touchDisabled = prefs.getBoolean(PREF_SOUNDCORE_CONTROL_TOUCH_DISABLED, false);
return new SoundcorePacket((short) 0x9410, new byte[]{encodeBoolean(touchDisabled)}).encode();
// Miscellaneous Settings
case PREF_SOUNDCORE_TOUCH_TONE:
boolean touchTone = prefs.getBoolean(PREF_SOUNDCORE_TOUCH_TONE, false);
return new SoundcorePacket((short) 0x8301, new byte[]{encodeBoolean(touchTone)}).encode();
case PREF_SOUNDCORE_BATTERY_LOW_TONE:
boolean batteryLowTone = prefs.getBoolean(PREF_SOUNDCORE_BATTERY_LOW_TONE, false);
return new SoundcorePacket((short) 0x8210, new byte[]{encodeBoolean(batteryLowTone)}).encode();
case PREF_SOUNDCORE_GAMING_MODE:
boolean gamingMode = prefs.getBoolean(PREF_SOUNDCORE_GAMING_MODE, false);
return new SoundcorePacket((short) 0x8701, new byte[]{encodeBoolean(gamingMode)}).encode();
case PREF_SOUNDCORE_AUTO_POWER_OFF:
int duration = Integer.parseInt(prefs.getString(PREF_SOUNDCORE_AUTO_POWER_OFF, "3"));
return setAutoPowerOff(duration).encode();
default:
LOG.debug("Unsupported CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
/**
* 0: No Auto Power off
* 1: Auto Power off 10 min
* 2: Auto Power off 20 min
* 3: Auto Power off 30 min
* 4: Auto Power off 60 min
*/
private SoundcorePacket setAutoPowerOff(int duration) {
byte[] payload;
if (duration > 0)
payload = new byte[] { (byte)0x01, (byte)(duration - 1) };
else
payload = new byte[] { (byte)0x00, (byte)0x03 };
return new SoundcorePacket((short) 0x8601, payload);
}
@Override
public byte[] encodeFindDevice(boolean start) {
boolean findLeft = start;
boolean findRight = start;
byte[] payload = new byte[]{encodeBoolean(findLeft), encodeBoolean(findRight), 0x00};
return new SoundcorePacket((short) 0x8910, payload).encode();
}
}
+9 -1
View File
@@ -4333,7 +4333,7 @@
<item>100</item>
</string-array>
<string-array name="soundcore_auto_power_off_names">
<string-array name="soundcore_motion_auto_power_off_names">
<item>@string/pref_button_action_disabled</item>
<item>@string/minutes_5</item>
<item>@string/minutes_10</item>
@@ -4341,6 +4341,14 @@
<item>@string/minutes_60</item>
</string-array>
<string-array name="soundcore_aerofit_auto_power_off_names">
<item>@string/pref_button_action_disabled</item>
<item>@string/minutes_10</item>
<item>@string/minutes_20</item>
<item>@string/minutes_30</item>
<item>@string/minutes_60</item>
</string-array>
<string-array name="soundcore_auto_power_off_values">
<item>0</item>
<item>1</item>
+7
View File
@@ -2012,6 +2012,7 @@
<string name="devicetype_soundcore_liberty4_nc" translatable="false">Soundcore Liberty 4 NC</string>
<string name="devicetype_soundcore_motion300" translatable="false">Soundcore Motion 300</string>
<string name="devicetype_soundcore_q30" translatable="false">Soundcore Q30</string>
<string name="devicetype_soundcore_aerofit2" translatable="false">Soundcore AeroFit 2</string>
<string name="devicetype_pixel_buds_a_series" translatable="false">Pixel Buds A-Series</string>
<string name="devicetype_moondrop_space_travel" translatable="false">Moondrop Space Travel</string>
<string name="devicetype_binary_sensor">Binary sensor</string>
@@ -3235,6 +3236,12 @@
<string name="soundcore_button_brightness_high">High</string>
<string name="soundcore_ldac_mode_title">LDAC Mode</string>
<string name="soundcore_ldac_mode_summary">Enabling LDAC will decrease the battery life and might lead to connection instability</string>
<string name="soundcore_auto_power_off_title">Auto Power Off</string>
<string name="soundcore_auto_power_off_summary">When disconnectiong from the phone, the device will power off after the specified timeout</string>
<string name="soundcore_battery_low_tone_title">Low Battery Tone</string>
<string name="soundcore_battery_low_tone_summary">Hear the prompt tone when the battery level is below 20% and 10%</string>
<string name="soundcore_gaming_mode_title">Gaming Mode</string>
<string name="soundcore_gaming_mode_summary">Reduce Latency and enhance sounds of footsteps and voices. May reduce connection stability.</string>
<string name="soundcore_adaptive_direction_title">Adaptive Direction</string>
<string name="soundcore_adaptive_direction_summary">Adjust equalizer preset automatically based on device direction</string>
<string name="soundcore_equalizer_preset">Preset</string>
@@ -0,0 +1,47 @@
<?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_sound"
android:title="@string/pref_header_sound">
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_gaming_mode"
android:layout="@layout/preference_checkbox"
android:summary="@string/soundcore_gaming_mode_summary"
android:title="@string/soundcore_gaming_mode_title"/>
</PreferenceCategory>
<PreferenceCategory
android:key="pref_key_header_soundcore_general"
android:title="@string/pref_header_general">
<ListPreference
android:defaultValue="3"
android:entries="@array/soundcore_aerofit_auto_power_off_names"
android:entryValues="@array/soundcore_auto_power_off_values"
android:icon="@drawable/ic_power_settings_new"
android:key="pref_soundcore_auto_power_off"
android:summary="@string/soundcore_auto_power_off_summary"
android:title="@string/soundcore_auto_power_off_title"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_control_touch_disabled"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_touch_lock_summary"
android:title="@string/prefs_touch_lock"/>
<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"/>
<SwitchPreferenceCompat
android:defaultValue="false"
android:key="pref_soundcore_battery_low_tone"
android:layout="@layout/preference_checkbox"
android:summary="@string/soundcore_battery_low_tone_summary"
android:title="@string/soundcore_battery_low_tone_title"/>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -16,7 +16,7 @@
android:title="@string/soundcore_button_brightness" />
<ListPreference
android:defaultValue="3"
android:entries="@array/soundcore_auto_power_off_names"
android:entries="@array/soundcore_motion_auto_power_off_names"
android:entryValues="@array/soundcore_auto_power_off_values"
android:icon="@drawable/ic_power_settings_new"
android:key="pref_soundcore_auto_power_off"