diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 4ef0d8a1b..3ebdfc5c9 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -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"); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java index e84d60fb6..1d3303851 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouConstants.java @@ -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; @@ -54,4 +59,4 @@ public final class JYouConstants { public static final byte ICON_TWITTER = 6; public static final byte ICON_WHATSAPP = 7; public static final byte ICON_LINE = 8; -} +} \ No newline at end of file diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java new file mode 100644 index 000000000..ee0475666 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/JYouSampleProvider.java @@ -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 { + + 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java similarity index 95% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java index 04ecdb5ad..c18c8399e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30Coordinator.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/TeclastH30/TeclastH30Coordinator.java @@ -15,7 +15,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java new file mode 100644 index 000000000..07a030175 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/jyou/y5/Y5Coordinator.java @@ -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 getPairingActivity() { + return null; + } + + @Override + public boolean supportsActivityDataFetching() { + //return true; // Аккум, обновление, будильник, график + return false; // Аккум, будильник, график, нет кнопки обновления в табах, активный режим работает + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public SampleProvider 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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java index de073474b..491510ff8 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -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; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java index 10962047a..cd38cad48 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -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); diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java new file mode 100644 index 000000000..ec9f08999 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/JYouDataRecord.java @@ -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; + } + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java new file mode 100644 index 000000000..c75d0d19f --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/RealtimeSamplesSupport.java @@ -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(); +} + diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java similarity index 99% rename from app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java rename to app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java index 9e9bf4358..d49361275 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30Support.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/TeclastH30/TeclastH30Support.java @@ -14,7 +14,7 @@ You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ -package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou; +package nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java new file mode 100644 index 000000000..c0f328163 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/jyou/y5/Y5Support.java @@ -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 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; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java index cc1fc17cc..bcb40f78e 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -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; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7942f6cf4..687374165 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -625,6 +625,7 @@ Watch 9 Roidmi Roidmi 3 + Y5 Choose export location Gadgetbridge notifications