1
0
mirror of https://codeberg.org/Freeyourgadget/Gadgetbridge synced 2024-11-29 13:26:50 +01:00

Add initial support for Nothing Ear(1) TWS (#2403)

Nothing Ear (1) are wireless earbuds that support active noise
suppression, transparency mode and several gestures.

This initial commit adds support for:
- reading battery level
- setting audio mode
- setting in-ear auto detection

Reviewed-on: https://codeberg.org/Freeyourgadget/Gadgetbridge/pulls/2403
Co-authored-by: daniele <daniele@noreply.codeberg.org>
Co-committed-by: daniele <daniele@noreply.codeberg.org>
This commit is contained in:
daniele 2021-09-30 22:40:18 +02:00 committed by Andreas Shimokawa
parent a6073fa938
commit a7f42f0c4f
15 changed files with 607 additions and 0 deletions

View File

@ -95,6 +95,9 @@ public class DeviceSettingsPreferenceConst {
public static final String PREF_BT_CONNECTED_ADVERTISEMENT = "bt_connected_advertisement"; public static final String PREF_BT_CONNECTED_ADVERTISEMENT = "bt_connected_advertisement";
public static final String PREF_TRANSLITERATION_ENABLED = "pref_transliteration_enabled"; public static final String PREF_TRANSLITERATION_ENABLED = "pref_transliteration_enabled";
public static final String PREF_NOTHING_EAR1_INEAR = "pref_nothing_inear_detection";
public static final String PREF_NOTHING_EAR1_AUDIOMODE = "pref_nothing_audiomode";
public static final String PREF_SOUNDS = "sounds"; public static final String PREF_SOUNDS = "sounds";
public static final String PREF_AUTH_KEY = "authkey"; public static final String PREF_AUTH_KEY = "authkey";
} }

View File

@ -104,6 +104,8 @@ import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.Dev
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_STRENGH_PERCENTAGE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_WEARLOCATION;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_VIBRATION_ENABLE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE;
import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_ACTIVATE_DISPLAY_ON_LIFT;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_BROADCAST;
import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION; import static nodomain.freeyourgadget.gadgetbridge.devices.huami.HuamiConst.PREF_DEVICE_ACTION_FELL_SLEEP_SELECTION;
@ -440,6 +442,9 @@ public class DeviceSpecificSettingsFragment extends PreferenceFragmentCompat {
addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION); addPreferenceHandlerFor(PREF_SONYSWR12_LOW_VIBRATION);
addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL); addPreferenceHandlerFor(PREF_SONYSWR12_SMART_INTERVAL);
addPreferenceHandlerFor(PREF_NOTHING_EAR1_INEAR);
addPreferenceHandlerFor(PREF_NOTHING_EAR1_AUDIOMODE);
String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF); String sleepTimeState = prefs.getString(PREF_SLEEP_TIME, PREF_DO_NOT_DISTURB_OFF);
boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED); boolean sleepTimeScheduled = sleepTimeState.equals(PREF_DO_NOT_DISTURB_SCHEDULED);

View File

@ -0,0 +1,130 @@
package nodomain.freeyourgadget.gadgetbridge.devices.nothing;
import android.app.Activity;
import android.content.Context;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import nodomain.freeyourgadget.gadgetbridge.GBException;
import nodomain.freeyourgadget.gadgetbridge.R;
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
public class Ear1Coordinator extends AbstractDeviceCoordinator {
@NonNull
@Override
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
if(candidate.getName().equals("Nothing ear (1)"))
return DeviceType.NOTHING_EAR1;
return DeviceType.UNKNOWN;
}
@Override
public DeviceType getDeviceType() {
return DeviceType.NOTHING_EAR1;
}
@Nullable
@Override
public Class<? extends Activity> getPairingActivity() {
return null;
}
@Override
public boolean supportsActivityDataFetching() {
return false;
}
@Override
public boolean supportsActivityTracking() {
return false;
}
@Override
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
return null;
}
@Override
public InstallHandler findInstallHandler(Uri uri, Context context) {
return null;
}
@Override
public boolean supportsScreenshots() {
return false;
}
@Override
public int getAlarmSlotCount() {
return 0;
}
@Override
public boolean supportsSmartWakeup(GBDevice device) {
return false;
}
@Override
public boolean supportsHeartRateMeasurement(GBDevice device) {
return false;
}
@Override
public String getManufacturer() {
return "Nothing";
}
@Override
public boolean supportsAppsManagement() {
return false;
}
@Override
public Class<? extends Activity> getAppsManagementActivity() {
return null;
}
@Override
public boolean supportsCalendarEvents() {
return false;
}
@Override
public boolean supportsRealtimeData() {
return false;
}
@Override
public boolean supportsWeather() {
return false;
}
@Override
public boolean supportsFindDevice() {
return true;
}
@Override
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
}
@Override
public int[] getSupportedDeviceSpecificSettings(GBDevice device) {
return new int[] {
R.xml.devicesettings_nothing_ear1
};
}
}

