Add support for EarFun Headphone devices Air S and Air Pro 4

This commit is contained in:
Lars Berning
2025-03-08 14:21:19 +00:00
committed by José Rebelo
parent f35bcc8890
commit d9d0573812
31 changed files with 2200 additions and 0 deletions
@@ -0,0 +1,51 @@
package nodomain.freeyourgadget.gadgetbridge.devices.earfun;
import androidx.annotation.NonNull;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
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.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunSettingsCustomizer;
public abstract class AbstractEarFunCoordinator extends AbstractDeviceCoordinator {
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public String getManufacturer() {
return "EarFun";
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return EarFunDeviceSupport.class;
}
@Override
public int getBondingStyle(){
return BONDING_STYLE_BOND;
}
@Override
public int getDefaultIconResource() {
return R.drawable.ic_device_nothingear;
}
@Override
public int getDisabledIconResource() {
return R.drawable.ic_device_nothingear_disabled;
}
@Override
public DeviceSpecificSettingsCustomizer getDeviceSpecificSettingsCustomizer(final GBDevice device) {
return new EarFunSettingsCustomizer(device);
}
}
@@ -0,0 +1,71 @@
package nodomain.freeyourgadget.gadgetbridge.devices.earfun;
import androidx.annotation.NonNull;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airpro4.EarFunAirPro4DeviceSupport;
public class EarFunAirPro4Coordinator extends AbstractEarFunCoordinator {
@Override
public int getDeviceNameResource() {
return R.string.devicetype_earfun_air_pro_4;
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
if (candidate.getName().startsWith("EarFun Air Pro 4")) {
return true;
}
// can't only check with name, because the device name can be changed
// via the device settings, so we use some of the UUIDs available on the device
// and the mac address prefix to hopefully detect this model reliable
String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
String[] uuidPrefixes = {"0000180f", "0000180a"};
boolean allServicesSupported = Arrays.stream(uuidPrefixes)
.map(uuidPrefix -> UUID.fromString(uuidPrefix + UUID_SUFFIX))
.map(candidate::supportsService).allMatch(b -> b);
boolean macAddressMatches = candidate.getMacAddress().toUpperCase().startsWith("70:5A:6F");
return allServicesSupported && macAddressMatches;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return EarFunAirPro4DeviceSupport.class;
}
@Override
public int getBatteryCount(final GBDevice device) {
return 3;
}
@Override
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
BatteryConfig battery1 = new BatteryConfig(2, R.drawable.ic_buds_pro_case, R.string.battery_case);
BatteryConfig battery2 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
BatteryConfig battery3 = new BatteryConfig(1, 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_earfun_air_pro_4_headphones);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_air_pro_4_gestures);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_device_name);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_10_band_equalizer);
return deviceSpecificSettings;
}
}
@@ -0,0 +1,77 @@
package nodomain.freeyourgadget.gadgetbridge.devices.earfun;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettings;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryConfig;
import nodomain.freeyourgadget.gadgetbridge.service.DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airs.EarFunAirSDeviceSupport;
public class EarFunAirSCoordinator extends AbstractEarFunCoordinator {
private static final Logger LOG = LoggerFactory.getLogger(EarFunAirSCoordinator.class);
@Override
public int getDeviceNameResource() {
return R.string.devicetype_earfun_air_s;
}
@Override
public boolean supports(GBDeviceCandidate candidate) {
if (candidate.getName().startsWith("EarFun Air S")) {
return true;
}
// can't only check with name, because the device name can be changed
// via the device settings, so we use some of the UUIDs available on the device
// an the mac address prefix to hopefully detect this model reliable
String UUID_SUFFIX = "-0000-1000-8000-00805f9b34fb";
String[] uuidPrefixes = {"00001101", "0000111e", "0000110b", "0000110e", "00000000"};
boolean allServicesSupported = Arrays.stream(uuidPrefixes)
.map(uuidPrefix -> UUID.fromString(uuidPrefix + UUID_SUFFIX))
.map(candidate::supportsService).allMatch(b -> b);
boolean macAddressMatches = candidate.getMacAddress().toUpperCase().startsWith("A8:99:DC");
return allServicesSupported && macAddressMatches;
}
@NonNull
@Override
public Class<? extends DeviceSupport> getDeviceSupportClass() {
return EarFunAirSDeviceSupport.class;
}
@Override
public int getBatteryCount(final GBDevice device) {
return 2;
}
@Override
public BatteryConfig[] getBatteryConfig(final GBDevice device) {
BatteryConfig battery1 = new BatteryConfig(0, R.drawable.ic_nothing_ear_l, R.string.left_earbud);
BatteryConfig battery2 = new BatteryConfig(1, R.drawable.ic_nothing_ear_r, R.string.right_earbud);
return new BatteryConfig[]{battery1, battery2};
}
@Override
public DeviceSpecificSettings getDeviceSpecificSettings(final GBDevice device) {
final DeviceSpecificSettings deviceSpecificSettings = new DeviceSpecificSettings();
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_air_s_headphones);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_air_s_gestures);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_device_name);
deviceSpecificSettings.addRootScreen(R.xml.devicesettings_earfun_6_band_equalizer);
return deviceSpecificSettings;
}
}
@@ -48,6 +48,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.colmi.ColmiR10Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.cycling_sensor.coordinator.CyclingSensorCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.divoom.PixooCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.domyos.DomyosT540Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.earfun.EarFunAirPro4Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.earfun.EarFunAirSCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.evenrealities.G1DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.femometer.FemometerVinca2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.fitpro.FitProDeviceCoordinator;
@@ -611,6 +613,8 @@ public enum DeviceType {
COLMI_R09(ColmiR09Coordinator.class),
COLMI_R10(ColmiR10Coordinator.class),
B_AND_W_P_SERIES(BandWPSeriesDeviceCoordinator.class),
EARFUN_AIR_S(EarFunAirSCoordinator.class),
EARFUN_AIR_PRO_4(EarFunAirPro4Coordinator.class),
SCANNABLE(ScannableDeviceCoordinator.class),
CYCLING_SENSOR(CyclingSensorCoordinator.class),
BLE_GATT_CLIENT(BleGattClientCoordinator.class),
@@ -0,0 +1,42 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import nodomain.freeyourgadget.gadgetbridge.service.AbstractHeadphoneDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class EarFunDeviceSupport extends AbstractHeadphoneDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(EarFunDeviceSupport.class);
@Override
public void onSendConfiguration(String config) {
super.onSendConfiguration(config);
}
@Override
public void onTestNewFunction() {
super.onTestNewFunction();
}
@Override
public synchronized EarFunIOThread getDeviceIOThread() {
return (EarFunIOThread) super.getDeviceIOThread();
}
@Override
public boolean useAutoConnect() {
return false;
}
protected GBDeviceProtocol createDeviceProtocol() {
return new EarFunProtocol(getDevice());
}
@Override
protected GBDeviceIoThread createDeviceIOThread() {
return new EarFunIOThread(getDevice(), getContext(), (EarFunProtocol) getDeviceProtocol(),
EarFunDeviceSupport.this, getBluetoothAdapter());
}
}
@@ -0,0 +1,55 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
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 EarFunIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(EarFunIOThread.class);
private final EarFunProtocol earFunProtocol;
public static final UUID GAIA_SPP_UUID = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");
private static final UUID GAIA_UUID = UUID.fromString("00001107-D102-11E1-9B23-00025B00A5A5");
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return GAIA_SPP_UUID;
}
public EarFunIOThread(GBDevice device, Context context, EarFunProtocol deviceProtocol,
EarFunDeviceSupport earFunDeviceSupport, BluetoothAdapter bluetoothAdapter) {
super(device, context, deviceProtocol, earFunDeviceSupport, bluetoothAdapter);
earFunProtocol = deviceProtocol;
}
@Override
protected void initialize() {
super.initialize();
write(earFunProtocol.encodeFirmwareVersionReq());
write(earFunProtocol.encodeBatteryReq());
write(earFunProtocol.encodeSoundReq());
write(earFunProtocol.encodeTouchActionReq());
}
@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,295 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import androidx.annotation.NonNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
public class EarFunPacket {
/*
EarFun uses the structure of Gaia packages, a checksum is never used
0 bytes 1 2 3 4 5 6 7 8 9 len+8
+--------+---------+--------+--------+--------+--------+--------+--------+ +--------+--------+
| SOP | VERSION | FLAGS | LENGTH | VENDOR ID | COMMAND ID | | PAYLOAD ... |
+--------+---------+--------+--------+--------+--------+--------+--------+ +--------+--------+
*/
private static final Logger LOG = LoggerFactory.getLogger(EarFunPacket.class);
private static final int HEADER_LENGTH = 8;
private static final byte START_OF_PACKET = (byte) 0xff;
public static final byte DEFAULT_VERSION = (byte) 0x04;
private static final List<Byte> SUPPORTED_VERSIONS = Arrays.asList(DEFAULT_VERSION, (byte) 0x03);
private static final byte FLAGS_NO_CHECKSUM = (byte) 0x00;
public static final short DEFAULT_VENDOR_ID = (short) 0x000A;
public static final short OTHER_VENDOR_ID = (short) 0x001D;
private static final List<Short> SUPPORTED_VENDOR_IDS = Arrays.asList(DEFAULT_VENDOR_ID, OTHER_VENDOR_ID);
static final int COMMAND_MASK = 0x7FFF;
static final int ACK_MASK = 0x8000;
public static final int COMMAND_INTENT_GET = 0x0080;
public static final int COMMAND_TYPE_MASK = 0x7F00;
public static final int COMMAND_CODE_MASK = 0x00FF;
public static final byte TYPE_CONFIGURATION = (byte) 0x01;
public static final byte TYPE_CONTROL = (byte) 0x02;
public static final byte TYPE_STATUS = (byte) 0x03;
public static final byte TYPE_FEATURE = (byte) 0x05;
public static final byte TYPE_DATA_TRANSFER = (byte) 0x06;
public static final byte TYPE_DEBUG = (byte) 0x07;
public static final byte TYPE_NOTIFICATION = (byte) 0x40;
public static final byte TYPE_ACKNOWLEDGE = (byte) 0x80;
public enum Command {
/*
A Command has the following structure:
0 bytes 1 2
+--------+---------+
| Type | Code |
+--------+---------+
to bring some structure into the commands, we use the following naming conventions:
REQUEST_.. commands to request a status, payload is usually empty in this case
RESPONSE_.. commands that are send back from the headphones as response to a REQUEST
SET_.. commands that can be used to set a value
*/
REQUEST_CONFIGURATION((short) 0x0001, OTHER_VENDOR_ID),
REQUEST_CONFIGURATION2((short) 0x0007, OTHER_VENDOR_ID),
REQUEST_018D((short) 0x000D, OTHER_VENDOR_ID),
// Pro 4: 000D01050108070103060207010005
RESPONSE_CONFIGURATION((short) 0x0101, OTHER_VENDOR_ID), // 00030108010103060207010004
UNIDENTIFIED_0107((short) 0x0107, OTHER_VENDOR_ID), // response, no payload
RESPONSE_CONFIGURATION2((short) 0x0187, OTHER_VENDOR_ID), // 00
RESPONSE_018D((short) 0x018D, OTHER_VENDOR_ID), // 05
UNIDENTIFIED_0282((short) 0x0282, OTHER_VENDOR_ID), // 01
REQUEST_RESPONSE_0300((short) 0x0300), // 00030301
REQUEST_RESPONSE_BATTERY_STATE_LEFT((short) 0x0306),
REQUEST_RESPONSE_BATTERY_STATE_RIGHT((short) 0x0307),
REQUEST_RESPONSE_FIRMWARE_VERSION((short) 0x0309),
SET_GAME_MODE((short) 0x0312),
REQUEST_RESPONSE_GAME_MODE((short) 0x0313),
SET_AMBIENT_SOUND((short) 0x0314),
REQUEST_RESPONSE_AMBIENT_SOUND((short) 0x0315),
SET_DEVICENAME((short) 0x0316),
REQUEST_RESPONSE_BATTERY_STATE_CASE((short) 0x0317),
REQUEST_RESPONSE_0318((short) 0x0318), // 0000 or 0001
UNIDENTIFIED_0321((short) 0x0321), // Pro 4
UNIDENTIFIED_0326((short) 0x0326), // 0001 Pro 4
SET_TOUCH_ACTION((short) 0x030A),
REQUEST_RESPONSE_TOUCH_ACTION((short) 0x030B),
UNIDENTIFIED_032C((short) 0x032C), // 00004C47205137000000000000000000000000000000006DC3F201 Pro 4
UNIDENTIFIED_032F((short) 0x032F), // 0013 Pro 4 // check
UNIDENTIFIED_0331((short) 0x0331), // 0000 Pro 4 // check
UNIDENTIFIED_0333((short) 0x0333), // 0000 Pro 4 // check
UNIDENTIFIED_0335((short) 0x0335), // 0000 Pro 4 // check
UNIDENTIFIED_0339((short) 0x0339), // 0001 Pro 4 // check
SET_ANC_MODE((short) 0x033A),
REQUEST_RESPONSE_ANC_MODE((short) 0x033B),
SET_TRANSPARENCY_MODE((short) 0x033C),
REQUEST_RESPONSE_TRANSPARENCY_MODE((short) 0x033D),
UNIDENTIFIED_034A((short) 0x034A), // 0000 Pro 4
UNIDENTIFIED_034C((short) 0x034C), // 0000 Pro 4
UNIDENTIFIED_034D((short) 0x034D), // 0003 Pro 4
UNIDENTIFIED_0348((short) 0x0348), // no REQUEST
UNIDENTIFIED_0350((short) 0x0350), // 0001 Pro 4
SET_EQUALIZER_BAND((short) 0x0E01, OTHER_VENDOR_ID), // answers with UNIDENTIFIED_0F81
UNIDENTIFIED_0E80((short) 0x0E80, OTHER_VENDOR_ID), // 01
UNIDENTIFIED_0F81((short) 0x0F81, OTHER_VENDOR_ID), // After setting EQ Band 01
UNIDENTIFIED_1080((short) 0x1080, OTHER_VENDOR_ID), // 0100 -> 0101 if ANC not off
UNIDENTIFIED_1081((short) 0x1081, OTHER_VENDOR_ID), // 01010000 <- otherwise, 0A010000 <- ANC Transparent
UNIDENTIFIED_1082((short) 0x1082, OTHER_VENDOR_ID), // 01010000 -> 01014646
UNIDENTIFIED_1083((short) 0x1083, OTHER_VENDOR_ID), // 0101, 0205, 03FF,
UNIDENTIFIED_1084((short) 0x1084, OTHER_VENDOR_ID), // 01FF, 02FF, 03FF, 04FF,
UNIDENTIFIED_1085((short) 0x1085, OTHER_VENDOR_ID), // 00
UNIDENTIFIED_8300((short) 0x8300), // Pro 4
UNIDENTIFIED_8306((short) 0x8306); // Pro 4
public final short commandId;
public final short vendorId;
public final byte version;
Command(short commandId) {
this(commandId, DEFAULT_VENDOR_ID, DEFAULT_VERSION);
}
Command(short commandId, short vendorId) {
this(commandId, vendorId, DEFAULT_VERSION);
}
Command(short commandId, short vendorId, byte version) {
this.commandId = commandId;
this.vendorId = vendorId;
this.version = version;
}
public short getCommand() {
return (short) (commandId & COMMAND_MASK);
}
public byte getType() {
return (byte) ((commandId & COMMAND_TYPE_MASK) >> 8);
}
public byte getCode() {
return (byte) ((commandId & COMMAND_CODE_MASK));
}
public boolean isAcknowledgement() {
return (commandId & ACK_MASK) != 0;
}
public boolean isIntentGet() {
return (commandId & COMMAND_INTENT_GET) != 0;
}
public static String describeType(byte type) {
switch (type) {
case TYPE_CONFIGURATION:
return "configuration";
case TYPE_CONTROL:
return "control";
case TYPE_STATUS:
return "status";
case TYPE_FEATURE:
return "feature";
case TYPE_DATA_TRANSFER:
return "datatransfer";
case TYPE_DEBUG:
return "debug";
case TYPE_NOTIFICATION:
return "notification";
case TYPE_ACKNOWLEDGE:
return "acknowledge";
default:
return "unknown";
}
}
public static String describeCommandId(short commandId) {
byte type = (byte) ((commandId & COMMAND_TYPE_MASK) >> 8);
byte code = (byte) ((commandId & COMMAND_CODE_MASK));
return "Command{" +
" commandId=" + hexdumpValue(commandId) +
", type=" + hexdumpValue(type) + String.format(" (%s)", describeType(type)) +
", code=" + hexdumpValue(code) +
" }";
}
public static Command getCommandById(short commandId) {
short commandValue = (short) (commandId & COMMAND_MASK);
return Arrays.stream(Command.values())
.filter(command -> command.commandId == commandValue)
.findFirst()
.orElseGet(() -> {
LOG.error("unknown command: {}", describeCommandId(commandId));
return null;
});
}
@NonNull
@Override
public String toString() {
return describeCommandId(commandId);
}
}
private final Command command;
private final byte[] payload;
public EarFunPacket(Command command) {
this(command, new byte[0]);
}
public EarFunPacket(Command command, byte payload) {
this(command, new byte[]{payload});
}
public EarFunPacket(Command command, byte[] payload) {
this.command = command;
this.payload = payload;
}
public Command getCommand() {
return command;
}
public byte[] getPayload() {
return payload;
}
public static EarFunPacket decode(ByteBuffer buf) {
if (buf.remaining() < HEADER_LENGTH)
return null;
if (buf.get() != START_OF_PACKET) {
LOG.error("Invalid start of packet: {}", hexdump(buf.array()));
return null;
}
byte version = buf.get();
if (!SUPPORTED_VERSIONS.contains(version)) {
LOG.error("Invalid version: {} in packet {}", hexdumpValue(version), hexdump(buf.array()));
return null;
}
byte flags = buf.get();
if (flags != FLAGS_NO_CHECKSUM) {
LOG.error("Invalid flags: {} in packet {}", hexdumpValue(flags), hexdump(buf.array()));
return null;
}
byte length = buf.get();
short vendorId = buf.getShort();
if (!SUPPORTED_VENDOR_IDS.contains(vendorId)) {
LOG.error("Invalid vendor ID: {} in packet {}", hexdumpValue(vendorId), hexdump(buf.array()));
return null;
}
short commandId = buf.getShort();
Command command = Command.getCommandById(commandId);
if (command == null) {
LOG.error("Received unknown command ID: {} in packet {}", hexdumpValue(commandId), hexdump(buf.array()));
return null;
}
byte[] payload = new byte[length];
buf.get(payload);
return new EarFunPacket(command, payload);
}
public byte[] encode() {
ByteBuffer buf = ByteBuffer.allocate(HEADER_LENGTH + payload.length);
buf.put(START_OF_PACKET);
buf.put(command.version);
buf.put(FLAGS_NO_CHECKSUM);
buf.put((byte) payload.length);
buf.putShort(command.vendorId);
buf.putShort(command.commandId);
buf.put(payload);
LOG.debug("encoded package: {}", hexdump(buf.array()));
return buf.array();
}
public static String hexdumpValue(byte value) {
return String.format("%02X", value & 0xFF);
}
public static String hexdumpValue(short value) {
return String.format("%04X", value & 0xFFFF);
}
@NonNull
@Override
public String toString() {
return "EarFunGaiaPacket{" +
" command=" + command.toString() +
", length=" + payload.length +
", payload=" + hexdump(payload) +
'}';
}
}
@@ -0,0 +1,102 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Interactions;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunPacketEncoder {
private static final Logger LOG = LoggerFactory.getLogger(EarFunPacketEncoder.class);
public static byte[] encodeBatteryReq() {
return joinPackets(
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_BATTERY_STATE_LEFT).encode(),
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_BATTERY_STATE_RIGHT).encode(),
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_BATTERY_STATE_CASE).encode()
);
}
public static byte[] encodeSoundReq() {
return joinPackets(
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_GAME_MODE).encode(),
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_AMBIENT_SOUND).encode(),
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_ANC_MODE).encode(),
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_TRANSPARENCY_MODE).encode()
);
}
public static byte[] encodeTouchActionReq() {
return joinPackets(new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_TOUCH_ACTION).encode());
}
public static byte[] encodeFirmwareVersionReq() {
return new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_FIRMWARE_VERSION).encode();
}
public static byte[] encodeSetGesture(Prefs prefs, String config, Interactions.InteractionType interactionType, Interactions.Position position) {
byte action = (byte) (Integer.parseInt(prefs.getString(config, "0")) & 0xFF);
byte[] payload;
if (position == Interactions.Position.LEFT) {
payload = new byte[]{interactionType.value, action, (byte) 0x00, (byte) 0x00};
} else {
payload = new byte[]{(byte) 0x00, (byte) 0x00, interactionType.value, action};
}
return new EarFunPacket(EarFunPacket.Command.SET_TOUCH_ACTION, payload).encode();
}
public static byte[] encodeSetEqualizerSixBands(Prefs prefs) {
List<byte[]> equalizerConfig = Arrays.stream(Equalizer.SixBandEqualizer).map(bandConfig -> {
if (bandConfig.key != null) {
return encodeSetEqualizerBand((short) (prefs.getInt(bandConfig.key, 0) & 0xFFFF), bandConfig.band);
} else {
return encodeSetEqualizerBand(bandConfig.band.defaultGain, bandConfig.band);
}
}).collect(Collectors.toList());
return joinPackets(equalizerConfig);
}
public static byte[] encodeSetEqualizerTenBands(Prefs prefs) {
List<byte[]> equalizerConfig = Arrays.stream(Equalizer.TenBandEqualizer).map(bandConfig -> {
if (bandConfig.key != null) {
return encodeSetEqualizerBand((short) (prefs.getInt(bandConfig.key, 0) & 0xFFFF), bandConfig.band);
} else {
return encodeSetEqualizerBand(bandConfig.band.defaultGain, bandConfig.band);
}
}).collect(Collectors.toList());
return joinPackets(equalizerConfig);
}
public static byte[] encodeSetEqualizerBand(double gainValue, Equalizer.Band band) {
short gain = (short) ((int) Math.round(gainValue * 60) & 0xFFFF);
ByteBuffer buf = ByteBuffer.allocate(9);
buf.put(band.bandId);
buf.put((byte) 0xFF);
buf.put((byte) 0x98);
buf.putShort(band.frequency);
buf.putShort(gain);
buf.putShort(band.qFactor);
byte[] payload = buf.array();
LOG.debug("equalizer payload: {}", hexdump(payload));
return new EarFunPacket(EarFunPacket.Command.SET_EQUALIZER_BAND, payload).encode();
}
public static byte[] joinPackets(List<byte[]> arrays) {
int totalLength = arrays.stream().mapToInt(array -> array.length).sum();
ByteBuffer byteBuffer = ByteBuffer.allocate(totalLength);
arrays.forEach(byteBuffer::put);
return byteBuffer.array();
}
public static byte[] joinPackets(byte[]... arrays) {
return joinPackets(Arrays.asList(arrays));
}
}
@@ -0,0 +1,169 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunPacketEncoder.joinPackets;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunResponseHandler.*;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.*;
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 nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Interactions;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(EarFunProtocol.class);
@Override
public GBDeviceEvent[] decodeResponse(byte[] data) {
List<GBDeviceEvent> events = new ArrayList<>();
ByteBuffer buf = ByteBuffer.wrap(data);
while (buf.hasRemaining()) {
EarFunPacket packet = EarFunPacket.decode(buf);
if (packet == null) break;
EarFunPacket.Command command = packet.getCommand();
byte[] payload = packet.getPayload();
LOG.info("received {} {}", packet.getCommand().name(), packet);
switch (command) {
case REQUEST_RESPONSE_BATTERY_STATE_LEFT:
events.add(handleBatteryInfo(0, payload));
break;
case REQUEST_RESPONSE_BATTERY_STATE_RIGHT:
events.add(handleBatteryInfo(1, payload));
break;
case REQUEST_RESPONSE_BATTERY_STATE_CASE:
events.add(handleBatteryInfo(2, payload));
break;
case REQUEST_RESPONSE_FIRMWARE_VERSION:
events.add(handleFirmwareVersionInfo(payload));
break;
case REQUEST_RESPONSE_GAME_MODE:
events.add(handleGameModeInfo(payload));
break;
case REQUEST_RESPONSE_AMBIENT_SOUND:
events.add(handleAmbientSoundInfo(payload));
break;
case REQUEST_RESPONSE_ANC_MODE:
events.add(handleAncModeInfo(payload));
break;
case REQUEST_RESPONSE_TRANSPARENCY_MODE:
events.add(handleTransparencyModeInfo(payload));
break;
case REQUEST_RESPONSE_TOUCH_ACTION:
events.add(handleTouchActionInfo(payload));
break;
default:
LOG.error("no handler for packet type {}", packet.getCommand().name());
}
}
return events.toArray(new GBDeviceEvent[0]);
}
@Override
public byte[] encodeTestNewFunction() {
return joinPackets(
new EarFunPacket(EarFunPacket.Command.REQUEST_RESPONSE_0318).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0321).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0326).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_032C).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_032F).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0331).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0333).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0335).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0339).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_034A).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_034C).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_034D).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0348).encode(),
new EarFunPacket(EarFunPacket.Command.UNIDENTIFIED_0350).encode()
);
}
@Override
public byte[] encodeSendConfiguration(String config) {
byte[] customConfiguration = encodeSendConfigurationCustomizer(config);
if (customConfiguration != null) {
return customConfiguration;
}
Prefs prefs = getDevicePrefs();
switch (config) {
case PREF_EARFUN_AMBIENT_SOUND_CONTROL:
int ambientSound = Integer.parseInt(prefs.getString(PREF_EARFUN_AMBIENT_SOUND_CONTROL, "0"));
return new EarFunPacket(EarFunPacket.Command.SET_AMBIENT_SOUND, (byte) ambientSound).encode();
case PREF_EARFUN_SINGLE_TAP_LEFT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.SINGLE, Interactions.Position.LEFT);
case PREF_EARFUN_SINGLE_TAP_RIGHT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.SINGLE, Interactions.Position.RIGHT);
case PREF_EARFUN_DOUBLE_TAP_LEFT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.DOUBLE, Interactions.Position.LEFT);
case PREF_EARFUN_DOUBLE_TAP_RIGHT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.DOUBLE, Interactions.Position.RIGHT);
case PREF_EARFUN_TRIPPLE_TAP_LEFT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.TRIPLE, Interactions.Position.LEFT);
case PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.TRIPLE, Interactions.Position.RIGHT);
case PREF_EARFUN_LONG_TAP_LEFT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.LONG, Interactions.Position.LEFT);
case PREF_EARFUN_LONG_TAP_RIGHT_ACTION:
return EarFunPacketEncoder.encodeSetGesture(prefs, config, Interactions.InteractionType.LONG, Interactions.Position.RIGHT);
case PREF_EARFUN_GAME_MODE:
int gameMode = prefs.getBoolean(PREF_EARFUN_GAME_MODE, false) ? 1 : 0;
return new EarFunPacket(EarFunPacket.Command.SET_GAME_MODE, (byte) gameMode).encode();
case PREF_EARFUN_DEVICE_NAME:
String deviceName = prefs.getString(PREF_EARFUN_DEVICE_NAME, "");
byte[] utf8EncodedName = deviceName.getBytes(StandardCharsets.UTF_8);
return new EarFunPacket(EarFunPacket.Command.SET_DEVICENAME, utf8EncodedName).encode();
case PREF_EARFUN_ANC_MODE:
byte ancMode = (byte) (Integer.parseInt(prefs.getString(PREF_EARFUN_ANC_MODE, "0")) & 0xFF);
return new EarFunPacket(EarFunPacket.Command.SET_ANC_MODE, ancMode).encode();
case PREF_EARFUN_TRANSPARENCY_MODE:
byte transparencyMode = (byte) (Integer.parseInt(prefs.getString(PREF_EARFUN_TRANSPARENCY_MODE, "0")) & 0xFF);
return new EarFunPacket(EarFunPacket.Command.SET_TRANSPARENCY_MODE, transparencyMode).encode();
default:
LOG.error("unhandled send configuration {}", config);
}
return null;
}
/**
* Overwrite this in derived classes to do some device specific custom handling of configurations
*
* @return the config, encoded as byte array
*/
public byte[] encodeSendConfigurationCustomizer(String config) {
return null;
}
public byte[] encodeBatteryReq() {
return EarFunPacketEncoder.encodeBatteryReq();
}
public byte[] encodeSoundReq() {
return EarFunPacketEncoder.encodeSoundReq();
}
public byte[] encodeTouchActionReq() {
return EarFunPacketEncoder.encodeTouchActionReq();
}
@Override
public byte[] encodeFirmwareVersionReq() {
return EarFunPacketEncoder.encodeFirmwareVersionReq();
}
protected EarFunProtocol(GBDevice device) {
super(device);
}
}
@@ -0,0 +1,76 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_AMBIENT_SOUND_CONTROL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_ANC_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_GAME_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_TRANSPARENCY_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Interactions.interactionPrefs;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
public class EarFunResponseHandler {
public static GBDeviceEvent handleBatteryInfo(int index, byte[] payload) {
int batteryLevel = payload[1];
GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo();
batteryInfo.batteryIndex = index;
batteryInfo.level = (batteryLevel > 0) ? batteryLevel : GBDevice.BATTERY_UNKNOWN;
batteryInfo.state = (batteryLevel > 0) ? BatteryState.BATTERY_NORMAL : BatteryState.UNKNOWN;
return batteryInfo;
}
public static GBDeviceEvent handleFirmwareVersionInfo(byte[] payload) {
final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo();
String[] versionParts = (new String(payload, StandardCharsets.UTF_8)).split("_");
versionInfo.fwVersion = versionParts[versionParts.length - 1];
if (versionParts.length > 1) {
versionInfo.hwVersion = String.join(" ", Arrays.copyOf(versionParts, versionParts.length - 1));
} else {
versionInfo.hwVersion = GBApplication.getContext().getString(R.string.n_a);
}
return versionInfo;
}
public static GBDeviceEvent handleGameModeInfo(byte[] payload) {
boolean gameMode = payload[1] == 0x01;
return new GBDeviceEventUpdatePreferences(PREF_EARFUN_GAME_MODE, gameMode);
}
public static GBDeviceEvent handleAmbientSoundInfo(byte[] payload) {
String ambientSoundMode = Integer.toString(payload[1]);
return new GBDeviceEventUpdatePreferences(PREF_EARFUN_AMBIENT_SOUND_CONTROL, ambientSoundMode);
}
public static GBDeviceEvent handleAncModeInfo(byte[] payload) {
String ancMode = Integer.toString(payload[1]);
return new GBDeviceEventUpdatePreferences(PREF_EARFUN_ANC_MODE, ancMode);
}
public static GBDeviceEvent handleTransparencyModeInfo(byte[] payload) {
String transparencyMode = Integer.toString(payload[1]);
return new GBDeviceEventUpdatePreferences(PREF_EARFUN_TRANSPARENCY_MODE, transparencyMode);
}
public static GBDeviceEvent handleTouchActionInfo(byte[] payload) {
GBDeviceEventUpdatePreferences updateEvent = new GBDeviceEventUpdatePreferences();
ByteBuffer buf = ByteBuffer.wrap(payload);
buf.get();
Arrays.stream(interactionPrefs).forEach(interactionType -> {
// only use the last byte - the first byte initially contains some non zero data
int action = buf.getShort() & 0xFF;
updateEvent.preferences.put(interactionType, Integer.toString(action));
});
return updateEvent;
}
}
@@ -0,0 +1,146 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.*;
import android.os.Parcel;
import android.text.InputFilter;
import android.text.Spanned;
import androidx.annotation.NonNull;
import androidx.preference.EditTextPreference;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.Set;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsCustomizer;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSpecificSettingsHandler;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunSettingsCustomizer implements DeviceSpecificSettingsCustomizer {
final GBDevice device;
public EarFunSettingsCustomizer(final GBDevice device) {
this.device = device;
}
public static final Creator<EarFunSettingsCustomizer> CREATOR = new Creator<EarFunSettingsCustomizer>() {
@Override
public EarFunSettingsCustomizer createFromParcel(final Parcel in) {
final GBDevice device = in.readParcelable(EarFunSettingsCustomizer.class.getClassLoader());
return new EarFunSettingsCustomizer(device);
}
@Override
public EarFunSettingsCustomizer[] newArray(final int size) {
return new EarFunSettingsCustomizer[size];
}
};
private static final Logger LOG = LoggerFactory.getLogger(EarFunSettingsCustomizer.class);
@Override
public void onPreferenceChange(Preference preference, DeviceSpecificSettingsHandler handler) {
if (preference.getKey().equals(PREF_EARFUN_AMBIENT_SOUND_CONTROL)) {
ListPreference listPreferenceAmbientSound = handler.findPreference(PREF_EARFUN_AMBIENT_SOUND_CONTROL);
ListPreference listPreferenceTransparencyMode = handler.findPreference(PREF_EARFUN_TRANSPARENCY_MODE);
ListPreference listPreferenceAncMode = handler.findPreference(PREF_EARFUN_ANC_MODE);
if (listPreferenceAmbientSound == null || listPreferenceTransparencyMode == null || listPreferenceAncMode == null) {
return;
}
switch (listPreferenceAmbientSound.getValue()) {
case "1": // noise cancelling
listPreferenceTransparencyMode.setVisible(false);
listPreferenceAncMode.setVisible(true);
break;
case "2": // transparency
listPreferenceTransparencyMode.setVisible(true);
listPreferenceAncMode.setVisible(false);
break;
default:
listPreferenceTransparencyMode.setVisible(false);
listPreferenceAncMode.setVisible(false);
}
}
}
@Override
public void customizeSettings(DeviceSpecificSettingsHandler handler, Prefs prefs, String rootKey) {
handler.addPreferenceHandlerFor(PREF_EARFUN_DEVICE_NAME);
handler.addPreferenceHandlerFor(PREF_EARFUN_AMBIENT_SOUND_CONTROL);
handler.addPreferenceHandlerFor(PREF_EARFUN_TRANSPARENCY_MODE);
handler.addPreferenceHandlerFor(PREF_EARFUN_ANC_MODE);
handler.addPreferenceHandlerFor(PREF_EARFUN_SINGLE_TAP_LEFT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_SINGLE_TAP_RIGHT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_DOUBLE_TAP_LEFT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_DOUBLE_TAP_RIGHT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_TRIPPLE_TAP_LEFT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_LONG_TAP_LEFT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_LONG_TAP_RIGHT_ACTION);
handler.addPreferenceHandlerFor(PREF_EARFUN_GAME_MODE);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_31_5);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_63);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_125);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_180);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_250);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_500);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_1000);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_2000);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_4000);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_8000);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_15000);
handler.addPreferenceHandlerFor(PREF_EARFUN_EQUALIZER_BAND_16000);
EditTextPreference editTextDeviceName = handler.findPreference(PREF_EARFUN_DEVICE_NAME);
if (editTextDeviceName != null) {
editTextDeviceName.setOnBindEditTextListener(editText -> {
InputFilter[] filters = new InputFilter[]{new InputFilterLength(25)};
editText.setFilters(filters);
});
editTextDeviceName.setText(device.getName());
}
}
@Override
public Set<String> getPreferenceKeysWithSummary() {
return Collections.emptySet();
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(@NonNull Parcel parcel, int i) {
}
private static class InputFilterLength implements InputFilter {
private final int maxLength;
public InputFilterLength(int maxLength) {
this.maxLength = maxLength;
}
@Override
public CharSequence filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
int keep = maxLength - (dest.length() - (dend - dstart));
if (keep <= 0) {
return "";
} else if (keep >= end - start) {
return null;
} else {
return source.subSequence(start, start + keep);
}
}
}
}
@@ -0,0 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airpro4;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class EarFunAirPro4DeviceSupport extends EarFunDeviceSupport {
protected GBDeviceProtocol createDeviceProtocol() {
return new EarFunAirPro4Protocol(getDevice());
}
}
@@ -0,0 +1,37 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airpro4;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunPacketEncoder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunProtocol;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunAirPro4Protocol extends EarFunProtocol {
private static final Logger LOG = LoggerFactory.getLogger(EarFunAirPro4Protocol.class);
public byte[] encodeSendConfigurationCustomizer(String config) {
if (containsKey(Equalizer.TenBandEqualizer, config)) {
Prefs prefs = getDevicePrefs();
return EarFunPacketEncoder.encodeSetEqualizerTenBands(prefs);
}
return null;
}
private static boolean containsKey(Equalizer.BandConfig[] array, String key) {
return Arrays.stream(array)
.anyMatch(element -> element.key.equals(key));
}
protected EarFunAirPro4Protocol(GBDevice device) {
super(device);
DeviceType type = device.getType();
}
}
@@ -0,0 +1,11 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airs;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class EarFunAirSDeviceSupport extends EarFunDeviceSupport {
protected GBDeviceProtocol createDeviceProtocol() {
return new EarFunAirSProtocol(getDevice());
}
}
@@ -0,0 +1,36 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.airs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunPacketEncoder;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.EarFunProtocol;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunAirSProtocol extends EarFunProtocol {
private static final Logger LOG = LoggerFactory.getLogger(EarFunAirSProtocol.class);
public byte[] encodeSendConfigurationCustomizer(String config) {
if (containsKey(Equalizer.SixBandEqualizer, config)) {
Prefs prefs = getDevicePrefs();
return EarFunPacketEncoder.encodeSetEqualizerSixBands(prefs);
}
return null;
}
private static boolean containsKey(Equalizer.BandConfig[] array, String key) {
return Arrays.stream(array)
.anyMatch(element -> element.key.equals(key));
}
protected EarFunAirSProtocol(GBDevice device) {
super(device);
DeviceType type = device.getType();
}
}
@@ -0,0 +1,29 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs;
public class EarFunSettingsPreferenceConst {
public static final String PREF_EARFUN_DEVICE_NAME = "pref_earfun_device_name";
public static final String PREF_EARFUN_AMBIENT_SOUND_CONTROL = "pref_earfun_ambient_sound_control";
public static final String PREF_EARFUN_TRANSPARENCY_MODE = "pref_earfun_transparency_mode";
public static final String PREF_EARFUN_ANC_MODE = "pref_earfun_anc_mode";
public static final String PREF_EARFUN_SINGLE_TAP_LEFT_ACTION = "pref_earfun_single_tap_left_action";
public static final String PREF_EARFUN_SINGLE_TAP_RIGHT_ACTION = "pref_earfun_single_tap_right_action";
public static final String PREF_EARFUN_DOUBLE_TAP_LEFT_ACTION = "pref_earfun_double_tap_left_action";
public static final String PREF_EARFUN_DOUBLE_TAP_RIGHT_ACTION = "pref_earfun_double_tap_right_action";
public static final String PREF_EARFUN_TRIPPLE_TAP_LEFT_ACTION = "pref_earfun_tripple_tap_left_action";
public static final String PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION = "pref_earfun_tripple_tap_right_action";
public static final String PREF_EARFUN_LONG_TAP_LEFT_ACTION = "pref_earfun_long_tap_left_action";
public static final String PREF_EARFUN_LONG_TAP_RIGHT_ACTION = "pref_earfun_long_tap_right_action";
public static final String PREF_EARFUN_GAME_MODE = "pref_earfun_game_mode";
public static final String PREF_EARFUN_EQUALIZER_BAND_31_5 = "pref_earfun_equalizer_band_31.5";
public static final String PREF_EARFUN_EQUALIZER_BAND_63 = "pref_earfun_equalizer_band_63";
public static final String PREF_EARFUN_EQUALIZER_BAND_125 = "pref_earfun_equalizer_band_125";
public static final String PREF_EARFUN_EQUALIZER_BAND_180 = "pref_earfun_equalizer_band_180";
public static final String PREF_EARFUN_EQUALIZER_BAND_250 = "pref_earfun_equalizer_band_250";
public static final String PREF_EARFUN_EQUALIZER_BAND_500 = "pref_earfun_equalizer_band_500";
public static final String PREF_EARFUN_EQUALIZER_BAND_1000 = "pref_earfun_equalizer_band_1000";
public static final String PREF_EARFUN_EQUALIZER_BAND_2000 = "pref_earfun_equalizer_band_2000";
public static final String PREF_EARFUN_EQUALIZER_BAND_4000 = "pref_earfun_equalizer_band_4000";
public static final String PREF_EARFUN_EQUALIZER_BAND_8000 = "pref_earfun_equalizer_band_8000";
public static final String PREF_EARFUN_EQUALIZER_BAND_15000 = "pref_earfun_equalizer_band_15000";
public static final String PREF_EARFUN_EQUALIZER_BAND_16000 = "pref_earfun_equalizer_band_16000";
}
@@ -0,0 +1,82 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.*;
public class Equalizer {
public enum Band {
SIX_BAND_63((byte) 0xA1, (short) 0x00BD, (short) 0x0B33),
SIX_BAND_180((byte) 0xA2, (short) 0x021C, (short) 0x0999),
SIX_BAND_500((byte) 0xA3, (short) 0x05DC, (short) 0x0999),
SIX_BAND_1000((byte) 0xA4, (short) 0x0BB8, (short) 0x0999),
SIX_BAND_8000((byte) 0xA5, (short) 0x5DC0, (short) 0x0999),
SIX_BAND_15000((byte) 0xA6, (short) 0xAFC8, (short) 0x0999),
// channels 7 - 10 are never used by the EarFun Air S, but they need to be set with dummy values
SIX_BAND_DUMMY_SEVEN((byte) 0xA7, (short) 0x0063, (short) 0x0800, 4.5),
SIX_BAND_DUMMY_EIGHT((byte) 0xA8, (short) 0x0258, (short) 0x0999, -4.5),
SIX_BAND_DUMMY_NINE((byte) 0xA9, (short) 0x1770, (short) 0x0E66, 1.5),
SIX_BAND_DUMMY_TEN((byte) 0xAA, (short) 0xAFC8, (short) 0x0999, 3.5),
TEN_BAND_31_5((byte) 0xA1, (short) 0x005E, (short) 0x0B33),
TEN_BAND_63((byte) 0xA2, (short) 0x00BD, (short) 0x0B33),
TEN_BAND_125((byte) 0xA3, (short) 0x0177, (short) 0x0B33),
TEN_BAND_250((byte) 0xA4, (short) 0x02EE, (short) 0x0B33),
TEN_BAND_500((byte) 0xA5, (short) 0x05DC, (short) 0x0B33),
TEN_BAND_1000((byte) 0xA6, (short) 0x0BB8, (short) 0x0B33),
TEN_BAND_2000((byte) 0xA7, (short) 0x1770, (short) 0x0B33),
TEN_BAND_4000((byte) 0xA8, (short) 0x2EE0, (short) 0x0B33),
TEN_BAND_8000((byte) 0xA9, (short) 0x5DC0, (short) 0x0B33),
TEN_BAND_16000((byte) 0xAA, (short) 0xBB80, (short) 0x0B33);
public final byte bandId;
public final short frequency;
public final short qFactor;
public final double defaultGain;
Band(byte bandId, short frequency, short qFactor) {
this(bandId, frequency, qFactor, 0);
}
Band(byte bandId, short frequency, short qFactor, double defaultGain) {
this.bandId = bandId;
this.frequency = frequency;
this.qFactor = qFactor;
this.defaultGain = defaultGain;
}
}
public static class BandConfig {
public BandConfig(Band band, String key) {
this.band = band;
this.key = key;
}
public Band band;
public String key;
}
public static BandConfig[] SixBandEqualizer = {
new BandConfig(Band.SIX_BAND_63, PREF_EARFUN_EQUALIZER_BAND_63),
new BandConfig(Band.SIX_BAND_180, PREF_EARFUN_EQUALIZER_BAND_180),
new BandConfig(Band.SIX_BAND_500, PREF_EARFUN_EQUALIZER_BAND_500),
new BandConfig(Band.SIX_BAND_1000, PREF_EARFUN_EQUALIZER_BAND_1000),
new BandConfig(Band.SIX_BAND_8000, PREF_EARFUN_EQUALIZER_BAND_8000),
new BandConfig(Band.SIX_BAND_15000, PREF_EARFUN_EQUALIZER_BAND_15000),
new BandConfig(Band.SIX_BAND_DUMMY_SEVEN, null),
new BandConfig(Band.SIX_BAND_DUMMY_EIGHT, null),
new BandConfig(Band.SIX_BAND_DUMMY_NINE, null),
new BandConfig(Band.SIX_BAND_DUMMY_TEN, null),
};
public static BandConfig[] TenBandEqualizer = {
new BandConfig(Band.TEN_BAND_31_5, PREF_EARFUN_EQUALIZER_BAND_31_5),
new BandConfig(Band.TEN_BAND_63, PREF_EARFUN_EQUALIZER_BAND_63),
new BandConfig(Band.TEN_BAND_125, PREF_EARFUN_EQUALIZER_BAND_125),
new BandConfig(Band.TEN_BAND_250, PREF_EARFUN_EQUALIZER_BAND_250),
new BandConfig(Band.TEN_BAND_500, PREF_EARFUN_EQUALIZER_BAND_500),
new BandConfig(Band.TEN_BAND_1000, PREF_EARFUN_EQUALIZER_BAND_1000),
new BandConfig(Band.TEN_BAND_2000, PREF_EARFUN_EQUALIZER_BAND_2000),
new BandConfig(Band.TEN_BAND_4000, PREF_EARFUN_EQUALIZER_BAND_4000),
new BandConfig(Band.TEN_BAND_8000, PREF_EARFUN_EQUALIZER_BAND_8000),
new BandConfig(Band.TEN_BAND_16000, PREF_EARFUN_EQUALIZER_BAND_16000),
};
}
@@ -0,0 +1,34 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.*;
public class Interactions {
public enum InteractionType {
SINGLE((byte) 0x01),
DOUBLE((byte) 0x02),
TRIPLE((byte) 0x03),
LONG((byte) 0x11);
public final byte value;
InteractionType(byte value) {
this.value = value;
}
}
public enum Position {
LEFT,
RIGHT
}
public static String[] interactionPrefs = {
PREF_EARFUN_SINGLE_TAP_LEFT_ACTION,
PREF_EARFUN_SINGLE_TAP_RIGHT_ACTION,
PREF_EARFUN_DOUBLE_TAP_LEFT_ACTION,
PREF_EARFUN_DOUBLE_TAP_RIGHT_ACTION,
PREF_EARFUN_TRIPPLE_TAP_LEFT_ACTION,
PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION,
PREF_EARFUN_LONG_TAP_LEFT_ACTION,
PREF_EARFUN_LONG_TAP_RIGHT_ACTION
};
}
+94
View File
@@ -3534,6 +3534,100 @@
<item>6</item>
</string-array>
<string-array name="earfun_ambient_sound_control_names">
<item>@string/sony_ambient_sound_off</item>
<item>@string/sony_ambient_sound_noise_cancelling</item>
<item>@string/prefs_active_noise_cancelling_transparency</item>
</string-array>
<string-array name="earfun_ambient_sound_control_values">
<item>0</item>
<item>1</item>
<item>2</item>
</string-array>
<string-array name="earfun_transparency_mode_names">
<item>@string/pref_default</item>
<item>@string/earfun_transparency_mode_natural</item>
</string-array>
<string-array name="earfun_transparency_mode_values">
<item>0</item>
<item>1</item>
</string-array>
<string-array name="earfun_anc_mode_names">
<item>@string/earfun_anc_mode_strong</item>
<item>@string/earfun_anc_mode_balanced</item>
<item>@string/earfun_anc_mode_ai_environment_adaptive</item>
<item>@string/earfun_anc_mode_ai_ear_adaptive</item>
<item>@string/earfun_anc_mode_wind_noise_cancelling</item>
</string-array>
<string-array name="earfun_anc_mode_values">
<item>2</item>
<item>3</item>
<item>4</item>
<item>0</item>
<item>1</item>
</string-array>
<string-array name="earfun_air_s_tap_action_names">
<item>@string/none</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_next</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_playpause</item>
<item>@string/prefs_game_mode</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/prefs_ambient_mode</item>
</string-array>
<string-array name="earfun_air_s_tap_action_values">
<item>16</item>
<item>2</item>
<item>3</item>
<item>5</item>
<item>4</item>
<item>1</item>
<item>7</item>
<item>6</item>
<item>8</item>
</string-array>
<string-array name="earfun_air_pro_4_tap_action_names">
<item>@string/none</item>
<item>@string/pref_media_volumeup</item>
<item>@string/pref_media_volumedown</item>
<item>@string/pref_media_next</item>
<item>@string/pref_media_previous</item>
<item>@string/pref_media_playpause</item>
<item>@string/prefs_game_mode</item>
<item>@string/pref_title_touch_voice_assistant</item>
<item>@string/earfun_tap_action_noise_control_all_modes</item>
<item>@string/earfun_tap_action_noise_control_normal_wind</item>
<item>@string/earfun_tap_action_noise_control_normal_ambient</item>
<item>@string/earfun_tap_action_noise_control_wind_ambient</item>
<item>@string/earfun_tap_action_redial</item>
</string-array>
<string-array name="earfun_air_pro_4_tap_action_values">
<item>16</item>
<item>2</item>
<item>3</item>
<item>5</item>
<item>4</item>
<item>1</item>
<item>7</item>
<item>6</item>
<item>8</item>
<item>9</item>
<item>10</item>
<item>11</item>
<item>13</item>
</string-array>
<string-array name="sony_ambient_sound_control_wind_noise_reduction_names">
<item>@string/sony_ambient_sound_off</item>
<item>@string/sony_ambient_sound_noise_cancelling</item>
+15
View File
@@ -1956,6 +1956,8 @@
<string name="devicetype_colmi_r09">Colmi R09</string>
<string name="devicetype_colmi_r10">Colmi R10</string>
<string name="devicetype_bandw_pseries">Bowers and Wilkins P series</string>
<string name="devicetype_earfun_air_s">EarFun Air S</string>
<string name="devicetype_earfun_air_pro_4">EarFun Air Pro 4</string>
<string name="choose_auto_export_location">Choose export location</string>
<string name="notification_channel_name">General</string>
<string name="notification_channel_high_priority_name">High-priority</string>
@@ -3703,4 +3705,17 @@
<string name="gree_pair_status_success">Pairing success.\n\nBind key: %s</string>
<string name="gree_pair_status_failure">Pairing failed: %s</string>
<string name="gree_pair_clipboard">Device name: %s\nMac address: %s\nBind key: %s</string>
<string name="earfun_transparency_mode">Transparency mode</string>
<string name="earfun_transparency_mode_natural">Natural</string>
<string name="earfun_anc_mode">ANC mode</string>
<string name="earfun_anc_mode_strong">Strong ANC</string>
<string name="earfun_anc_mode_balanced">Balanced ANC</string>
<string name="earfun_anc_mode_ai_environment_adaptive">AI Environment Adaptive ANC</string>
<string name="earfun_anc_mode_ai_ear_adaptive">AI Ear Adaptive ANC</string>
<string name="earfun_anc_mode_wind_noise_cancelling">Wind Noise Cancelling</string>
<string name="earfun_tap_action_noise_control_all_modes">Noise Control (All Modes)</string>
<string name="earfun_tap_action_noise_control_normal_wind">Noise Control (Normal, Wind Noise)</string>
<string name="earfun_tap_action_noise_control_normal_ambient">Noise Control (Normal, Ambient Sound)</string>
<string name="earfun_tap_action_noise_control_wind_ambient">Noise Control (Wind Noise, Ambient Sound)</string>
<string name="earfun_tap_action_redial">Redial</string>
</resources>
@@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_10_band_equalizer"
android:persistent="false"
android:title="@string/pref_header_equalizer">
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_31.5"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="31.5 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_63"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="63 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_125"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="125 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_250"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="250 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_500"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="500 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_1000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="1 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_2000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="2 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_4000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="4 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_8000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="8 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_16000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="16 kHz" />
</PreferenceScreen>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<PreferenceScreen
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_6_band_equalizer"
android:persistent="false"
android:title="@string/pref_header_equalizer">
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_63"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="63 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_180"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="180 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_500"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="500 Hz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_1000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="1 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_8000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="8 kHz" />
<SeekBarPreference
android:defaultValue="0"
android:icon="@drawable/ic_graphic_eq"
android:key="pref_earfun_equalizer_band_15000"
android:max="6"
app:min="-6"
app:showSeekBarValue="true"
android:title="15 kHz" />
</PreferenceScreen>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_header_earfun_air_pro_4_system"
android:title="@string/pref_header_system">
<PreferenceScreen
android:icon="@drawable/ic_touch"
android:key="pref_screen_earfun_touch_options"
android:persistent="false"
android:title="@string/prefs_galaxy_touch_options">
<PreferenceCategory android:title="@string/single_tap">
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:icon="@drawable/ic_filter_1"
android:defaultValue="16"
android:key="pref_earfun_single_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_single_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/double_tap">
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:icon="@drawable/ic_filter_2"
android:defaultValue="16"
android:key="pref_earfun_double_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_double_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/triple_tap">
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:icon="@drawable/ic_filter_3"
android:defaultValue="16"
android:key="pref_earfun_tripple_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_tripple_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/long_press">
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:icon="@drawable/ic_touch"
android:defaultValue="16"
android:key="pref_earfun_long_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_pro_4_tap_action_names"
android:entryValues="@array/earfun_air_pro_4_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_long_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
</PreferenceScreen>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_header_earfun_air_pro_4_ambient_sound_control"
android:title="@string/pref_header_sony_sound_control">
<ListPreference
android:defaultValue="0"
android:entries="@array/earfun_ambient_sound_control_names"
android:entryValues="@array/earfun_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_earfun_ambient_sound_control"
android:summary="%s"
android:title="@string/pref_header_sony_ambient_sound_control" />
<ListPreference
android:defaultValue="0"
android:entries="@array/earfun_transparency_mode_names"
android:entryValues="@array/earfun_transparency_mode_values"
android:icon="@drawable/ic_hearing"
android:key="pref_earfun_transparency_mode"
android:summary="%s"
android:title="@string/earfun_transparency_mode" />
<ListPreference
android:defaultValue="0"
android:entries="@array/earfun_anc_mode_names"
android:entryValues="@array/earfun_anc_mode_values"
android:icon="@drawable/ic_hearing"
android:key="pref_earfun_anc_mode"
android:summary="%s"
android:title="@string/earfun_anc_mode" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_videogame"
android:key="pref_earfun_game_mode"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_game_mode_summary"
android:title="@string/prefs_game_mode" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_header_earfun_air_s_system"
android:title="@string/pref_header_system">
<PreferenceScreen
android:icon="@drawable/ic_touch"
android:key="pref_screen_earfun_touch_options"
android:persistent="false"
android:title="@string/prefs_galaxy_touch_options">
<PreferenceCategory android:title="@string/single_tap">
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:icon="@drawable/ic_filter_1"
android:defaultValue="16"
android:key="pref_earfun_single_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_single_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/double_tap">
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:icon="@drawable/ic_filter_2"
android:defaultValue="16"
android:key="pref_earfun_double_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_double_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/triple_tap">
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:icon="@drawable/ic_filter_3"
android:defaultValue="16"
android:key="pref_earfun_tripple_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_tripple_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
<PreferenceCategory android:title="@string/long_press">
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:icon="@drawable/ic_touch"
android:defaultValue="16"
android:key="pref_earfun_long_tap_left_action"
android:summary="%s"
android:title="@string/prefs_left" />
<ListPreference
android:entries="@array/earfun_air_s_tap_action_names"
android:entryValues="@array/earfun_air_s_tap_action_values"
android:defaultValue="16"
android:key="pref_earfun_long_tap_right_action"
android:summary="%s"
android:title="@string/prefs_right" />
</PreferenceCategory>
</PreferenceScreen>
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:key="pref_header_earfun_air_s_ambient_sound_control"
android:title="@string/pref_header_sony_sound_control">
<ListPreference
android:defaultValue="0"
android:entries="@array/earfun_ambient_sound_control_names"
android:entryValues="@array/earfun_ambient_sound_control_values"
android:icon="@drawable/ic_hearing"
android:key="pref_earfun_ambient_sound_control"
android:summary="%s"
android:title="@string/pref_header_sony_ambient_sound_control" />
<SwitchPreferenceCompat
android:defaultValue="false"
android:icon="@drawable/ic_videogame"
android:key="pref_earfun_game_mode"
android:layout="@layout/preference_checkbox"
android:summary="@string/prefs_game_mode_summary"
android:title="@string/prefs_game_mode" />
</PreferenceCategory>
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<EditTextPreference
android:icon="@drawable/ic_bluetooth"
android:key="pref_earfun_device_name"
android:title="@string/prefs_device_name"
android:inputType="text"
app:useSimpleSummaryProvider="true" />
</androidx.preference.PreferenceScreen>
@@ -0,0 +1,148 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import org.junit.Test;
import java.util.Arrays;
import java.util.List;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Equalizer;
import nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.Interactions;
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
public class EarFunPacketEncoderTest {
Prefs mockPrefs = mock(Prefs.class);
@Test
public void testEncodeSetGesture() {
when(mockPrefs.getString(anyString(), anyString())).thenReturn("1");
byte[] expected = new EarFunPacket(EarFunPacket.Command.SET_TOUCH_ACTION, new byte[]{1, 1, 0, 0}).encode();
assertArrayEquals(expected, EarFunPacketEncoder.encodeSetGesture(mockPrefs, "anyKey",
Interactions.InteractionType.SINGLE,
Interactions.Position.LEFT)
);
}
@Test
public void testEncodeSetEqualizerSixBands() {
for (Equalizer.BandConfig bandConfig : Equalizer.SixBandEqualizer) {
if (bandConfig.key != null) {
when(mockPrefs.getInt(bandConfig.key, 0)).thenReturn(5);
}
}
byte[] result = EarFunPacketEncoder.encodeSetEqualizerSixBands(mockPrefs);
assertNotNull(result);
assertEquals(170, result.length);
}
@Test
public void testEncodeSetEqualizerTenBands() {
for (Equalizer.BandConfig bandConfig : Equalizer.TenBandEqualizer) {
if (bandConfig.key != null) {
when(mockPrefs.getInt(bandConfig.key, 0)).thenReturn(-5);
}
}
byte[] result = EarFunPacketEncoder.encodeSetEqualizerTenBands(mockPrefs);
assertNotNull(result);
assertEquals(170, result.length);
}
@Test
public void testEncodeBatteryReq() {
byte[] encoded = EarFunPacketEncoder.encodeBatteryReq();
assertNotNull(encoded);
assertEquals(24, encoded.length);
}
@Test
public void testEncodeSoundReq() {
byte[] encoded = EarFunPacketEncoder.encodeSoundReq();
assertNotNull(encoded);
assertEquals(32, encoded.length);
}
@Test
public void testEncodeTouchActionReq() {
byte[] encoded = EarFunPacketEncoder.encodeTouchActionReq();
assertNotNull(encoded);
assertEquals(8, encoded.length);
}
@Test
public void testEncodeFirmwareVersionReq() {
byte[] encoded = EarFunPacketEncoder.encodeFirmwareVersionReq();
assertNotNull(encoded);
assertEquals(8, encoded.length);
}
@Test
public void testJoinPacketsMultipleArrays() {
byte[] array1 = {1, 2, 3};
byte[] array2 = {4, 5, 6};
byte[] array3 = {7, 8, 9};
byte[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] result = EarFunPacketEncoder.joinPackets(array1, array2, array3);
assertArrayEquals(expected, result);
}
@Test
public void testJoinPacketsEmptyArray() {
byte[] array1 = {};
byte[] array2 = {1, 2, 3};
byte[] expected = {1, 2, 3};
byte[] result = EarFunPacketEncoder.joinPackets(array1, array2);
assertArrayEquals(expected, result);
}
@Test
public void testJoinPacketsSingleArray() {
byte[] array1 = {1, 2, 3};
byte[] expected = {1, 2, 3};
byte[] result = EarFunPacketEncoder.joinPackets(array1);
assertArrayEquals(expected, result);
}
@Test
public void testJoinPacketsNoArrays() {
byte[] expected = {};
byte[] result = EarFunPacketEncoder.joinPackets();
assertArrayEquals(expected, result);
}
@Test
public void testJoinPacketsDifferentLengths() {
byte[] array1 = {1, 2};
byte[] array2 = {3, 4, 5, 6};
byte[] array3 = {7};
byte[] expected = {1, 2, 3, 4, 5, 6, 7};
byte[] result = EarFunPacketEncoder.joinPackets(array1, array2, array3);
assertArrayEquals(expected, result);
}
@Test
public void testJoinPacketsListInput() {
List<byte[]> arrays = Arrays.asList(new byte[]{1, 2}, new byte[]{3, 4, 5}, new byte[]{6, 7, 8, 9});
byte[] expected = {1, 2, 3, 4, 5, 6, 7, 8, 9};
byte[] result = EarFunPacketEncoder.joinPackets(arrays);
assertArrayEquals(expected, result);
}
}
@@ -0,0 +1,74 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.util.Arrays;
public class EarFunPacketTest {
@Test
public void testEncode() {
EarFunPacket.Command command = EarFunPacket.Command.SET_AMBIENT_SOUND;
byte[] payload = {0x01, 0x02, 0x03};
EarFunPacket packet = new EarFunPacket(command, payload);
byte[] encoded = packet.encode();
assertEquals((byte) 0xff, encoded[0]);
assertEquals(EarFunPacket.DEFAULT_VERSION, encoded[1]);
assertEquals(0x00, encoded[2]);
assertEquals(payload.length, encoded[3]);
assertEquals(EarFunPacket.DEFAULT_VENDOR_ID, ByteBuffer.wrap(encoded, 4, 2).getShort());
assertEquals(command.commandId, ByteBuffer.wrap(encoded, 6, 2).getShort());
byte[] decodedPayload = Arrays.copyOfRange(encoded, 8, encoded.length);
assertArrayEquals(payload, decodedPayload);
}
@Test
public void testDecode() {
byte[] payload = {0x01, 0x02, 0x03};
ByteBuffer buffer = ByteBuffer.allocate(11)
.put((byte) 0xff)
.put(EarFunPacket.DEFAULT_VERSION)
.put((byte) 0x00)
.put((byte) payload.length)
.putShort(EarFunPacket.DEFAULT_VENDOR_ID)
.putShort(EarFunPacket.Command.REQUEST_CONFIGURATION.commandId)
.put(payload);
buffer.flip();
EarFunPacket packet = EarFunPacket.decode(buffer);
assertNotNull(packet);
assertEquals(EarFunPacket.Command.REQUEST_CONFIGURATION, packet.getCommand());
assertArrayEquals(payload, packet.getPayload());
}
@Test
public void testDecodeInvalidPacket() {
ByteBuffer buffer = ByteBuffer.allocate(2)
.put((byte) 0xff)
.put(EarFunPacket.DEFAULT_VERSION);
buffer.flip();
EarFunPacket packet = EarFunPacket.decode(buffer);
assertNull(packet);
}
@Test
public void testGetCommandById() {
EarFunPacket.Command command = EarFunPacket.Command.getCommandById((short) 0x0001);
assertNotNull(command);
assertEquals(EarFunPacket.Command.REQUEST_CONFIGURATION, command);
}
@Test
public void testInvalidCommandId() {
EarFunPacket.Command command = EarFunPacket.Command.getCommandById((short) 0x1234);
assertNull(command);
}
}
@@ -0,0 +1,36 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_GAME_MODE;
public class EarFunProtocolTest {
private EarFunProtocol protocol;
private GBDevice device;
@Before
public void setUp() {
protocol = new EarFunProtocol(device);
}
@Test
public void testDecodeResponse() {
byte[] data = {(byte) 0xFF, 0x03, 0x00, 0x02, 0x00, 0x0A, 0x03, 0x07, 0x00, 0x64};
GBDeviceEvent[] events = protocol.decodeResponse(data);
assertNotNull(events);
assertEquals(1, events.length);
assertTrue(events[0] instanceof GBDeviceEventBatteryInfo);
GBDeviceEventBatteryInfo batteryInfo = (GBDeviceEventBatteryInfo) events[0];
assertEquals(1, batteryInfo.batteryIndex);
assertEquals(100, batteryInfo.level);
}
}
@@ -0,0 +1,99 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.earfun;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_AMBIENT_SOUND_CONTROL;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_ANC_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_GAME_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_SINGLE_TAP_LEFT_ACTION;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_TRANSPARENCY_MODE;
import static nodomain.freeyourgadget.gadgetbridge.service.devices.earfun.prefs.EarFunSettingsPreferenceConst.PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION;
import org.junit.Test;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventUpdatePreferences;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
public class EarFunResponseHandlerTest {
@Test
public void testHandleBatteryInfo() {
byte[] payload = new byte[]{0x00, 0x64};
GBDeviceEvent event = EarFunResponseHandler.handleBatteryInfo(1, payload);
GBDeviceEventBatteryInfo batteryInfo = (GBDeviceEventBatteryInfo) event;
assertEquals(1, batteryInfo.batteryIndex);
assertEquals(100, batteryInfo.level);
assertEquals(BatteryState.BATTERY_NORMAL, batteryInfo.state);
}
@Test
public void testHandleFirmwareVersionInfo() {
byte[] payload = "twa200_20250225_3.4.8".getBytes(StandardCharsets.UTF_8);
GBDeviceEvent event = EarFunResponseHandler.handleFirmwareVersionInfo(payload);
GBDeviceEventVersionInfo versionInfo = (GBDeviceEventVersionInfo) event;
assertEquals("3.4.8", versionInfo.fwVersion);
assertEquals("twa200 20250225", versionInfo.hwVersion);
}
@Test
public void testHandleGameModeInfo() {
byte[] payload = new byte[]{0x00, 0x01};
GBDeviceEvent event = EarFunResponseHandler.handleGameModeInfo(payload);
GBDeviceEventUpdatePreferences updateEvent = (GBDeviceEventUpdatePreferences) event;
assertTrue(updateEvent.preferences.containsKey(PREF_EARFUN_GAME_MODE));
assertEquals(updateEvent.preferences.get(PREF_EARFUN_GAME_MODE), true);
}
@Test
public void testHandleAmbientSoundInfo() {
byte[] payload = new byte[]{0x00, 0x02};
GBDeviceEvent event = EarFunResponseHandler.handleAmbientSoundInfo(payload);
GBDeviceEventUpdatePreferences updateEvent = (GBDeviceEventUpdatePreferences) event;
assertTrue(updateEvent.preferences.containsKey(PREF_EARFUN_AMBIENT_SOUND_CONTROL));
assertEquals(updateEvent.preferences.get(PREF_EARFUN_AMBIENT_SOUND_CONTROL), "2");
}
@Test
public void testHandleAncModeInfo() {
byte[] payload = new byte[]{0x00, 0x04};
GBDeviceEvent event = EarFunResponseHandler.handleAncModeInfo(payload);
GBDeviceEventUpdatePreferences updateEvent = (GBDeviceEventUpdatePreferences) event;
assertTrue(updateEvent.preferences.containsKey(PREF_EARFUN_ANC_MODE));
assertEquals(updateEvent.preferences.get(PREF_EARFUN_ANC_MODE), "4");
}
@Test
public void testHandleTransparencyModeInfo() {
byte[] payload = new byte[]{0x00, 0x01};
GBDeviceEvent event = EarFunResponseHandler.handleTransparencyModeInfo(payload);
GBDeviceEventUpdatePreferences updateEvent = (GBDeviceEventUpdatePreferences) event;
assertTrue(updateEvent.preferences.containsKey(PREF_EARFUN_TRANSPARENCY_MODE));
assertEquals(updateEvent.preferences.get(PREF_EARFUN_TRANSPARENCY_MODE), "1");
}
@Test
public void testHandleTouchActionInfo() {
byte[] payload = ByteBuffer.allocate(18)
.put((byte) 0x01)
.putShort((short) 0x0001)
.putShort((short) 0x0002)
.putShort((short) 0x0003)
.putShort((short) 0x0004)
.putShort((short) 0x0005)
.putShort((short) 0x0006)
.putShort((short) 0x0007)
.putShort((short) 0x0008)
.array();
GBDeviceEvent event = EarFunResponseHandler.handleTouchActionInfo(payload);
GBDeviceEventUpdatePreferences updateEvent = (GBDeviceEventUpdatePreferences) event;
assertEquals(8, updateEvent.preferences.size());
assertEquals(updateEvent.preferences.get(PREF_EARFUN_SINGLE_TAP_LEFT_ACTION), "1");
assertEquals(updateEvent.preferences.get(PREF_EARFUN_TRIPPLE_TAP_RIGHT_ACTION), "6");
}
}