diff --git a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java index 1afb4246d..8aed0d76e 100644 --- a/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java +++ b/GBDaoGenerator/src/nodomain/freeyourgadget/gadgetbridge/daogen/GBDaoGenerator.java @@ -69,6 +69,7 @@ public class GBDaoGenerator { addHPlusHealthActivitySample(schema, user, device); addNo1F1ActivitySample(schema, user, device); addXWatchActivitySample(schema, user, device); + addZeTimeActivitySample(schema, user, device); addCalendarSyncState(schema, device); @@ -286,6 +287,20 @@ public class GBDaoGenerator { return activitySample; } + private static Entity addZeTimeActivitySample(Schema schema, Entity user, Entity device) { + Entity activitySample = addEntity(schema, "ZeTimeActivitySample"); + activitySample.implementsSerializable(); + addCommonActivitySampleProperties("AbstractActivitySample", activitySample, user, device); + activitySample.addIntProperty(SAMPLE_STEPS).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_KIND).notNull().codeBeforeGetterAndSetter(OVERRIDE); + activitySample.addIntProperty(SAMPLE_RAW_INTENSITY).notNull().codeBeforeGetterAndSetter(OVERRIDE); + addHeartRateProperties(activitySample); + activitySample.addIntProperty("caloriesBurnt"); + activitySample.addIntProperty("distanceMeters"); + activitySample.addIntProperty("activeTimeMinutes"); + 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/zetime/ZeTimeConstants.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java new file mode 100644 index 000000000..d36f0e5f7 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeConstants.java @@ -0,0 +1,92 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.zetime; + +/** + * Created by lightforce on 08.06.18. + */ + +import java.util.UUID; + +public class ZeTimeConstants { + public static final UUID UUID_WRITE_CHARACTERISTIC = UUID.fromString("00008001-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_ACK_CHARACTERISTIC = UUID.fromString("00008002-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_REPLY_CHARACTERISTIC = UUID.fromString("00008003-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_NOTIFY_CHARACTERISTIC = UUID.fromString("00008004-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_8005 = UUID.fromString("00008005-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CHARACTERISTIC_HEART_RATE = UUID.fromString("00002a37-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_CONFIG_DESCRIPTOR = UUID.fromString("00002902-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_SERVICE_BASE = UUID.fromString("00006006-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_SERVICE_EXTEND = UUID.fromString("00007006-0000-1000-8000-00805f9b34fb"); + public static final UUID UUID_SERVICE_HEART_RATE = UUID.fromString("0000180d-0000-1000-8000-00805f9b34fb"); + + public static final byte CMD_PREAMBLE = (byte) 0x6f; + // list all available commands + public static final byte CMD_RESPOND = (byte) 0x01; + public static final byte CMD_WATCH_ID = (byte) 0x02; + public static final byte CMD_DEVICE_VERSION = (byte) 0x03; + public static final byte CMD_DATE_TIME = (byte) 0x04; + public static final byte CMD_TIME_SURFACE_SETTINGS = (byte) 0x05; + public static final byte CMD_SURFACE_DISPLAY_SETTIGNS = (byte) 0x06; + public static final byte CMD_SCREEN_BRIGHTNESS = (byte) 0x07; + public static final byte CMD_BATTERY_POWER = (byte) 0x08; + public static final byte CMD_VOLUME_SETTINGS = (byte) 0x09; + public static final byte CMD_SHOCK_MODE = (byte) 0x0A; + public static final byte CMD_LANGUAGE_SETTINGS = (byte) 0x0B; + public static final byte CMD_UNIT_SETTINGS = (byte) 0x0C; + public static final byte CMD_FACTORY_RESTORE = (byte) 0x0D; + public static final byte CMD_ENTER_UPGRADE_MODE = (byte) 0x0E; + public static final byte CMD_SHOCK_STRENGTH = (byte) 0x10; + public static final byte CMD_WORK_MODE = (byte) 0x12; + public static final byte CMD_SCREEN_ON_TIME = (byte) 0x13; + public static final byte CMD_SNOOZE = (byte) 0x14; + public static final byte CMD_DO_NOT_DISTURB = (byte) 0x15; + public static final byte CMD_USER_INFO = (byte) 0x30; + public static final byte CMD_USAGE_HABITS = (byte) 0x31; + public static final byte CMD_USER_NAME = (byte) 0x32; + public static final byte CMD_GOALS = (byte) 0x50; + public static final byte CMD_AVAIABLE_DATA = (byte) 0x52; + public static final byte CMD_DELETE_STEP_COUNT = (byte) 0x53; + public static final byte CMD_GET_STEP_COUNT = (byte) 0x54; + public static final byte CMD_DELETE_SLEEP_DATA = (byte) 0x55; + public static final byte CMD_GET_SLEEP_DATA = (byte) 0x56; + public static final byte CMD_DELETE_HEARTRATE_DATA = (byte) 0x5A; + public static final byte CMD_GET_HEARTRATE_DATA = (byte) 0x5B; + public static final byte CMD_AUTO_HEARTRATE = (byte) 0x5C; + public static final byte CMD_HEARTRATE_ALARM_LIMITS = (byte) 0x5D; + public static final byte CMD_INACTIVITY_ALERT = (byte) 0x5E; + public static final byte CMD_CALORIES_TYPE = (byte) 0x60; + public static final byte CMD_GET_HEARTRATE_EXDATA = (byte) 0x61; + public static final byte CMD_PUSH_EX_MSG = (byte) 0x76; + public static final byte CMD_PUSH_WEATHER_DATA = (byte) 0x77; + public static final byte CMD_PUSH_CALENDAR_DAY = (byte) 0x99; + public static final byte CMD_MUSIC_CONTROL = (byte) 0xD0; + // here are the action commands + public static final byte CMD_REQUEST = (byte) 0x70; + public static final byte CMD_SEND = (byte) 0x71; + public static final byte CMD_REQUEST_RESPOND = (byte) 0x80; + // further commands + public static final byte CMD_END = (byte) 0x8f; + public static final byte CMD_ACK_WRITE = (byte) 0x03; + // notification types and icons + public static final byte NOTIFICATION_MISSED_CALL = (byte) 0x00; + public static final byte NOTIFICATION_SMS = (byte) 0x01; + public static final byte NOTIFICATION_SOCIAL = (byte) 0x02; + public static final byte NOTIFICATION_EMAIL = (byte) 0x03; + public static final byte NOTIFICATION_CALENDAR = (byte) 0x04; + public static final byte NOTIFICATION_INCOME_CALL = (byte) 0x05; + public static final byte NOTIFICATION_CALL_OFF = (byte) 0x06; + public static final byte NOTIFICATION_WECHAT = (byte) 0x07; + public static final byte NOTIFICATION_VIBER = (byte) 0x08; + public static final byte NOTIFICATION_SNAPCHAT = (byte) 0x09; + public static final byte NOTIFICATION_WHATSAPP = (byte) 0x0A; + public static final byte NOTIFICATION_QQ = (byte) 0x0B; + public static final byte NOTIFICATION_FACEBOOK = (byte) 0x0C; + public static final byte NOTIFICATION_HANGOUTS = (byte) 0x0D; + public static final byte NOTIFICATION_GMAIL = (byte) 0x0E; + public static final byte NOTIFICATION_MESSENGER = (byte) 0x0F; + public static final byte NOTIFICATION_INSTAGRAM = (byte) 0x10; + public static final byte NOTIFICATION_TWITTER = (byte) 0x11; + public static final byte NOTIFICATION_LINKEDIN = (byte) 0x12; + public static final byte NOTIFICATION_UBER = (byte) 0x13; + public static final byte NOTIFICATION_LINE = (byte) 0x14; + public static final byte NOTIFICATION_SKYPE = (byte) 0x15; +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java new file mode 100644 index 000000000..7e59b77be --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeCoordinator.java @@ -0,0 +1,134 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.zetime; + +import android.app.Activity; +import android.bluetooth.le.ScanFilter; +import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +import java.util.Collection; + +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.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; + +/** + * Created by lightwars on 06.02.18. + */ + +public class ZeTimeCoordinator extends AbstractDeviceCoordinator { + @Override + public DeviceType getDeviceType() { + return DeviceType.ZETIME; + } + + @NonNull + @Override + public Collection createBLEScanFilters() { + return super.createBLEScanFilters(); + } + + @NonNull + @Override + public DeviceType getSupportedType(GBDeviceCandidate candidate) { + String name = candidate.getDevice().getName(); + if (name != null && name.startsWith("ZeTime")) { + return DeviceType.ZETIME; + } + return DeviceType.UNKNOWN; + } + + @Nullable + @Override + public Class getPairingActivity() { + return null; + } + + @Override + public boolean supportsScreenshots() { + return false; + } + + @Override + public boolean supportsAlarmConfiguration() { + return true; + } + + @Override + public boolean supportsWeather() { + return true; + } + + @Override + public InstallHandler findInstallHandler(Uri uri, Context context) { + return null; + } + + @Override + public String getManufacturer() { + return "MyKronoz"; + } + + @Override + public boolean supportsActivityTracking() { + return true; + } + + @Override + public Class getAppsManagementActivity() { + return null; + } + + @Override + public boolean supportsHeartRateMeasurement(GBDevice device) { + return true; + } + + @Override + public boolean supportsActivityDataFetching() { + return true; + } + + @Override + public boolean supportsAppsManagement() { + return false; + } + + @Override + public boolean supportsCalendarEvents() { + return true; + } + + @Override + public SampleProvider getSampleProvider(GBDevice device, DaoSession session) { + return new ZeTimeSampleProvider(device, session); + } + + @Override + protected void deleteDevice(@NonNull GBDevice gbDevice, @NonNull Device device, @NonNull DaoSession session) throws GBException { + + } + + @Override + public boolean supportsSmartWakeup(GBDevice device) { + return false; + } + + @Override + public boolean supportsRealtimeData() { + return false; + } + + @Override + public int getBondingStyle(GBDevice device) { + return BONDING_STYLE_NONE; + } +} diff --git a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java new file mode 100644 index 000000000..8ce05e4a1 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/devices/zetime/ZeTimeSampleProvider.java @@ -0,0 +1,69 @@ +package nodomain.freeyourgadget.gadgetbridge.devices.zetime; + +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.ZeTimeActivitySample; +import nodomain.freeyourgadget.gadgetbridge.entities.ZeTimeActivitySampleDao; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; + +public class ZeTimeSampleProvider extends AbstractSampleProvider { + + private final float movementDivisor = 6000.0f; + private GBDevice mDevice; + private DaoSession mSession; + + public ZeTimeSampleProvider(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 ZeTimeActivitySample createActivitySample() { + return new ZeTimeActivitySample(); + } + + @Override + public AbstractDao getSampleDao() { + return getSession().getZeTimeActivitySampleDao(); + } + + @Nullable + @Override + protected Property getRawKindSampleProperty() { + return ZeTimeActivitySampleDao.Properties.RawKind; + } + + @NonNull + @Override + protected Property getTimestampSampleProperty() { + return ZeTimeActivitySampleDao.Properties.Timestamp; + } + + @NonNull + @Override + protected Property getDeviceIdentifierSampleProperty() { + return ZeTimeActivitySampleDao.Properties.DeviceId; + } +} 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 78b79ab38..82e07a1af 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/DeviceType.java @@ -46,6 +46,7 @@ public enum DeviceType { NO1F1(50, R.drawable.ic_device_hplus, R.drawable.ic_device_hplus_disabled, R.string.devicetype_no1_f1), TECLASTH30(60, R.drawable.ic_device_h30_h10, R.drawable.ic_device_h30_h10_disabled, R.string.devicetype_teclast_h30), XWATCH(70, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_xwatch), + ZETIME(80, R.drawable.ic_device_default, R.drawable.ic_device_default_disabled, R.string.devicetype_mykronoz_zetime), 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/model/Weather.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java index 347bef6e8..f2c5a9cf6 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/model/Weather.java @@ -599,4 +599,114 @@ public class Weather { return ""; } } + + public static byte mapToZeTimeCondition(int openWeatherMapCondition) { +/* deducted values: + 0 = partly cloudy + 1 = cloudy + 2 = sunny + 3 = windy/gale + 4 = heavy rain + 5 = snowy + 6 = storm + */ + switch (openWeatherMapCondition) { +//Group 2xx: Thunderstorm + case 200: //thunderstorm with light rain: //11d + case 201: //thunderstorm with rain: //11d + case 202: //thunderstorm with heavy rain: //11d + case 210: //light thunderstorm:: //11d + case 211: //thunderstorm: //11d + case 230: //thunderstorm with light drizzle: //11d + case 231: //thunderstorm with drizzle: //11d + case 232: //thunderstorm with heavy drizzle: //11d + case 212: //heavy thunderstorm: //11d + case 221: //ragged thunderstorm: //11d +//Group 7xx: Atmosphere + case 771: //squalls: //[[file:50d.png]] + case 781: //tornado: //[[file:50d.png]] +//Group 90x: Extreme + case 900: //tornado + case 901: //tropical storm +//Group 9xx: Additional + case 960: //storm + case 961: //violent storm + case 902: //hurricane + case 962: //hurricane + return 6; +//Group 3xx: Drizzle + case 300: //light intensity drizzle: //09d + case 301: //drizzle: //09d + case 302: //heavy intensity drizzle: //09d + case 310: //light intensity drizzle rain: //09d + case 311: //drizzle rain: //09d + case 312: //heavy intensity drizzle rain: //09d + case 313: //shower rain and drizzle: //09d + case 314: //heavy shower rain and drizzle: //09d + case 321: //shower drizzle: //09d +//Group 5xx: Rain + case 500: //light rain: //10d + case 501: //moderate rain: //10d + case 502: //heavy intensity rain: //10d + case 503: //very heavy rain: //10d + case 504: //extreme rain: //10d + case 511: //freezing rain: //13d + case 520: //light intensity shower rain: //09d + case 521: //shower rain: //09d + case 522: //heavy intensity shower rain: //09d + case 531: //ragged shower rain: //09d +//Group 90x: Extreme + case 906: //hail + return 4; +//Group 6xx: Snow + case 600: //light snow: //[[file:13d.png]] + case 601: //snow: //[[file:13d.png]] + case 620: //light shower snow: //[[file:13d.png]] + case 602: //heavy snow: //[[file:13d.png]] + case 611: //sleet: //[[file:13d.png]] + case 612: //shower sleet: //[[file:13d.png]] + case 621: //shower snow: //[[file:13d.png]] + case 622: //heavy shower snow: //[[file:13d.png]] + case 615: //light rain and snow: //[[file:13d.png]] + case 616: //rain and snow: //[[file:13d.png]] +//Group 90x: Extreme + case 903: //cold + return 5; +//Group 7xx: Atmosphere + case 701: //mist: //[[file:50d.png]] + case 711: //smoke: //[[file:50d.png]] + case 721: //haze: //[[file:50d.png]] + case 731: //sandcase dust whirls: //[[file:50d.png]] + case 741: //fog: //[[file:50d.png]] + case 751: //sand: //[[file:50d.png]] + case 761: //dust: //[[file:50d.png]] + case 762: //volcanic ash: //[[file:50d.png]] + return 1; +//Group 800: Clear + case 800: //clear sky: //[[file:01d.png]] [[file:01n.png]] +//Group 90x: Extreme + case 904: //hot + return 2; +//Group 80x: Clouds + case 801: //few clouds: //[[file:02d.png]] [[file:02n.png]] + case 802: //scattered clouds: //[[file:03d.png]] [[file:03d.png]] + case 803: //broken clouds: //[[file:04d.png]] [[file:03d.png]] + case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]] + default: + return 0; + +//Group 9xx: Additional + case 905: //windy + case 951: //calm + case 952: //light breeze + case 953: //gentle breeze + case 954: //moderate breeze + case 955: //fresh breeze + case 956: //strong breeze + case 957: //high windcase near gale + case 958: //gale + case 959: //severe gale + return 3; + } + } } 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 555bdaec4..33e9f15d5 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/DeviceSupportFactory.java @@ -41,6 +41,7 @@ import nodomain.freeyourgadget.gadgetbridge.service.devices.vibratissimo.Vibrati import nodomain.freeyourgadget.gadgetbridge.service.devices.hplus.HPlusSupport; import nodomain.freeyourgadget.gadgetbridge.service.devices.jyou.TeclastH30Support; import nodomain.freeyourgadget.gadgetbridge.service.devices.xwatch.XWatchSupport; +import nodomain.freeyourgadget.gadgetbridge.service.devices.zetime.ZeTimeDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.util.GB; public class DeviceSupportFactory { @@ -151,6 +152,9 @@ public class DeviceSupportFactory { break; case XWATCH: deviceSupport = new ServiceDeviceSupport(new XWatchSupport(), EnumSet.of(ServiceDeviceSupport.Flags.BUSY_CHECKING)); + case ZETIME: + deviceSupport = new ServiceDeviceSupport(new ZeTimeDeviceSupport(), 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/zetime/ZeTimeDeviceSupport.java b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java new file mode 100644 index 000000000..902bc5b31 --- /dev/null +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/service/devices/zetime/ZeTimeDeviceSupport.java @@ -0,0 +1,1131 @@ +package nodomain.freeyourgadget.gadgetbridge.service.devices.zetime; + +import android.bluetooth.BluetoothGatt; +import android.bluetooth.BluetoothGattCharacteristic; +import android.net.Uri; +import android.widget.Toast; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.UUID; + +import nodomain.freeyourgadget.gadgetbridge.GBApplication; +import nodomain.freeyourgadget.gadgetbridge.R; +import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; +import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventMusicControl; +import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; +import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeConstants; +import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeSampleProvider; +import nodomain.freeyourgadget.gadgetbridge.entities.ZeTimeActivitySample; +import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; +import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; +import nodomain.freeyourgadget.gadgetbridge.model.Alarm; +import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; +import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; +import nodomain.freeyourgadget.gadgetbridge.model.CannedMessagesSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicSpec; +import nodomain.freeyourgadget.gadgetbridge.model.MusicStateSpec; +import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; +import nodomain.freeyourgadget.gadgetbridge.model.Weather; +import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; +import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; +import nodomain.freeyourgadget.gadgetbridge.service.btle.GattService; +import nodomain.freeyourgadget.gadgetbridge.service.btle.TransactionBuilder; +import nodomain.freeyourgadget.gadgetbridge.service.btle.actions.SetDeviceStateAction; +import nodomain.freeyourgadget.gadgetbridge.util.GB; + +/** + * Created by Kranz on 08.02.2018. + */ + +public class ZeTimeDeviceSupport extends AbstractBTLEDeviceSupport { + private static final Logger LOG = LoggerFactory.getLogger(ZeTimeDeviceSupport.class); + private final GBDeviceEventBatteryInfo batteryCmd = new GBDeviceEventBatteryInfo(); + private final GBDeviceEventVersionInfo versionCmd = new GBDeviceEventVersionInfo(); + private final GBDeviceEventMusicControl musicCmd = new GBDeviceEventMusicControl(); + private final int sixHourOffset = 21600; + private byte[] lastMsg; + private byte msgPart; + private int availableSleepData; + private int availableStepsData; + private int availableHeartRateData; + private int progressSteps; + private int progressSleep; + private int progressHeartRate; + private final int maxMsgLength = 20; + private boolean callIncoming = false; + private String songtitle = null; + private byte musicState = -1; + public byte[] music = null; + public byte volume = 50; + + public BluetoothGattCharacteristic notifyCharacteristic = null; + public BluetoothGattCharacteristic writeCharacteristic = null; + public BluetoothGattCharacteristic ackCharacteristic = null; + public BluetoothGattCharacteristic replyCharacteristic = null; + + public ZeTimeDeviceSupport(){ + super(LOG); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); + addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); + addSupportedService(ZeTimeConstants.UUID_SERVICE_BASE); + addSupportedService(ZeTimeConstants.UUID_SERVICE_EXTEND); + addSupportedService(ZeTimeConstants.UUID_SERVICE_HEART_RATE); + } + @Override + protected TransactionBuilder initializeDevice(TransactionBuilder builder) { + LOG.info("Initializing"); + msgPart = 0; + availableStepsData = 0; + availableHeartRateData = 0; + availableSleepData = 0; + progressSteps = 0; + progressSleep = 0; + progressHeartRate = 0; + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZING, getContext())); + + notifyCharacteristic = getCharacteristic(ZeTimeConstants.UUID_NOTIFY_CHARACTERISTIC); + writeCharacteristic = getCharacteristic(ZeTimeConstants.UUID_WRITE_CHARACTERISTIC); + ackCharacteristic = getCharacteristic(ZeTimeConstants.UUID_ACK_CHARACTERISTIC); + replyCharacteristic = getCharacteristic(ZeTimeConstants.UUID_REPLY_CHARACTERISTIC); + + builder.notify(ackCharacteristic, true); + builder.notify(notifyCharacteristic, true); + requestDeviceInfo(builder); + requestBatteryInfo(builder); + requestActivityInfo(builder); + synchronizeTime(builder); + + replyMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_MUSIC_CONTROL, + ZeTimeConstants.CMD_REQUEST_RESPOND, + 0x02, + 0x00, + 0x02, + volume, + ZeTimeConstants.CMD_END}); + + builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); + LOG.info("Initialization Done"); + return builder; + } + + @Override + public void onSendConfiguration(String config) { + + } + + @Override + public void onDeleteCalendarEvent(byte type, long id) { + + } + + @Override + public void onHeartRateTest() { + + } + + @Override + public void onEnableRealtimeSteps(boolean enable) { + + } + + @Override + public void onFindDevice(boolean start) { + + } + + @Override + public boolean useAutoConnect() { + return false; + } + + @Override + public void onSetHeartRateMeasurementInterval(int seconds) { + + } + + @Override + public void onSetAlarms(ArrayList alarms) { + + } + + @Override + public void onAppConfiguration(UUID appUuid, String config, Integer id) { + + } + + @Override + public void onEnableHeartRateSleepSupport(boolean enable) { + + } + + @Override + public void onSetMusicInfo(MusicSpec musicSpec) { + songtitle = musicSpec.track; + if(musicState != -1) { + music = new byte[songtitle.getBytes(StandardCharsets.UTF_8).length + 7]; // 7 bytes for status and overhead + music[0] = ZeTimeConstants.CMD_PREAMBLE; + music[1] = ZeTimeConstants.CMD_MUSIC_CONTROL; + music[2] = ZeTimeConstants.CMD_REQUEST_RESPOND; + music[3] = (byte) ((songtitle.getBytes(StandardCharsets.UTF_8).length + 1) & 0xff); + music[4] = (byte) ((songtitle.getBytes(StandardCharsets.UTF_8).length + 1) >> 8); + music[5] = musicState; + System.arraycopy(songtitle.getBytes(StandardCharsets.UTF_8), 0, music, 6, songtitle.getBytes(StandardCharsets.UTF_8).length); + music[music.length - 1] = ZeTimeConstants.CMD_END; + if (music != null) { + try { + TransactionBuilder builder = performInitialized("setMusicStateInfo"); + replyMsgToWatch(builder, music); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error setting music state and info: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + } + + @Override + public void onSetCallState(CallSpec callSpec) { + int subject_length = 0; + int notification_length = 0; + byte[] subject = null; + byte[] notification = null; + Calendar time = GregorianCalendar.getInstance(); + // convert every single digit of the date to ascii characters + // we do it like so: use the base chrachter of '0' and add the digit + byte[] datetimeBytes = new byte[]{ + (byte) ((time.get(Calendar.YEAR) / 1000) + '0'), + (byte) (((time.get(Calendar.YEAR) / 100)%10) + '0'), + (byte) (((time.get(Calendar.YEAR) / 10)%10) + '0'), + (byte) ((time.get(Calendar.YEAR)%10) + '0'), + (byte) (((time.get(Calendar.MONTH)+1)/10) + '0'), + (byte) (((time.get(Calendar.MONTH)+1)%10) + '0'), + (byte) ((time.get(Calendar.DAY_OF_MONTH)/10) + '0'), + (byte) ((time.get(Calendar.DAY_OF_MONTH)%10) + '0'), + (byte) 'T', + (byte) ((time.get(Calendar.HOUR_OF_DAY)/10) + '0'), + (byte) ((time.get(Calendar.HOUR_OF_DAY)%10) + '0'), + (byte) ((time.get(Calendar.MINUTE)/10) + '0'), + (byte) ((time.get(Calendar.MINUTE)%10) + '0'), + (byte) ((time.get(Calendar.SECOND)/10) + '0'), + (byte) ((time.get(Calendar.SECOND)%10) + '0'), + }; + + if(callIncoming || (callSpec.command == CallSpec.CALL_INCOMING)) { + if (callSpec.command == CallSpec.CALL_INCOMING) { + if (callSpec.name != null) { + notification_length += callSpec.name.getBytes(StandardCharsets.UTF_8).length; + subject_length = callSpec.name.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(callSpec.name.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } else if (callSpec.number != null) { + notification_length += callSpec.number.getBytes(StandardCharsets.UTF_8).length; + subject_length = callSpec.number.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(callSpec.number.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } + notification_length += datetimeBytes.length + 10; // add message overhead + notification = new byte[notification_length]; + notification[0] = ZeTimeConstants.CMD_PREAMBLE; + notification[1] = ZeTimeConstants.CMD_PUSH_EX_MSG; + notification[2] = ZeTimeConstants.CMD_SEND; + notification[3] = (byte) ((notification_length - 6) & 0xff); + notification[4] = (byte) ((notification_length - 6) >> 8); + notification[5] = ZeTimeConstants.NOTIFICATION_INCOME_CALL; + notification[6] = 1; + notification[7] = (byte) subject_length; + notification[8] = (byte) 0; + System.arraycopy(subject, 0, notification, 9, subject_length); + System.arraycopy(datetimeBytes, 0, notification, 9 + subject_length, datetimeBytes.length); + notification[notification_length - 1] = ZeTimeConstants.CMD_END; + callIncoming = true; + } else { + notification_length = datetimeBytes.length + 10; // add message overhead + notification = new byte[notification_length]; + notification[0] = ZeTimeConstants.CMD_PREAMBLE; + notification[1] = ZeTimeConstants.CMD_PUSH_EX_MSG; + notification[2] = ZeTimeConstants.CMD_SEND; + notification[3] = (byte) ((notification_length - 6) & 0xff); + notification[4] = (byte) ((notification_length - 6) >> 8); + notification[5] = ZeTimeConstants.NOTIFICATION_CALL_OFF; + notification[6] = 1; + notification[7] = (byte) 0; + notification[8] = (byte) 0; + System.arraycopy(datetimeBytes, 0, notification, 9, datetimeBytes.length); + notification[notification_length - 1] = ZeTimeConstants.CMD_END; + callIncoming = false; + } + if(notification != null) + { + try { + TransactionBuilder builder = performInitialized("setCallState"); + sendMsgToWatch(builder, notification); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error set call state: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + + } + + @Override + public void onAppStart(UUID uuid, boolean start) { + + } + + @Override + public void onEnableRealtimeHeartRateMeasurement(boolean enable) { + + } + + @Override + public void onSetConstantVibration(int integer) { + + } + + @Override + public void onFetchRecordedData(int dataTypes) { + try { + TransactionBuilder builder = performInitialized("fetchActivityData"); + requestActivityInfo(builder); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error on fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onTestNewFunction() { + + } + + @Override + public void onSetMusicState(MusicStateSpec stateSpec) { + musicState = stateSpec.state; + if(songtitle != null) { + music = new byte[songtitle.getBytes(StandardCharsets.UTF_8).length + 7]; // 7 bytes for status and overhead + music[0] = ZeTimeConstants.CMD_PREAMBLE; + music[1] = ZeTimeConstants.CMD_MUSIC_CONTROL; + music[2] = ZeTimeConstants.CMD_REQUEST_RESPOND; + music[3] = (byte) ((songtitle.getBytes(StandardCharsets.UTF_8).length + 1) & 0xff); + music[4] = (byte) ((songtitle.getBytes(StandardCharsets.UTF_8).length + 1) >> 8); + if (stateSpec.state == MusicStateSpec.STATE_PLAYING) { + music[5] = 0; + } else { + music[5] = 1; + } + System.arraycopy(songtitle.getBytes(StandardCharsets.UTF_8), 0, music, 6, songtitle.getBytes(StandardCharsets.UTF_8).length); + music[music.length - 1] = ZeTimeConstants.CMD_END; + if (music != null) { + try { + TransactionBuilder builder = performInitialized("setMusicStateInfo"); + replyMsgToWatch(builder, music); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error setting music state and info: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + } + + @Override + public void onAddCalendarEvent(CalendarEventSpec calendarEventSpec) { + Calendar time = GregorianCalendar.getInstance(); + byte[] CalendarEvent = new byte[calendarEventSpec.title.getBytes(StandardCharsets.UTF_8).length + 16]; // 26 bytes for calendar and overhead + time.setTimeInMillis(calendarEventSpec.timestamp); + CalendarEvent[0] = ZeTimeConstants.CMD_PREAMBLE; + CalendarEvent[1] = ZeTimeConstants.CMD_PUSH_CALENDAR_DAY; + CalendarEvent[2] = ZeTimeConstants.CMD_SEND; + CalendarEvent[3] = (byte)((calendarEventSpec.title.getBytes(StandardCharsets.UTF_8).length + 10) & 0xff); + CalendarEvent[4] = (byte)((calendarEventSpec.title.getBytes(StandardCharsets.UTF_8).length + 10) >> 8); + CalendarEvent[5] = (byte)(calendarEventSpec.type + 0x1); + CalendarEvent[6] = (byte)(time.get(Calendar.YEAR) & 0xff); + CalendarEvent[7] = (byte)(time.get(Calendar.YEAR) >> 8); + CalendarEvent[8] = (byte)(time.get(Calendar.MONTH)+1); + CalendarEvent[9] = (byte)time.get(Calendar.DAY_OF_MONTH); + CalendarEvent[10] = (byte) (time.get(Calendar.HOUR_OF_DAY) & 0xff); + CalendarEvent[11] = (byte) (time.get(Calendar.HOUR_OF_DAY) >> 8); + CalendarEvent[12] = (byte) (time.get(Calendar.MINUTE) & 0xff); + CalendarEvent[13] = (byte) (time.get(Calendar.MINUTE) >> 8); + CalendarEvent[14] = (byte) calendarEventSpec.title.getBytes(StandardCharsets.UTF_8).length; + System.arraycopy(calendarEventSpec.title.getBytes(StandardCharsets.UTF_8), 0, CalendarEvent, 15, calendarEventSpec.title.getBytes(StandardCharsets.UTF_8).length); + CalendarEvent[CalendarEvent.length-1] = ZeTimeConstants.CMD_END; + if(CalendarEvent != null) + { + try { + TransactionBuilder builder = performInitialized("sendCalendarEvenr"); + sendMsgToWatch(builder, CalendarEvent); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error sending calendar event: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + + @Override + public void onSetTime() { + try { + TransactionBuilder builder = performInitialized("synchronizeTime"); + synchronizeTime(builder); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error setting the time: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + @Override + public void onAppDelete(UUID uuid) { + + } + + @Override + public void onAppInfoReq() { + + } + + @Override + public void onSetCannedMessages(CannedMessagesSpec cannedMessagesSpec) { + + } + + @Override + public void onReboot() { + + } + + @Override + public void onScreenshotReq() { + + } + + @Override + public void onSendWeather(WeatherSpec weatherSpec) { + byte[] weather = new byte[weatherSpec.location.getBytes(StandardCharsets.UTF_8).length + 26]; // 26 bytes for weatherdata and overhead + weather[0] = ZeTimeConstants.CMD_PREAMBLE; + weather[1] = ZeTimeConstants.CMD_PUSH_WEATHER_DATA; + weather[2] = ZeTimeConstants.CMD_SEND; + weather[3] = (byte)((weatherSpec.location.getBytes(StandardCharsets.UTF_8).length + 20) & 0xff); + weather[4] = (byte)((weatherSpec.location.getBytes(StandardCharsets.UTF_8).length + 20) >> 8); + weather[5] = 0; // celsius + weather[6] = (byte)(weatherSpec.currentTemp - 273); + weather[7] = (byte)(weatherSpec.todayMinTemp - 273); + weather[8] = (byte)(weatherSpec.todayMaxTemp - 273); + weather[9] = Weather.mapToZeTimeCondition(weatherSpec.currentConditionCode); + for(int forecast = 0; forecast < 3; forecast++) { + weather[10+(forecast*5)] = 0; // celsius + weather[11+(forecast*5)] = (byte) 0xff; + weather[12+(forecast*5)] = (byte) (weatherSpec.forecasts.get(forecast).minTemp - 273); + weather[13+(forecast*5)] = (byte) (weatherSpec.forecasts.get(forecast).maxTemp - 273); + weather[14+(forecast*5)] = Weather.mapToZeTimeCondition(weatherSpec.forecasts.get(forecast).conditionCode); + } + System.arraycopy(weatherSpec.location.getBytes(StandardCharsets.UTF_8), 0, weather, 25, weatherSpec.location.getBytes(StandardCharsets.UTF_8).length); + weather[weather.length-1] = ZeTimeConstants.CMD_END; + if(weather != null) + { + try { + TransactionBuilder builder = performInitialized("sendWeahter"); + sendMsgToWatch(builder, weather); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error sending weather: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + + @Override + public void onAppReorder(UUID[] uuids) { + + } + + @Override + public void onInstallApp(Uri uri) { + + } + + @Override + public void onDeleteNotification(int id) { + + } + + @Override + public void onNotification(NotificationSpec notificationSpec) { + + int subject_length = 0; + int body_length = notificationSpec.body.getBytes(StandardCharsets.UTF_8).length; + if(body_length > 256) + { + body_length = 256; + } + int notification_length = body_length; + byte[] subject = null; + byte[] notification = null; + Calendar time = GregorianCalendar.getInstance(); + // convert every single digit of the date to ascii characters + // we do it like so: use the base chrachter of '0' and add the digit + byte[] datetimeBytes = new byte[]{ + (byte) ((time.get(Calendar.YEAR) / 1000) + '0'), + (byte) (((time.get(Calendar.YEAR) / 100)%10) + '0'), + (byte) (((time.get(Calendar.YEAR) / 10)%10) + '0'), + (byte) ((time.get(Calendar.YEAR)%10) + '0'), + (byte) (((time.get(Calendar.MONTH)+1)/10) + '0'), + (byte) (((time.get(Calendar.MONTH)+1)%10) + '0'), + (byte) ((time.get(Calendar.DAY_OF_MONTH)/10) + '0'), + (byte) ((time.get(Calendar.DAY_OF_MONTH)%10) + '0'), + (byte) 'T', + (byte) ((time.get(Calendar.HOUR_OF_DAY)/10) + '0'), + (byte) ((time.get(Calendar.HOUR_OF_DAY)%10) + '0'), + (byte) ((time.get(Calendar.MINUTE)/10) + '0'), + (byte) ((time.get(Calendar.MINUTE)%10) + '0'), + (byte) ((time.get(Calendar.SECOND)/10) + '0'), + (byte) ((time.get(Calendar.SECOND)%10) + '0'), + }; + + if (notificationSpec.sender != null) + { + notification_length += notificationSpec.sender.getBytes(StandardCharsets.UTF_8).length; + subject_length = notificationSpec.sender.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(notificationSpec.sender.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } else if(notificationSpec.phoneNumber != null) + { + notification_length += notificationSpec.phoneNumber.getBytes(StandardCharsets.UTF_8).length; + subject_length = notificationSpec.phoneNumber.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(notificationSpec.phoneNumber.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } else if(notificationSpec.subject != null) + { + notification_length += notificationSpec.subject.getBytes(StandardCharsets.UTF_8).length; + subject_length = notificationSpec.subject.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(notificationSpec.subject.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } else if(notificationSpec.title != null) + { + notification_length += notificationSpec.title.getBytes(StandardCharsets.UTF_8).length; + subject_length = notificationSpec.title.getBytes(StandardCharsets.UTF_8).length; + subject = new byte[subject_length]; + System.arraycopy(notificationSpec.title.getBytes(StandardCharsets.UTF_8), 0, subject, 0, subject_length); + } + notification_length += datetimeBytes.length + 10; // add message overhead + notification = new byte[notification_length]; + notification[0] = ZeTimeConstants.CMD_PREAMBLE; + notification[1] = ZeTimeConstants.CMD_PUSH_EX_MSG; + notification[2] = ZeTimeConstants.CMD_SEND; + notification[3] = (byte)((notification_length-6) & 0xff); + notification[4] = (byte)((notification_length-6) >> 8); + notification[6] = 1; + notification[7] = (byte)subject_length; + notification[8] = (byte)body_length; + System.arraycopy(subject, 0, notification, 9, subject_length); + System.arraycopy(notificationSpec.body.getBytes(StandardCharsets.UTF_8), 0, notification, 9+subject_length, body_length); + System.arraycopy(datetimeBytes, 0, notification, 9+subject_length+body_length, datetimeBytes.length); + notification[notification_length-1] = ZeTimeConstants.CMD_END; + + switch(notificationSpec.type) + { + case GENERIC_SMS: + notification[5] = ZeTimeConstants.NOTIFICATION_SMS; + break; + case GENERIC_PHONE: + notification[5] = ZeTimeConstants.NOTIFICATION_MISSED_CALL; + break; + case GMAIL: + case GOOGLE_INBOX: + case MAILBOX: + case OUTLOOK: + case YAHOO_MAIL: + case GENERIC_EMAIL: + notification[5] = ZeTimeConstants.NOTIFICATION_EMAIL; + break; + case WECHAT: + notification[5] = ZeTimeConstants.NOTIFICATION_WECHAT; + break; + case VIBER: + notification[5] = ZeTimeConstants.NOTIFICATION_VIBER; + break; + case WHATSAPP: + notification[5] = ZeTimeConstants.NOTIFICATION_WHATSAPP; + break; + case FACEBOOK: + case FACEBOOK_MESSENGER: + notification[5] = ZeTimeConstants.NOTIFICATION_FACEBOOK; + break; + case GOOGLE_HANGOUTS: + notification[5] = ZeTimeConstants.NOTIFICATION_HANGOUTS; + break; + case LINE: + notification[5] = ZeTimeConstants.NOTIFICATION_LINE; + break; + case SKYPE: + notification[5] = ZeTimeConstants.NOTIFICATION_SKYPE; + break; + case CONVERSATIONS: + case RIOT: + case SIGNAL: + case TELEGRAM: + case THREEMA: + case KONTALK: + case ANTOX: + case GOOGLE_MESSENGER: + case HIPCHAT: + case KIK: + case KAKAO_TALK: + case SLACK: + notification[5] = ZeTimeConstants.NOTIFICATION_MESSENGER; + break; + case SNAPCHAT: + notification[5] = ZeTimeConstants.NOTIFICATION_SNAPCHAT; + break; + case INSTAGRAM: + notification[5] = ZeTimeConstants.NOTIFICATION_INSTAGRAM; + break; + case TWITTER: + notification[5] = ZeTimeConstants.NOTIFICATION_TWITTER; + break; + case LINKEDIN: + notification[5] = ZeTimeConstants.NOTIFICATION_LINKEDIN; + break; + case GENERIC_CALENDAR: + notification[5] = ZeTimeConstants.NOTIFICATION_CALENDAR; + break; + default: + notification[5] = ZeTimeConstants.NOTIFICATION_SOCIAL; + break; + } + if(notification != null) + { + try { + TransactionBuilder builder = performInitialized("sendNotification"); + sendMsgToWatch(builder, notification); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error sending notification: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + + @Override + public boolean onCharacteristicChanged(BluetoothGatt gatt, + BluetoothGattCharacteristic characteristic) { + super.onCharacteristicChanged(gatt, characteristic); + + UUID characteristicUUID = characteristic.getUuid(); + if (ZeTimeConstants.UUID_ACK_CHARACTERISTIC.equals(characteristicUUID)) { + byte[] data = receiveCompleteMsg(characteristic.getValue()); + if(isMsgFormatOK(data)) { + switch (data[1]) { + case ZeTimeConstants.CMD_WATCH_ID: + break; + case ZeTimeConstants.CMD_DEVICE_VERSION: + handleDeviceInfo(data); + break; + case ZeTimeConstants.CMD_BATTERY_POWER: + handleBatteryInfo(data); + break; + case ZeTimeConstants.CMD_SHOCK_STRENGTH: + break; + case ZeTimeConstants.CMD_AVAIABLE_DATA: + handleActivityFetching(data); + break; + case ZeTimeConstants.CMD_GET_STEP_COUNT: + handleStepsData(data); + break; + case ZeTimeConstants.CMD_GET_SLEEP_DATA: + handleSleepData(data); + break; + case ZeTimeConstants.CMD_GET_HEARTRATE_EXDATA: + handleHeartRateData(data); + break; + case ZeTimeConstants.CMD_MUSIC_CONTROL: + handleMusicControl(data); + break; + } + } + return true; + } else if (ZeTimeConstants.UUID_NOTIFY_CHARACTERISTIC.equals(characteristicUUID)) + { + byte[] data = receiveCompleteMsg(characteristic.getValue()); + if(isMsgFormatOK(data)) { + switch (data[1]) + { + case ZeTimeConstants.CMD_MUSIC_CONTROL: + handleMusicControl(data); + break; + } + return true; + } + } + else { + LOG.info("Unhandled characteristic changed: " + characteristicUUID); + logMessageContent(characteristic.getValue()); + } + return false; + } + + private boolean isMsgFormatOK(byte[] msg) + { + if(msg != null) { + if (msg[0] == ZeTimeConstants.CMD_PREAMBLE) { + if ((msg[3] != 0) || (msg[4] != 0)) { + int payloadSize = (msg[4] << 8)&0xff00 | (msg[3]&0xff); + int msgLength = payloadSize + 6; + if (msgLength == msg.length) { + if (msg[msgLength - 1] == ZeTimeConstants.CMD_END) { + return true; + } + } + } + } + } + return false; + } + + private byte[] receiveCompleteMsg(byte[] msg) + { + if(msgPart == 0) { + int payloadSize = (msg[4] << 8)&0xff00 | (msg[3]&0xff); + if (payloadSize > 14) { + lastMsg = new byte[msg.length]; + System.arraycopy(msg, 0, lastMsg, 0, msg.length); + msgPart++; + return null; + } else { + return msg; + } + } else + { + byte[] completeMsg = new byte[lastMsg.length + msg.length]; + System.arraycopy(lastMsg, 0, completeMsg, 0, lastMsg.length); + System.arraycopy(msg, 0, completeMsg, lastMsg.length, msg.length); + msgPart = 0; + return completeMsg; + } + } + + private ZeTimeDeviceSupport requestBatteryInfo(TransactionBuilder builder) { + LOG.debug("Requesting Battery Info!"); + builder.write(writeCharacteristic,new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_BATTERY_POWER, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + return this; + } + + private ZeTimeDeviceSupport requestDeviceInfo(TransactionBuilder builder) { + LOG.debug("Requesting Device Info!"); + builder.write(writeCharacteristic,new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_WATCH_ID, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + + builder.write(writeCharacteristic,new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_DEVICE_VERSION, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x05, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + + builder.write(writeCharacteristic,new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_DEVICE_VERSION, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x02, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + return this; + } + + private ZeTimeDeviceSupport requestActivityInfo(TransactionBuilder builder) { + builder.write(writeCharacteristic, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_AVAIABLE_DATA, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + return this; + } + + private ZeTimeDeviceSupport requestShockStrength(TransactionBuilder builder) { + builder.write(writeCharacteristic, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_SHOCK_STRENGTH, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + return this; + } + + private void handleBatteryInfo(byte[] value) { + batteryCmd.level = ((short) value[5]); + if(batteryCmd.level <= 25) + { + batteryCmd.state = BatteryState.BATTERY_LOW; + } else + { + batteryCmd.state = BatteryState.BATTERY_NORMAL; + } + evaluateGBDeviceEvent(batteryCmd); + } + + private void handleDeviceInfo(byte[] value) { + value[value.length-1] = 0; // convert the end to a String end + byte[] string = Arrays.copyOfRange(value,6, value.length-1); + if(value[5] == 5) + { + versionCmd.fwVersion = new String(string); + } else{ + versionCmd.hwVersion = new String(string); + } + evaluateGBDeviceEvent(versionCmd); + } + + private void handleActivityFetching(byte[] msg) + { + availableStepsData = (int) ((msg[5]&0xff) | (msg[6] << 8)&0xff00); + availableSleepData = (int) ((msg[7]&0xff) | (msg[8] << 8)&0xff00); + availableHeartRateData= (int) ((msg[9]&0xff) | (msg[10] << 8)&0xff00); + if(availableStepsData > 0){ + getStepData(); + } else if(availableHeartRateData > 0) + { + getHeartRateData(); + } else if(availableSleepData > 0) + { + getSleepData(); + } + } + + private void getStepData() + { + try { + TransactionBuilder builder = performInitialized("fetchStepData"); + builder.write(writeCharacteristic, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_GET_STEP_COUNT, + ZeTimeConstants.CMD_REQUEST, + 0x02, + 0x00, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void getHeartRateData() + { + try { + TransactionBuilder builder = performInitialized("fetchStepData"); + builder.write(writeCharacteristic, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_GET_HEARTRATE_EXDATA, + ZeTimeConstants.CMD_REQUEST, + 0x01, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void getSleepData() + { + try { + TransactionBuilder builder = performInitialized("fetchStepData"); + builder.write(writeCharacteristic, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_GET_SLEEP_DATA, + ZeTimeConstants.CMD_REQUEST, + 0x02, + 0x00, + 0x00, + 0x00, + ZeTimeConstants.CMD_END}); + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error fetching activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + + private void handleStepsData(byte[] msg) + { + ZeTimeActivitySample sample = new ZeTimeActivitySample(); + int timestamp = (msg[10] << 24)&0xff000000 | (msg[9] << 16)&0xff0000 | (msg[8] << 8)&0xff00 | (msg[7]&0xff); + timestamp += sixHourOffset; // the timestamp from the watch has an offset of six hours, do not know why... + sample.setTimestamp(timestamp); + sample.setSteps((msg[14] << 24)&0xff000000 | (msg[13] << 16)&0xff0000 | (msg[12] << 8)&0xff00 | (msg[11]&0xff)); + sample.setCaloriesBurnt((msg[18] << 24)&0xff000000 | (msg[17] << 16)&0xff0000 | (msg[16] << 8)&0xff00 | (msg[15]&0xff)); + sample.setDistanceMeters((msg[22] << 24)&0xff000000 | (msg[21] << 16)&0xff0000 | (msg[20] << 8)&0xff00 | (msg[19]&0xff)); + sample.setActiveTimeMinutes((msg[26] << 24)&0xff000000 | (msg[25] << 16)&0xff0000 | (msg[24] << 8)&0xff00 | (msg[23]&0xff)); + sample.setRawKind(ActivityKind.TYPE_ACTIVITY); + sample.setRawIntensity(sample.getSteps()); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + sample.setUserId(DBHelper.getUser(dbHandler.getDaoSession()).getId()); + sample.setDeviceId(DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId()); + ZeTimeSampleProvider provider = new ZeTimeSampleProvider(getDevice(), dbHandler.getDaoSession()); + provider.addGBActivitySample(sample); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null,"Data transfer failed", false, 0, getContext()); + } + + progressSteps = (msg[5]&0xff) | ((msg[6] << 8)&0xff00); + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, (int) (progressSteps *100 / availableStepsData), getContext()); + if (progressSteps == availableStepsData) { + progressSteps = 0; + availableStepsData = 0; + GB.updateTransferNotification(null,"", false, 100, getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + if(availableHeartRateData > 0) { + getHeartRateData(); + } else if(availableSleepData > 0) + { + getSleepData(); + } + } + } + + private void handleSleepData(byte[] msg) + { + ZeTimeActivitySample sample = new ZeTimeActivitySample(); + int timestamp = (msg[10] << 24)&0xff000000 | (msg[9] << 16)&0xff0000 | (msg[8] << 8)&0xff00 | (msg[7]&0xff); + timestamp += sixHourOffset; // the timestamp from the watch has an offset of six hours, do not know why... + sample.setTimestamp(timestamp); + if(msg[11] == 0) { + sample.setRawKind(ActivityKind.TYPE_DEEP_SLEEP); + } else if(msg[11] == 1) + { + sample.setRawKind(ActivityKind.TYPE_LIGHT_SLEEP); + } else + { + sample.setRawKind(ActivityKind.TYPE_UNKNOWN); + } + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + sample.setUserId(DBHelper.getUser(dbHandler.getDaoSession()).getId()); + sample.setDeviceId(DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId()); + ZeTimeSampleProvider provider = new ZeTimeSampleProvider(getDevice(), dbHandler.getDaoSession()); + provider.addGBActivitySample(sample); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null,"Data transfer failed", false, 0, getContext()); + } + + progressSleep = (msg[5]&0xff) | (msg[6] << 8)&0xff00; + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, (int) (progressSleep *100 / availableSleepData), getContext()); + if (progressSleep == availableSleepData) { + progressSleep = 0; + availableSleepData = 0; + GB.updateTransferNotification(null,"", false, 100, getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + } + } + + private void handleHeartRateData(byte[] msg) + { + ZeTimeActivitySample sample = new ZeTimeActivitySample(); + int timestamp = (msg[10] << 24)&0xff000000 | (msg[9] << 16)&0xff0000 | (msg[8] << 8)&0xff00 | (msg[7]&0xff); + timestamp += sixHourOffset; // the timestamp from the watch has an offset of six hours, do not know why... + sample.setHeartRate(msg[11]); + sample.setTimestamp(timestamp); + + try (DBHandler dbHandler = GBApplication.acquireDB()) { + sample.setUserId(DBHelper.getUser(dbHandler.getDaoSession()).getId()); + sample.setDeviceId(DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId()); + ZeTimeSampleProvider provider = new ZeTimeSampleProvider(getDevice(), dbHandler.getDaoSession()); + provider.addGBActivitySample(sample); + } catch (Exception ex) { + GB.toast(getContext(), "Error saving steps data: " + ex.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + GB.updateTransferNotification(null,"Data transfer failed", false, 0, getContext()); + } + + progressHeartRate = (msg[5]&0xff) | ((msg[6] << 8)&0xff00); + GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, (int) (progressHeartRate *100 / availableHeartRateData), getContext()); + if (progressHeartRate == availableHeartRateData) { + progressHeartRate = 0; + availableHeartRateData = 0; + GB.updateTransferNotification(null,"", false, 100, getContext()); + if (getDevice().isBusy()) { + getDevice().unsetBusyTask(); + getDevice().sendDeviceUpdateIntent(getContext()); + } + if(availableSleepData > 0) + { + getSleepData(); + } + } + } + + private void sendMsgToWatch(TransactionBuilder builder, byte[] msg) + { + if(msg.length > maxMsgLength) + { + int msgpartlength = 0; + byte[] msgpart = null; + + do { + if((msg.length - msgpartlength) < maxMsgLength) + { + msgpart = new byte[msg.length - msgpartlength]; + System.arraycopy(msg, msgpartlength, msgpart, 0, msg.length - msgpartlength); + msgpartlength += (msg.length - msgpartlength); + } else { + msgpart = new byte[maxMsgLength]; + System.arraycopy(msg, msgpartlength, msgpart, 0, maxMsgLength); + msgpartlength += maxMsgLength; + } + builder.write(writeCharacteristic, msgpart); + }while(msgpartlength < msg.length); + } else + { + builder.write(writeCharacteristic, msg); + } + builder.write(ackCharacteristic, new byte[]{ZeTimeConstants.CMD_ACK_WRITE}); + } + + private void handleMusicControl(byte[] musicControlMsg) + { + if(musicControlMsg[2] == ZeTimeConstants.CMD_SEND) { + switch (musicControlMsg[5]) { + case 0: // play current song + musicCmd.event = GBDeviceEventMusicControl.Event.PLAY; + break; + case 1: // pause current song + musicCmd.event = GBDeviceEventMusicControl.Event.PAUSE; + break; + case 2: // skip to previous song + musicCmd.event = GBDeviceEventMusicControl.Event.PREVIOUS; + break; + case 3: // skip to next song + musicCmd.event = GBDeviceEventMusicControl.Event.NEXT; + break; + case 4: // change volume + if (musicControlMsg[6] > volume) { + musicCmd.event = GBDeviceEventMusicControl.Event.VOLUMEUP; + if(volume < 90) { + volume += 10; + } + } else { + musicCmd.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; + if(volume > 10) { + volume -= 10; + } + } + try { + TransactionBuilder builder = performInitialized("replyMusicVolume"); + replyMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_MUSIC_CONTROL, + ZeTimeConstants.CMD_REQUEST_RESPOND, + 0x02, + 0x00, + 0x02, + volume, + ZeTimeConstants.CMD_END}); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error reply the music volume: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + break; + } + evaluateGBDeviceEvent(musicCmd); + } else { + if (music != null) { + music[2] = ZeTimeConstants.CMD_REQUEST_RESPOND; + try { + TransactionBuilder builder = performInitialized("replyMusicState"); + replyMsgToWatch(builder, music); + builder.queue(getQueue()); + } catch (IOException e) { + GB.toast(getContext(), "Error reply the music state: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); + } + } + } + } + + private void replyMsgToWatch(TransactionBuilder builder, byte[] msg) + { + if(msg.length > maxMsgLength) + { + int msgpartlength = 0; + byte[] msgpart = null; + + do { + if((msg.length - msgpartlength) < maxMsgLength) + { + msgpart = new byte[msg.length - msgpartlength]; + System.arraycopy(msg, msgpartlength, msgpart, 0, msg.length - msgpartlength); + msgpartlength += (msg.length - msgpartlength); + } else { + msgpart = new byte[maxMsgLength]; + System.arraycopy(msg, msgpartlength, msgpart, 0, maxMsgLength); + msgpartlength += maxMsgLength; + } + builder.write(replyCharacteristic, msgpart); + }while(msgpartlength < msg.length); + } else + { + builder.write(replyCharacteristic, msg); + } + } + + private void synchronizeTime(TransactionBuilder builder) + { + Calendar now = GregorianCalendar.getInstance(); + byte[] timeSync = new byte[]{ZeTimeConstants.CMD_PREAMBLE, + ZeTimeConstants.CMD_DATE_TIME, + ZeTimeConstants.CMD_SEND, + 0x0c, + 0x00, + (byte)(now.get(Calendar.YEAR) & 0xff), + (byte)(now.get(Calendar.YEAR) >> 8), + (byte)(now.get(Calendar.MONTH) + 1), + (byte)now.get(Calendar.DAY_OF_MONTH), + (byte)now.get(Calendar.HOUR_OF_DAY), + (byte)now.get(Calendar.MINUTE), + (byte)now.get(Calendar.SECOND), + 0x00, // is 24h + 0x00, // SetTime after calibration + 0x01, // Unit + (byte)((now.get(Calendar.ZONE_OFFSET)/3600000) + (now.get(Calendar.DST_OFFSET)/3600000)), // TimeZone hour + daylight saving + 0x00, // TimeZone minute + ZeTimeConstants.CMD_END}; + sendMsgToWatch(builder, timeSync); + } +} 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 1b3a6279d..feab94917 100644 --- a/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java +++ b/app/src/main/java/nodomain/freeyourgadget/gadgetbridge/util/DeviceHelper.java @@ -58,6 +58,7 @@ import nodomain.freeyourgadget.gadgetbridge.devices.no1f1.No1F1Coordinator; import nodomain.freeyourgadget.gadgetbridge.devices.pebble.PebbleCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.vibratissimo.VibratissimoCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.xwatch.XWatchCoordinator; +import nodomain.freeyourgadget.gadgetbridge.devices.zetime.ZeTimeCoordinator; import nodomain.freeyourgadget.gadgetbridge.entities.Device; import nodomain.freeyourgadget.gadgetbridge.entities.DeviceAttributes; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; @@ -211,6 +212,7 @@ public class DeviceHelper { result.add(new EXRIZUK8Coordinator()); result.add(new TeclastH30Coordinator()); result.add(new XWatchCoordinator()); + result.add(new ZeTimeCoordinator()); return result; } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1b574c87c..fd44522e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -565,6 +565,7 @@ No.1 F1 Teclast H30 XWatch + MyKronoz ZeTime Choose export location Gadgetbridge notifications