View File

@ -100,6 +100,7 @@ public enum DeviceType {
WASPOS(330, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_waspos), WASPOS(330, R.drawable.ic_device_pebble, R.drawable.ic_device_pebble_disabled, R.string.devicetype_waspos),
UM25(350, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_um25), UM25(350, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_um25),
DOMYOS_T540(400, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_domyos_t540), DOMYOS_T540(400, R.drawable.ic_device_lovetoy, R.drawable.ic_device_lovetoy_disabled, R.string.devicetype_domyos_t540),
NOTHING_EAR1(410, R.drawable.ic_device_nothingear, R.drawable.ic_device_nothingear_disabled, R.string.devicetype_nothingear1),
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test); TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
private final int key; private final int key;

View File

@ -80,6 +80,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.miband.MiBandSupport
import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.mijia_lywsd02.MijiaLywsd02Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.miscale2.MiScale2DeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.no1f1.No1F1Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.nothing.Ear1Support;
import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.nut.NutSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport;
import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.pinetime.PineTimeJFSupport;
@ -358,6 +359,9 @@ public class DeviceSupportFactory {
case FITPRO: case FITPRO:
deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING)); deviceSupport = new ServiceDeviceSupport(new FitProDeviceSupport(), EnumSet.of(ServiceDeviceSupport.Flags.THROTTLING, ServiceDeviceSupport.Flags.BUSY_CHECKING));
break; break;
case NOTHING_EAR1:
deviceSupport = new ServiceDeviceSupport(new Ear1Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
break;
} }
if (deviceSupport != null) { if (deviceSupport != null) {
deviceSupport.setContext(gbDevice, mBtAdapter, mContext); deviceSupport.setContext(gbDevice, mBtAdapter, mContext);

View File

@ -0,0 +1,89 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
import android.net.Uri;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
import nodomain.freeyourgadget.gadgetbridge.service.serial.AbstractSerialDeviceSupport;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceIoThread;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
public class Ear1Support extends AbstractSerialDeviceSupport {
private static final Logger LOG = LoggerFactory.getLogger(Ear1Support.class);
@Override
public void onSendConfiguration(String config) {
super.onSendConfiguration(config);
}
@Override
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
}
@Override
public void onInstallApp(Uri uri) {
}
@Override
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
}
@Override
public void onHeartRateTest() {
}
@Override
public void onSetConstantVibration(int integer) {
}
@Override
public void onSetHeartRateMeasurementInterval(int seconds) {
}
@Override
public void onReadConfiguration(String config) {
}
@Override
public void onTestNewFunction() {
//getDeviceIOThread().write(((NothingProtocol) getDeviceProtocol()).encodeBatteryStatusReq());
}
@Override
public boolean connect() {
getDeviceIOThread().start();
return true;
}
@Override
public synchronized NothingIOThread getDeviceIOThread() {
return (NothingIOThread) super.getDeviceIOThread();
}
@Override
public boolean useAutoConnect() {
return false;
}
protected GBDeviceProtocol createDeviceProtocol() {
return new NothingProtocol(getDevice());
}
@Override
protected GBDeviceIoThread createDeviceIOThread() {
return new NothingIOThread(getDevice(), getContext(), (NothingProtocol) getDeviceProtocol(), Ear1Support.this, getBluetoothAdapter());
}
}

View File

