mirror of
https://codeberg.org/Freeyourgadget/Gadgetbridge.git
synced 2025-01-10 17:11:56 +01:00
Add support JYou Y5 device.
This commit is contained in:
parent
934f36f45a
commit
fbb75694c7
@ -45,7 +45,7 @@ public class GBDaoGenerator {
|
||||
|
||||
|
||||
public static void main(String[] args) throws Exception {
|
||||
Schema schema = new Schema(18, MAIN_PACKAGE + ".entities");
|
||||
Schema schema = new Schema(19, MAIN_PACKAGE + ".entities");
|
||||
|
||||
Entity userAttributes = addUserAttributes(schema);
|
||||
Entity user = addUserInfo(schema, userAttributes);
|
||||
@ -71,7 +71,7 @@ public class GBDaoGenerator {
|
||||
addXWatchActivitySample(schema, user, device);
|
||||
addZeTimeActivitySample(schema, user, device);
|
||||
addID115ActivitySample(schema, user, device);
|
||||
|
||||
addJYouActivitySample(schema, user, device);
|
||||
addCalendarSyncState(schema, device);
|
||||
|
||||
addBipActivitySummary(schema, user, device);
|
||||
@ -314,6 +314,19 @@ public class GBDaoGenerator {
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static Entity addJYouActivitySample(Schema schema, Entity user, Entity device) {
|
||||
Entity activitySample = addEntity(schema, "JYouActivitySample");
|
||||
activitySample.implementsSerializable();
|
||||
addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device);
|
||||
activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE);
|
||||
activitySample.addIntProperty("caloriesBurnt");
|
||||
activitySample.addIntProperty("distanceMeters");
|
||||
activitySample.addIntProperty("activeTimeMinutes");
|
||||
addHeartRateProperties(activitySample);
|
||||
return activitySample;
|
||||
}
|
||||
|
||||
private static void addCommonActivitySampleProperties(String superClass, Entity activitySample, Entity user, Entity device) {
|
||||
activitySample.setSuperclass(superClass);
|
||||
activitySample.addImport(MAIN_PACKAGE + ".devices.SampleProvider");
|
||||
|
@ -36,12 +36,17 @@ public final class JYouConstants {
|
||||
public static final byte CMD_SET_SLEEP_TIME = 0x27;
|
||||
public static final byte CMD_SET_DND_SETTINGS = 0x39;
|
||||
public static final byte CMD_SET_INACTIVITY_WARNING_TIME = 0x24;
|
||||
|
||||
public static final byte CMD_ACTION_HEARTRATE_SWITCH = 0x0D;
|
||||
public static final byte CMD_ACTION_SHOW_NOTIFICATION = 0x2C;
|
||||
public static final byte CMD_ACTION_REBOOT_DEVICE = 0x0E;
|
||||
|
||||
public static final byte RECEIVE_BATTERY_LEVEL = (byte)0xF7;
|
||||
public static final byte RECEIVE_HISTORY_SLEEP_COUNT = 0x32;
|
||||
public static final byte RECEIVE_BLOOD_PRESSURE = (byte) 0xE8;
|
||||
public static final byte RECEIVE_WATCH_MAC = (byte)0xEC;
|
||||
public static final byte RECEIVE_GET_PHOTO = (byte)0xF3;
|
||||
public static final byte RECEIVE_DEVICE_INFO = (byte)0xF6;
|
||||
public static final byte RECEIVE_BATTERY_LEVEL = (byte)0xF7;
|
||||
public static final byte RECEIVE_STEPS_DATA = (byte)0xF9;
|
||||
public static final byte RECEIVE_HEARTRATE = (byte)0xFC;
|
||||
|
||||
|
@ -0,0 +1,70 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.jyou;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.AbstractDao;
|
||||
import de.greenrobot.dao.Property;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySampleDao;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
|
||||
public class JYouSampleProvider extends AbstractSampleProvider<JYouActivitySample> {
|
||||
|
||||
public static final int TYPE_ACTIVITY = -1;
|
||||
private final float movementDivisor = 6000.0f;
|
||||
private GBDevice mDevice;
|
||||
private DaoSession mSession;
|
||||
|
||||
public JYouSampleProvider(GBDevice device, DaoSession session) {
|
||||
super(device, session);
|
||||
|
||||
mSession = session;
|
||||
mDevice = device;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int normalizeType(int rawType) {
|
||||
return rawType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int toRawActivityKind(int activityKind) {
|
||||
return activityKind;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float normalizeIntensity(int rawIntensity) {
|
||||
return rawIntensity/movementDivisor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public JYouActivitySample createActivitySample() {
|
||||
return new JYouActivitySample();
|
||||
}
|
||||
|
||||
@Override
|
||||
public AbstractDao<JYouActivitySample, ?> getSampleDao() {
|
||||
return getSession().getJYouActivitySampleDao();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected Property getRawKindSampleProperty() {
|
||||
return JYouActivitySampleDao.Properties.RawKind;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getTimestampSampleProperty() {
|
||||
return JYouActivitySampleDao.Properties.Timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
protected Property getDeviceIdentifierSampleProperty() {
|
||||
return JYouActivitySampleDao.Properties.DeviceId;
|
||||
}
|
||||
}
|
@ -15,7 +15,7 @@
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.jyou;
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.app.Activity;
|
||||
@ -26,18 +26,16 @@ import android.os.Build;
|
||||
import android.os.ParcelUuid;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.activities.charts.ChartsActivity;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceType;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.Prefs;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDeviceCandidate;
|
||||
|
||||
import org.slf4j.Logger;
|
@ -0,0 +1,133 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.devices.jyou.y5;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import de.greenrobot.dao.query.QueryBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBException;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.AbstractDeviceCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.InstallHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySampleDao;
|
||||
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 Y5Coordinator extends AbstractDeviceCoordinator {
|
||||
@Override
|
||||
protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException {
|
||||
Long deviceId = device.getId();
|
||||
QueryBuilder<?> qb = session.getJYouActivitySampleDao().queryBuilder();
|
||||
qb.where(JYouActivitySampleDao.Properties.DeviceId.eq(deviceId)).buildDelete().executeDeleteWithoutDetachingEntities();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public DeviceType getSupportedType(GBDeviceCandidate candidate) {
|
||||
try {
|
||||
String name = candidate.getDevice().getName();
|
||||
if (name != null) {
|
||||
if (name.contains("Y5")) {
|
||||
return DeviceType.Y5;
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
ex.getLocalizedMessage();
|
||||
}
|
||||
return DeviceType.UNKNOWN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DeviceType getDeviceType() {
|
||||
return DeviceType.Y5;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Class<? extends Activity> getPairingActivity() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityDataFetching() {
|
||||
//return true; // Аккум, обновление, будильник, график
|
||||
return false; // Аккум, будильник, график, нет кнопки обновления в табах, активный режим работает
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsActivityTracking() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public SampleProvider<? extends ActivitySample> getSampleProvider(GBDevice device, DaoSession session) {
|
||||
return new JYouSampleProvider(device, session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InstallHandler findInstallHandler(Uri uri, Context context) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsScreenshots() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsAlarmConfiguration() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsSmartWakeup(GBDevice device) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsHeartRateMeasurement(GBDevice device) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getManufacturer() {
|
||||
return "Y5";
|
||||
}
|
||||
|
||||
@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 true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsWeather() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsFindDevice() {
|
||||
return false;
|
||||
}
|
||||
}
|
@ -51,6 +51,7 @@ public enum DeviceType {
|
||||
WATCH9(100, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_watch9),
|
||||
ROIDMI(110, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi),
|
||||
ROIDMI3(112, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_roidmi3),
|
||||
Y5(120, R.drawable.ic_device_roidmi, R.drawable.ic_device_roidmi_disabled, R.string.devicetype_y5),
|
||||
TEST(1000, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_test);
|
||||
|
||||
private final int key;
|
||||
|
@ -41,7 +41,8 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.pebble.PebbleSupport
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.roidmi.RoidmiSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.VibratissimoSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30.TeclastH30Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.y5.Y5Support;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.watch9.Watch9DeviceSupport;
|
||||
@ -171,6 +172,9 @@ public class DeviceSupportFactory {
|
||||
case ROIDMI3:
|
||||
deviceSupport = new ServiceDeviceSupport(new RoidmiSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
case Y5:
|
||||
deviceSupport = new ServiceDeviceSupport(new Y5Support(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING));
|
||||
break;
|
||||
}
|
||||
if (deviceSupport != null) {
|
||||
deviceSupport.setContext(gbDevice, mBtAdapter, mContext);
|
||||
|
@ -0,0 +1,65 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou;
|
||||
|
||||
/*
|
||||
* @author Pavel Elagin <elagin.pasha@gmail.com>
|
||||
*/
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind;
|
||||
|
||||
public class JYouDataRecord {
|
||||
public final static int TYPE_UNKNOWN = 0;
|
||||
public final static int TYPE_SLEEP = 100;
|
||||
public final static int TYPE_DAY_SUMMARY = 101;
|
||||
public final static int TYPE_DAY_SLOT = 102;
|
||||
public final static int TYPE_REALTIME = 103;
|
||||
|
||||
public int type = TYPE_UNKNOWN;
|
||||
public int activityKind = ActivityKind.TYPE_UNKNOWN;
|
||||
|
||||
/**
|
||||
* Time of this record in seconds
|
||||
*/
|
||||
public int timestamp;
|
||||
|
||||
/**
|
||||
* Raw data as sent from the device
|
||||
*/
|
||||
public byte[] rawData;
|
||||
|
||||
protected JYouDataRecord(){
|
||||
|
||||
}
|
||||
|
||||
protected JYouDataRecord(byte[] data, int type){
|
||||
this.rawData = data;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public byte[] getRawData() {
|
||||
|
||||
return rawData;
|
||||
}
|
||||
|
||||
public class RecordInterval {
|
||||
/**
|
||||
* Start time of this interval in seconds
|
||||
*/
|
||||
public int timestampFrom;
|
||||
|
||||
/**
|
||||
* End time of this interval in seconds
|
||||
*/
|
||||
public int timestampTo;
|
||||
|
||||
/**
|
||||
* Type of activity {@link ActivityKind}
|
||||
*/
|
||||
public int activityKind;
|
||||
|
||||
RecordInterval(int timestampFrom, int timestampTo, int activityKind) {
|
||||
this.timestampFrom = timestampFrom;
|
||||
this.timestampTo = timestampTo;
|
||||
this.activityKind = activityKind;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou;
|
||||
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
|
||||
public abstract class RealtimeSamplesSupport {
|
||||
private final long delay;
|
||||
private final long period;
|
||||
|
||||
protected int steps;
|
||||
protected int heartrateBpm;
|
||||
private int lastSteps;
|
||||
// subclasses may add more
|
||||
|
||||
private Timer realtimeStorageTimer;
|
||||
|
||||
public RealtimeSamplesSupport(long delay, long period) {
|
||||
this.delay = delay;
|
||||
this.period = period;
|
||||
}
|
||||
|
||||
public synchronized void start() {
|
||||
if (isRunning()) {
|
||||
return; // already running
|
||||
}
|
||||
realtimeStorageTimer = new Timer("JYou Realtime Storage Timer");
|
||||
realtimeStorageTimer.scheduleAtFixedRate(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
triggerCurrentSample();
|
||||
}
|
||||
}, delay, period);
|
||||
}
|
||||
|
||||
public synchronized void stop() {
|
||||
if (realtimeStorageTimer != null) {
|
||||
realtimeStorageTimer.cancel();
|
||||
realtimeStorageTimer.purge();
|
||||
realtimeStorageTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized boolean isRunning() {
|
||||
return realtimeStorageTimer != null;
|
||||
}
|
||||
|
||||
public synchronized void setSteps(int stepsPerMinute) {
|
||||
this.steps = stepsPerMinute;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the number of steps recorded since the last measurements. If no
|
||||
* steps are available yet, ActivitySample.NOT_MEASURED is returned.
|
||||
* @return
|
||||
*/
|
||||
public synchronized int getSteps() {
|
||||
if (steps == ActivitySample.NOT_MEASURED) {
|
||||
return ActivitySample.NOT_MEASURED;
|
||||
}
|
||||
if (lastSteps == 0) {
|
||||
return ActivitySample.NOT_MEASURED; // wait until we have a delta between two samples
|
||||
}
|
||||
int delta = steps - lastSteps;
|
||||
if (delta < 0) {
|
||||
return 0;
|
||||
}
|
||||
return delta;
|
||||
}
|
||||
|
||||
public void setHeartrateBpm(int hrBpm) {
|
||||
this.heartrateBpm = hrBpm;
|
||||
}
|
||||
|
||||
public int getHeartrateBpm() {
|
||||
return heartrateBpm;
|
||||
}
|
||||
|
||||
public void triggerCurrentSample() {
|
||||
doCurrentSample();
|
||||
resetCurrentValues();
|
||||
}
|
||||
|
||||
protected synchronized void resetCurrentValues() {
|
||||
if (steps >= lastSteps) {
|
||||
lastSteps = steps;
|
||||
}
|
||||
steps = ActivitySample.NOT_MEASURED;
|
||||
heartrateBpm = ActivitySample.NOT_MEASURED;
|
||||
}
|
||||
|
||||
protected abstract void doCurrentSample();
|
||||
}
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>. */
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou;
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
@ -0,0 +1,570 @@
|
||||
package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.y5;
|
||||
|
||||
import android.bluetooth.BluetoothGatt;
|
||||
import android.bluetooth.BluetoothGattCharacteristic;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.UUID;
|
||||
|
||||
import nodomain.freeyourgadget.gadgetbridge.GBApplication;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHandler;
|
||||
import nodomain.freeyourgadget.gadgetbridge.database.DBHelper;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.SampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouConstants;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.JYouSampleProvider;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.DaoSession;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.Device;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.JYouActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.entities.User;
|
||||
import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.Alarm;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CallSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.DeviceService;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder;
|
||||
import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.RealtimeSamplesSupport;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.GB;
|
||||
import nodomain.freeyourgadget.gadgetbridge.util.StringUtils;
|
||||
|
||||
public class Y5Support extends AbstractBTLEDeviceSupport {
|
||||
private static final Logger LOG = LoggerFactory.getLogger(Y5Support.class);
|
||||
|
||||
public BluetoothGattCharacteristic ctrlCharacteristic = null;
|
||||
public BluetoothGattCharacteristic measureCharacteristic = null;
|
||||
|
||||
private RealtimeSamplesSupport realtimeSamplesSupport;
|
||||
|
||||
private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo();
|
||||
private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo();
|
||||
|
||||
public Y5Support() {
|
||||
super(LOG);
|
||||
addSupportedService(JYouConstants.UUID_SERVICE_JYOU);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TransactionBuilder initializeDevice(TransactionBuilder builder) {
|
||||
LOG.info("Initializing");
|
||||
|
||||
gbDevice.setState(GBDevice.State.INITIALIZING);
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
|
||||
measureCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_MEASURE);
|
||||
ctrlCharacteristic = getCharacteristic(JYouConstants.UUID_CHARACTERISTIC_CONTROL);
|
||||
|
||||
builder.setGattCallback(this);
|
||||
builder.notify(measureCharacteristic, true);
|
||||
|
||||
syncSettings(builder);
|
||||
|
||||
gbDevice.setState(GBDevice.State.INITIALIZED);
|
||||
gbDevice.sendDeviceUpdateIntent(getContext());
|
||||
|
||||
LOG.info("Initialization Done");
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCharacteristicChanged(BluetoothGatt gatt,
|
||||
BluetoothGattCharacteristic characteristic) {
|
||||
if (super.onCharacteristicChanged(gatt, characteristic)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
UUID characteristicUUID = characteristic.getUuid();
|
||||
byte[] data = characteristic.getValue();
|
||||
if (data.length == 0)
|
||||
return true;
|
||||
|
||||
switch (data[0]) {
|
||||
case JYouConstants.RECEIVE_HISTORY_SLEEP_COUNT:
|
||||
LOG.info("onCharacteristicChanged: " + data[0]);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_BLOOD_PRESSURE:
|
||||
int heartRate = data[2];
|
||||
int bloodPressureHigh = data[3];
|
||||
int bloodPressureLow = data[4];
|
||||
int bloodOxygen = data[5];
|
||||
int Fatigue = data[6];
|
||||
LOG.info("RECEIVE_BLOOD_PRESSURE: Heart rate: " + heartRate + " Pressure high: " + bloodPressureHigh+ " pressure low: " + bloodPressureLow);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_DEVICE_INFO:
|
||||
int model = data[7];
|
||||
int fwVerNum = data[4] & 0xFF;
|
||||
versionCmd.fwVersion = (fwVerNum / 100) + "." + ((fwVerNum % 100) / 10) + "." + ((fwVerNum % 100) % 10);
|
||||
handleGBDeviceEvent(versionCmd);
|
||||
LOG.info("Firmware version is: " + versionCmd.fwVersion);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_BATTERY_LEVEL:
|
||||
batteryCmd.level = data[8];
|
||||
handleGBDeviceEvent(batteryCmd);
|
||||
LOG.info("Battery level is: " + batteryCmd.level);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_STEPS_DATA:
|
||||
int steps = ByteBuffer.wrap(data, 5, 4).getInt();
|
||||
LOG.info("Number of walked steps: " + steps);
|
||||
handleRealtimeSteps(steps);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_HEARTRATE:
|
||||
handleHeartrate(data[8]);
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_WATCH_MAC:
|
||||
return true;
|
||||
case JYouConstants.RECEIVE_GET_PHOTO:
|
||||
return true;
|
||||
default:
|
||||
LOG.info("Unhandled characteristic change: " + characteristicUUID + " code: " + String.format("0x%1x ...", data[0]));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void handleRealtimeSteps(int value) {
|
||||
//todo Call on connect the device
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("realtime steps: " + value);
|
||||
}
|
||||
getRealtimeSamplesSupport().setSteps(value);
|
||||
}
|
||||
|
||||
private void handleHeartrate(int value) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("heart rate: " + value);
|
||||
}
|
||||
RealtimeSamplesSupport realtimeSamplesSupport = getRealtimeSamplesSupport();
|
||||
realtimeSamplesSupport.setHeartrateBpm(value);
|
||||
if (!realtimeSamplesSupport.isRunning()) {
|
||||
// single shot measurement, manually invoke storage and result publishing
|
||||
realtimeSamplesSupport.triggerCurrentSample();
|
||||
}
|
||||
}
|
||||
|
||||
public JYouActivitySample createActivitySample(Device device, User user, int timestampInSeconds, SampleProvider provider) {
|
||||
JYouActivitySample sample = new JYouActivitySample();
|
||||
sample.setDevice(device);
|
||||
sample.setUser(user);
|
||||
sample.setTimestamp(timestampInSeconds);
|
||||
sample.setProvider(provider);
|
||||
return sample;
|
||||
}
|
||||
|
||||
private void enableRealtimeSamplesTimer(boolean enable) {
|
||||
if (enable) {
|
||||
getRealtimeSamplesSupport().start();
|
||||
} else {
|
||||
if (realtimeSamplesSupport != null) {
|
||||
realtimeSamplesSupport.stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RealtimeSamplesSupport getRealtimeSamplesSupport() {
|
||||
if (realtimeSamplesSupport == null) {
|
||||
realtimeSamplesSupport = new RealtimeSamplesSupport(1000, 1000) {
|
||||
@Override
|
||||
public void doCurrentSample() {
|
||||
|
||||
try (DBHandler handler = GBApplication.acquireDB()) {
|
||||
DaoSession session = handler.getDaoSession();
|
||||
int ts = (int) (System.currentTimeMillis() / 1000);
|
||||
JYouSampleProvider provider = new JYouSampleProvider(gbDevice, session);
|
||||
JYouActivitySample sample = createActivitySample(DBHelper.getDevice(getDevice(), session), DBHelper.getUser(session), ts, provider);
|
||||
sample.setHeartRate(getHeartrateBpm());
|
||||
sample.setRawIntensity(ActivitySample.NOT_MEASURED);
|
||||
sample.setRawKind(JYouSampleProvider.TYPE_ACTIVITY); // to make it visible in the charts TODO: add a MANUAL kind for that?
|
||||
|
||||
provider.addGBActivitySample(sample);
|
||||
|
||||
// set the steps only afterwards, since realtime steps are also recorded
|
||||
// in the regular samples and we must not count them twice
|
||||
// Note: we know that the DAO sample is never committed again, so we simply
|
||||
// change the value here in memory.
|
||||
sample.setSteps(getSteps());
|
||||
if(steps > 1){
|
||||
LOG.debug("Have steps: " + getSteps());
|
||||
}
|
||||
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("realtime sample: " + sample);
|
||||
}
|
||||
|
||||
Intent intent = new Intent(DeviceService.ACTION_REALTIME_SAMPLES)
|
||||
.putExtra(DeviceService.EXTRA_REALTIME_SAMPLE, sample);
|
||||
LocalBroadcastManager.getInstance(getContext()).sendBroadcast(intent);
|
||||
|
||||
} catch (Exception e) {
|
||||
LOG.warn("Unable to acquire db for saving realtime samples", e);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
return realtimeSamplesSupport;
|
||||
}
|
||||
|
||||
private void syncDateAndTime(TransactionBuilder builder) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
String strYear = String.valueOf(cal.get(Calendar.YEAR));
|
||||
byte year1 = (byte)Integer.parseInt(strYear.substring(0, 2));
|
||||
byte year2 = (byte)Integer.parseInt(strYear.substring(2, 4));
|
||||
byte month = (byte)cal.get(Calendar.MONTH);
|
||||
byte day = (byte)cal.get(Calendar.DAY_OF_MONTH);
|
||||
byte hour = (byte)cal.get(Calendar.HOUR_OF_DAY);
|
||||
byte minute = (byte)cal.get(Calendar.MINUTE);
|
||||
byte second = (byte)cal.get(Calendar.SECOND);
|
||||
byte weekDay = (byte)cal.get(Calendar.DAY_OF_WEEK);
|
||||
|
||||
builder.write(ctrlCharacteristic, commandWithChecksum(
|
||||
JYouConstants.CMD_SET_DATE_AND_TIME,
|
||||
(year1 << 24) | (year2 << 16) | (month << 8) | day,
|
||||
(hour << 24) | (minute << 16) | (second << 8) | weekDay
|
||||
));
|
||||
}
|
||||
|
||||
private void syncSettings(TransactionBuilder builder) {
|
||||
syncDateAndTime(builder);
|
||||
}
|
||||
|
||||
private void showNotification(byte icon, String title, String message) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("ShowNotification");
|
||||
|
||||
byte[] titleBytes = stringToUTF8Bytes(title, 16);
|
||||
byte[] messageBytes = stringToUTF8Bytes(message, 80);
|
||||
|
||||
for (int i = 1; i <= 7; i++)
|
||||
{
|
||||
byte[] currentPacket = new byte[20];
|
||||
currentPacket[0] = JYouConstants.CMD_ACTION_SHOW_NOTIFICATION;
|
||||
currentPacket[1] = 7;
|
||||
currentPacket[2] = (byte)i;
|
||||
switch(i) {
|
||||
case 1:
|
||||
currentPacket[4] = icon;
|
||||
break;
|
||||
case 2:
|
||||
if (titleBytes != null) {
|
||||
System.arraycopy(titleBytes, 0, currentPacket, 3, 6);
|
||||
System.arraycopy(titleBytes, 6, currentPacket, 10, 10);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if (messageBytes != null) {
|
||||
System.arraycopy(messageBytes, 16 * (i - 3), currentPacket, 3, 6);
|
||||
System.arraycopy(messageBytes, 6 + 16 * (i - 3), currentPacket, 10, 10);
|
||||
}
|
||||
break;
|
||||
}
|
||||
builder.write(ctrlCharacteristic, currentPacket);
|
||||
}
|
||||
performConnected(builder.getTransaction());
|
||||
} catch (IOException e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean useAutoConnect() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotification(NotificationSpec notificationSpec) {
|
||||
String notificationTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title);
|
||||
byte icon;
|
||||
switch (notificationSpec.type) {
|
||||
case GENERIC_SMS:
|
||||
icon = JYouConstants.ICON_SMS;
|
||||
break;
|
||||
case FACEBOOK:
|
||||
case FACEBOOK_MESSENGER:
|
||||
icon = JYouConstants.ICON_FACEBOOK;
|
||||
break;
|
||||
case TWITTER:
|
||||
icon = JYouConstants.ICON_TWITTER;
|
||||
break;
|
||||
case WHATSAPP:
|
||||
icon = JYouConstants.ICON_WHATSAPP;
|
||||
break;
|
||||
default:
|
||||
icon = JYouConstants.ICON_LINE;
|
||||
break;
|
||||
}
|
||||
showNotification(icon, notificationTitle, notificationSpec.body);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteNotification(int id) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetAlarms(ArrayList<? extends Alarm> alarms) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("SetAlarms");
|
||||
|
||||
for (int i = 0; i < alarms.size(); i++)
|
||||
{
|
||||
byte cmd;
|
||||
switch (i) {
|
||||
case 0:
|
||||
cmd = JYouConstants.CMD_SET_ALARM_1;
|
||||
break;
|
||||
case 1:
|
||||
cmd = JYouConstants.CMD_SET_ALARM_2;
|
||||
break;
|
||||
case 2:
|
||||
cmd = JYouConstants.CMD_SET_ALARM_3;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
Calendar cal = alarms.get(i).getAlarmCal();
|
||||
builder.write(ctrlCharacteristic, commandWithChecksum(
|
||||
cmd,
|
||||
alarms.get(i).isEnabled() ? cal.get(Calendar.HOUR_OF_DAY) : -1,
|
||||
alarms.get(i).isEnabled() ? cal.get(Calendar.MINUTE) : -1
|
||||
));
|
||||
}
|
||||
performConnected(builder.getTransaction());
|
||||
GB.toast(getContext(), "Alarm settings applied - do note that the current device does not support day specification", Toast.LENGTH_LONG, GB.INFO);
|
||||
} catch(IOException e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetTime() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("SetTime");
|
||||
syncDateAndTime(builder);
|
||||
performConnected(builder.getTransaction());
|
||||
} catch(IOException e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCallState(CallSpec callSpec) {
|
||||
switch (callSpec.command) {
|
||||
case CallSpec.CALL_INCOMING:
|
||||
showNotification(JYouConstants.ICON_CALL, callSpec.name, callSpec.number);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicState(MusicStateSpec stateSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetMusicInfo(MusicSpec musicSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeSteps(boolean enable) {
|
||||
onEnableRealtimeHeartRateMeasurement(enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInstallApp(Uri uri) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppInfoReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppStart(UUID uuid, boolean start) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppDelete(UUID uuid) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppConfiguration(UUID appUuid, String config, Integer id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppReorder(UUID[] uuids) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFetchRecordedData(int dataTypes) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void dispose() {
|
||||
LOG.info("Dispose");
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReboot() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("Reboot");
|
||||
builder.write(ctrlCharacteristic, commandWithChecksum(
|
||||
JYouConstants.CMD_ACTION_REBOOT_DEVICE, 0, 0
|
||||
));
|
||||
performConnected(builder.getTransaction());
|
||||
} catch(Exception e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onHeartRateTest() {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("HeartRateTest");
|
||||
builder.write(ctrlCharacteristic, commandWithChecksum(
|
||||
JYouConstants.CMD_SET_HEARTRATE_AUTO, 0, 0
|
||||
|
||||
));
|
||||
performConnected(builder.getTransaction());
|
||||
} catch(Exception e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableRealtimeHeartRateMeasurement(boolean enable) {
|
||||
try {
|
||||
TransactionBuilder builder = performInitialized("RealTimeHeartMeasurement");
|
||||
builder.write(ctrlCharacteristic, commandWithChecksum(
|
||||
JYouConstants.CMD_ACTION_HEARTRATE_SWITCH, 0, enable ? 1 : 0
|
||||
));
|
||||
performConnected(builder.getTransaction());
|
||||
enableRealtimeSamplesTimer(enable);
|
||||
} catch (Exception e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFindDevice(boolean start) {
|
||||
if (start) {
|
||||
showNotification(JYouConstants.ICON_QQ, "Gadgetbridge", "Bzzt! Bzzt!");
|
||||
GB.toast(getContext(), "As your device doesn't have sound, it will only vibrate 3 times consecutively", Toast.LENGTH_LONG, GB.INFO);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetConstantVibration(int integer) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScreenshotReq() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnableHeartRateSleepSupport(boolean enable) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetHeartRateMeasurementInterval(int seconds) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDeleteCalendarEvent(byte type, long id) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendConfiguration(String config) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTestNewFunction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendWeather(WeatherSpec weatherSpec) {
|
||||
|
||||
}
|
||||
|
||||
private byte[] commandWithChecksum(byte cmd, int argSlot1, int argSlot2)
|
||||
{
|
||||
ByteBuffer buf = ByteBuffer.allocate(10);
|
||||
buf.put(cmd);
|
||||
buf.putInt(argSlot1);
|
||||
buf.putInt(argSlot2);
|
||||
|
||||
byte[] bytesToWrite = buf.array();
|
||||
|
||||
byte checksum = 0;
|
||||
for (byte b : bytesToWrite) {
|
||||
checksum += b;
|
||||
}
|
||||
|
||||
bytesToWrite[9] = checksum;
|
||||
|
||||
return bytesToWrite;
|
||||
}
|
||||
|
||||
private byte[] stringToUTF8Bytes(String src, int byteCount) {
|
||||
try {
|
||||
if (src == null)
|
||||
return null;
|
||||
|
||||
for (int i = src.length(); i > 0; i--) {
|
||||
String sub = src.substring(0, i);
|
||||
byte[] subUTF8 = sub.getBytes("UTF-8");
|
||||
|
||||
if (subUTF8.length == byteCount) {
|
||||
return subUTF8;
|
||||
}
|
||||
|
||||
if (subUTF8.length < byteCount) {
|
||||
byte[] largerSubUTF8 = new byte[byteCount];
|
||||
System.arraycopy(subUTF8, 0, largerSubUTF8, 0, subUTF8.length);
|
||||
return largerSubUTF8;
|
||||
}
|
||||
}
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LOG.warn(e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
@ -51,7 +51,8 @@ import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2Coordin
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband2.MiBand2HRXCoordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.huami.miband3.MiBand3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.id115.ID115Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.TeclastH30.TeclastH30Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.jyou.y5.Y5Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi1Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.roidmi.Roidmi3Coordinator;
|
||||
import nodomain.freeyourgadget.gadgetbridge.devices.liveview.LiveviewCoordinator;
|
||||
@ -221,7 +222,7 @@ public class DeviceHelper {
|
||||
result.add(new Watch9DeviceCoordinator());
|
||||
result.add(new Roidmi1Coordinator());
|
||||
result.add(new Roidmi3Coordinator());
|
||||
|
||||
result.add(new Y5Coordinator());
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -625,6 +625,7 @@
|
||||
<string name="devicetype_watch9">Watch 9</string>
|
||||
<string name="devicetype_roidmi">Roidmi</string>
|
||||
<string name="devicetype_roidmi3">Roidmi 3</string>
|
||||
<string name="devicetype_y5">Y5</string>
|
||||
|
||||
<string name="choose_auto_export_location">Choose export location</string>
|
||||
<string name="notification_channel_name">Gadgetbridge notifications</string>
|
||||
|
Loading…
Reference in New Issue
Block a user