@ -0,0 +1,45 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
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 static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
public class NothingIOThread extends BtClassicIoThread {
private static final Logger LOG = LoggerFactory.getLogger(NothingIOThread.class);
private final NothingProtocol mNothingProtocol;
@NonNull
protected UUID getUuidToConnect(@NonNull ParcelUuid[] uuids) {
return mNothingProtocol.UUID_DEVICE_CTRL;
}
public NothingIOThread(GBDevice device, Context context, NothingProtocol deviceProtocol, Ear1Support ear1Support, BluetoothAdapter bluetoothAdapter) {
super(device, context, deviceProtocol, ear1Support, bluetoothAdapter);
mNothingProtocol = deviceProtocol;
}
@Override
protected byte[] parseIncoming(InputStream inStream) throws IOException {
byte[] buffer = new byte[1048576]; //HUGE read
int bytes = inStream.read(buffer);
LOG.debug("read " + bytes + " bytes. " + hexdump(buffer, 0, bytes));
return Arrays.copyOf(buffer, bytes);
}
}

View File

@ -0,0 +1,235 @@
package nodomain.freeyourgadget.gadgetbridge.service.devices.nothing;
import android.content.SharedPreferences;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
import java.util.UUID;
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEvent;
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
import nodomain.freeyourgadget.gadgetbridge.model.BatteryState;
import nodomain.freeyourgadget.gadgetbridge.service.serial.GBDeviceProtocol;
import static nodomain.freeyourgadget.gadgetbridge.util.CheckSums.getCRC16ansi;
import static nodomain.freeyourgadget.gadgetbridge.util.GB.hexdump;
public class NothingProtocol extends GBDeviceProtocol {
private static final Logger LOG = LoggerFactory.getLogger(NothingProtocol.class);
final UUID UUID_DEVICE_CTRL = UUID.fromString("aeac4a03-dff5-498f-843a-34487cf133eb");
public static final byte CONTROL_DEVICE_TYPE_TWS_HEADSET = 1;
private static final int CONTROL_CRC = 0x20;
private static final byte MASK_RSP_CODE = 0x1f;
private static final short MASK_DEVICE_TYPE = 0x0F00;
private static final short MASK_REQUEST_CMD = (short) 0x8000;
private static final byte MASK_BATTERY = 0x7f;
private static final byte MASK_BATTERY_CHARGING = (byte) 0x80;
//incoming
private static final short battery_status = (short) 0xe001;
private static final short battery_status2 = (short) 0xc007;
private static final short unk_maybe_ack = (short) 0xf002;
private static final short unk_close_case = (short) 0xe002; //sent twice when the case is closed with earphones in
//outgoing
private static final short in_ear_detection = (short) 0xf004;
@Override
public GBDeviceEvent[] decodeResponse(byte[] responseData) {
ByteBuffer incoming = ByteBuffer.wrap(responseData);
incoming.order(ByteOrder.LITTLE_ENDIAN);
byte sof = incoming.get();
if (sof != 0x55) {
LOG.error("Error in message, wrong start of frame: " + hexdump(responseData));
return null;
}
short control = incoming.getShort();
if (!isSupportedDevice(control)) {
LOG.error("Unsupported device specified in message: " + hexdump(responseData));
return null;
}
if (!isOk(control)) {
LOG.error("Message is not ok: " + hexdump(responseData));
return null;
}
short command = incoming.getShort();
short length = incoming.getShort();
incoming.get();
byte[] payload = Arrays.copyOfRange(responseData, incoming.position(), incoming.position() + length);
switch (getRequestCommand(command)) {
case battery_status:
case battery_status2:
return handleBatteryInfo(payload);
case unk_maybe_ack:
LOG.debug("received ack");
break;
case unk_close_case:
LOG.debug("case closed");
break;
default:
LOG.debug("Incoming message - control:" + control + " requestCommand: " + (getRequestCommand(command) & 0xffff) + "length: " + length + " dump: " + hexdump(responseData));
}
return null;
}
boolean isCrcNeeded(short control) {
return (control & CONTROL_CRC) != 0;
}
private byte[] encodeMessage(short control, short command, byte[] payload) {
ByteBuffer msgBuf = ByteBuffer.allocate(8 + payload.length);
msgBuf.order(ByteOrder.LITTLE_ENDIAN);
msgBuf.put((byte) 0x55); //sof
msgBuf.putShort(control);
msgBuf.putShort(command);
msgBuf.putShort((short) payload.length);
msgBuf.put((byte) 0x00); //fsn TODO: is this always 0?
msgBuf.put(payload);
if (isCrcNeeded(control)) {
msgBuf.position(0);
ByteBuffer crcBuf = ByteBuffer.allocate(msgBuf.capacity() + 2);
crcBuf.order(ByteOrder.LITTLE_ENDIAN);
crcBuf.put(msgBuf);
crcBuf.putShort((short) getCRC16ansi(msgBuf.array()));
return crcBuf.array();
}
return msgBuf.array();
}
byte[] encodeBatteryStatusReq() {
return encodeMessage((short) 0x120, (short) 0xc007, new byte[]{});
}
byte[] encodeAudioMode(String desired) {
byte[] payload = new byte[]{0x01, 0x05, 0x00};
switch (desired) {
case "anc":
payload[1] = 0x01;
break;
case "transparency":
payload[1] = 0x07;
break;
case "off":
default:
}
return encodeMessage((short) 0x120, (short) 0xf00f, payload);
}
@Override
public byte[] encodeFindDevice(boolean start) {
byte payload = (byte) (start ? 0x01 : 0x00);
return encodeMessage((short) 0x120, (short) 0xf002, new byte[]{payload});
}
@Override
public byte[] encodeSendConfiguration(String config) {
SharedPreferences prefs = GBApplication.getDeviceSpecificSharedPrefs(getDevice().getAddress());
switch (config) {
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR:
byte enabled = (byte) (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_INEAR, true) ? 0x01 : 0x00);
return encodeMessage((short) 0x120, in_ear_detection, new byte[]{0x01, 0x01, enabled});
// response: 55 20 01 04 70 00 00 00
case DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE:
return encodeAudioMode(prefs.getString(DeviceSettingsPreferenceConst.PREF_NOTHING_EAR1_AUDIOMODE, "off"));
// response: 55 20 01 0F 70 00 00 00
default:
LOG.debug("CONFIG: " + config);
}
return super.encodeSendConfiguration(config);
}
@Override
public byte[] encodeSetTime() {
// This are earphones, there is no time to set here. However this method gets called soon
// after connecting, hence we use it to perform some initializations.
return encodeBatteryStatusReq();
}
private GBDeviceEvent[] handleBatteryInfo(byte[] payload) {
//LOG.debug("Battery payload: " + hexdump(payload));
/* payload:
1st byte is number of batteries, then $number pairs follow:
{idx, value}
idx is 0x02 for left ear, 0x03 for right ear, 0x04 for case
value goes from 0-64 (equivalent of 0-100 in hexadecimal)
Since Gadgetbridge supports only one battery, we use an average of the levels for the
battery level.
If one of the batteries is recharging, we consider the battery as recharging.
*/
GBDeviceEventBatteryInfo evBattery = new GBDeviceEventBatteryInfo();
evBattery.level = 0;
boolean batteryCharging = false;
int numBatteries = payload[0];
for (int i = 0; i < numBatteries; i++) {
evBattery.level += (short) ((payload[2 + 2 * i] & MASK_BATTERY) / numBatteries);
if (!batteryCharging)
batteryCharging = ((payload[2 + 2 * i]) & MASK_BATTERY_CHARGING) == 1;
//LOG.debug("single battery level: " + hexdump(payload, 2+2*i,1) +"-"+ ((payload[2+2*i] & 0xff))+":" + evBattery.level);
}
evBattery.state = BatteryState.UNKNOWN;
evBattery.state = batteryCharging ? BatteryState.BATTERY_CHARGING : evBattery.state;
return new GBDeviceEvent[]{evBattery};
}
private short getRequestCommand(short command) {
return (short) (command | MASK_REQUEST_CMD);
}
private boolean isOk(short control) {
return (control & MASK_RSP_CODE) == 0;
}
private boolean isSupportedDevice(short control) {
return getDeviceType(control) == CONTROL_DEVICE_TYPE_TWS_HEADSET;
}
private byte getDeviceType(short control) {
return (byte) ((control & MASK_DEVICE_TYPE) >> 8);
}
protected NothingProtocol(GBDevice device) {
super(device);
}
}

View File

@ -58,6 +58,24 @@ public class CheckSums {
return crc; return crc;
} }
public static int getCRC16ansi(byte[] seq) {
int crc = 0xffff;
int polynomial = 0xA001;
for (int i = 0; i < seq.length; i++) {
crc ^= seq[i] & 0xFF;
for (int j = 0; j < 8; j++) {
if ((crc & 1) != 0) {
crc = (crc >>> 1) ^ polynomial;
} else {
crc = crc >>> 1;
}
}
}
return crc & 0xFFFF;
}
public static int getCRC32(byte[] seq) { public static int getCRC32(byte[] seq) {
CRC32 crc = new CRC32(); CRC32 crc = new CRC32();
crc.update(seq); crc.update(seq);

View File

@ -97,6 +97,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.miband.MiBandCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.mijia_lywsd02.MijiaLywsd02Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.miscale2.MiScale2DeviceCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nothing.Ear1Coordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.nut.NutCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator;
import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pinetime.PineTimeJFCoordinator;
@ -306,6 +307,7 @@ public class DeviceHelper {
result.add(new UM25Coordinator()); result.add(new UM25Coordinator());
result.add(new DomyosT540Cooridnator()); result.add(new DomyosT540Cooridnator());
result.add(new FitProDeviceCoordinator()); result.add(new FitProDeviceCoordinator());
result.add(new Ear1Coordinator());
return result; return result;
} }

View File

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="45sp"
android:height="45sp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:fillColor="#2196f3"
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
android:strokeWidth="3.57115173" />
<path
android:pathData="m11.248,18.9974a2.6708,2.6529 90,0 1,-2.6529 -2.6708l0,-3.6234a5.5464,5.5093 89.9997,0 1,-3.3073 -1.6559,0.8903 0.8843,89.9998 0,1 -0.2299,-0.6054l0,-3.3652a0.8903,0.8843 89.9998,0 1,0.2299 -0.6054,5.6977 5.6596,90.0002 0,1 4.1916,-1.7182l0.1945,0a4.4513,4.4216 89.9998,0 1,4.227 4.4513l0,7.1222a2.6708,2.6529 90,0 1,-2.6529 2.6708zM6.8264,10.0947a4.113,4.0855 90.0003,0 0,2.6529 0.8903,0.8903 0.8843,89.9998 0,1 0.8843,0.8903l0,4.4513a0.8903,0.8843 90,0 0,1.7686 0L12.1323,9.2044a2.6708,2.6529 90,0 0,-2.5114 -2.6708l-0.1415,0a4.113,4.0855 90.0003,0 0,-2.6529 0.8903z"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M18.3225,24.339A2.6708,2.6529 90,0 1,15.6695 21.6682L15.6695,14.546a4.4513,4.4216 89.9998,0 1,4.227 -4.4513l0.1945,0a5.6977,5.6596 90.0002,0 1,4.1916 1.7182,0.8903 0.8843,89.9998 0,1 0.2299,0.6054l0,3.3652a0.8903,0.8843 89.9998,0 1,-0.2299 0.6054,5.5464 5.5093,89.9997 0,1 -3.3073,1.6559l0,3.6234a2.6708,2.6529 90,0 1,-2.6529 2.6708zM20.0911,11.8752l-0.1415,0a2.6708,2.6529 90,0 0,-2.5114 2.6708l0,7.1222a0.8903,0.8843 90.0003,0 0,1.7686 0l0,-4.4513a0.8903,0.8843 89.9998,0 1,0.8843 -0.8903,4.113 4.0855,90.0003 0,0 2.6529,-0.8903L22.744,12.7655a4.113,4.0855 90.0003,0 0,-2.6529 -0.8903z"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.595,8.7593m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M20.9754,14.1009m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
android:strokeWidth="0.8872858"
android:fillColor="#aa0000"/>
</vector>

View File

@ -0,0 +1,26 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="45sp"
android:height="45sp"
android:viewportWidth="30"
android:viewportHeight="30">
<path
android:fillColor="#8a8a8a"
android:pathData="M3.871 3.413h20.925a0.947 0.947 0 0 1 0.948 0.947v20.01a0.947 0.947 0 0 1-0.948 0.948H3.871a0.947 0.947 0 0 1-0.947-0.948V4.36A0.947 0.947 0 0 1 3.87 3.413z"
android:strokeWidth="3.57115173" />
<path
android:pathData="m11.248,18.9974a2.6708,2.6529 90,0 1,-2.6529 -2.6708l0,-3.6234a5.5464,5.5093 89.9997,0 1,-3.3073 -1.6559,0.8903 0.8843,89.9998 0,1 -0.2299,-0.6054l0,-3.3652a0.8903,0.8843 89.9998,0 1,0.2299 -0.6054,5.6977 5.6596,90.0002 0,1 4.1916,-1.7182l0.1945,0a4.4513,4.4216 89.9998,0 1,4.227 4.4513l0,7.1222a2.6708,2.6529 90,0 1,-2.6529 2.6708zM6.8264,10.0947a4.113,4.0855 90.0003,0 0,2.6529 0.8903,0.8903 0.8843,89.9998 0,1 0.8843,0.8903l0,4.4513a0.8903,0.8843 90,0 0,1.7686 0L12.1323,9.2044a2.6708,2.6529 90,0 0,-2.5114 -2.6708l-0.1415,0a4.113,4.0855 90.0003,0 0,-2.6529 0.8903z"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M18.3225,24.339A2.6708,2.6529 90,0 1,15.6695 21.6682L15.6695,14.546a4.4513,4.4216 89.9998,0 1,4.227 -4.4513l0.1945,0a5.6977,5.6596 90.0002,0 1,4.1916 1.7182,0.8903 0.8843,89.9998 0,1 0.2299,0.6054l0,3.3652a0.8903,0.8843 89.9998,0 1,-0.2299 0.6054,5.5464 5.5093,89.9997 0,1 -3.3073,1.6559l0,3.6234a2.6708,2.6529 90,0 1,-2.6529 2.6708zM20.0911,11.8752l-0.1415,0a2.6708,2.6529 90,0 0,-2.5114 2.6708l0,7.1222a0.8903,0.8843 90.0003,0 0,1.7686 0l0,-4.4513a0.8903,0.8843 89.9998,0 1,0.8843 -0.8903,4.113 4.0855,90.0003 0,0 2.6529,-0.8903L22.744,12.7655a4.113,4.0855 90.0003,0 0,-2.6529 -0.8903z"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.595,8.7593m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
<path
android:pathData="M20.9754,14.1009m-0.8843,0a0.8903,0.8843 90.0003,1 1,1.7686 0a0.8903,0.8843 90.0003,1 1,-1.7686 0"
android:strokeWidth="0.8872858"
android:fillColor="#ffffff"/>
</vector>

View File

@ -1775,5 +1775,11 @@
<item>2</item> <item>2</item>
<item>3</item> <item>3</item>
</string-array> </string-array>
<string-array name="nothing_ear1_audio_mode">
<item>anc</item>
<item>transparency</item>
<item>off</item>
</string-array>
</resources> </resources>

View File

@ -1271,4 +1271,5 @@
<string name="prefs_autoheartrate_measurement">Automatic Heart Rate measurements</string> <string name="prefs_autoheartrate_measurement">Automatic Heart Rate measurements</string>
<string name="prefs_autoheartrate_sleep">Take measurements during sleep</string> <string name="prefs_autoheartrate_sleep">Take measurements during sleep</string>
<string name="prefs_autoheartrate_interval">Frequency of measurements</string> <string name="prefs_autoheartrate_interval">Frequency of measurements</string>
<string name="devicetype_nothingear1">Nothing Ear (1)</string>
</resources> </resources>

View File

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.preference.PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<SwitchPreference
android:defaultValue="true"
android:icon="@drawable/ic_extension"
android:key="pref_nothing_inear_detection"
android:summary="Play/pause the music depending if you wear the earbuds"
android:title="In-Ear detection" />
<ListPreference
android:icon="@drawable/ic_extension"
android:entryValues="@array/nothing_ear1_audio_mode"
android:entries="@array/nothing_ear1_audio_mode"
android:key="pref_nothing_audiomode"
android:summary="%s"
android:title="Audio mode" />
</androidx.preference.PreferenceScreen>