/* Copyright (C) 2018-2024 Andreas Shimokawa, Arjan Schrijver, Carsten Pfeiffer, chklump, Damien Gaignon, José Rebelo, Maxim Baz, Petr Vaněk, Sebastian Kranz This file is part of Gadgetbridge. Gadgetbridge is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. Gadgetbridge is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 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.zetime; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.SharedPreferences; import android.widget.Toast; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.List; import java.util.Locale; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst; import nodomain.freeyourgadget.gadgetbridge.database.DBHandler; import nodomain.freeyourgadget.gadgetbridge.database.DBHelper; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventBatteryInfo; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventCallControl; 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.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.CalendarEventSpec; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarEvent; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; 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.service.btle.actions.WaitAction; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.Prefs; import nodomain.freeyourgadget.gadgetbridge.util.calendar.CalendarManager; import static nodomain.freeyourgadget.gadgetbridge.activities.devicesettings.DeviceSettingsPreferenceConst.PREF_SYNC_CALENDAR; 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 GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); private final int eightHourOffset = 28800; 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; private byte volume = 50; private byte[][] remindersOnWatch = new byte[3][10]; private BluetoothGattCharacteristic notifyCharacteristic = null; private BluetoothGattCharacteristic writeCharacteristic = null; private BluetoothGattCharacteristic ackCharacteristic = null; private 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); setUserInfo(builder); setUserGoals(builder); setLanguage(builder); requestActivityInfo(builder); synchronizeTime(builder); initMusicVolume(builder); onReadReminders(builder); sendUpcomingCalendarEvents(builder); return builder; } @Override public void onSendConfiguration(String config) { try { TransactionBuilder builder = performInitialized("sendConfiguration"); switch (config) { case DeviceSettingsPreferenceConst.PREF_WEARLOCATION: setWrist(builder); break; case DeviceSettingsPreferenceConst.PREF_LANGUAGE: setLanguage(builder); break; case ZeTimeConstants.PREF_SCREENTIME: setScreenTime(builder); break; case ZeTimeConstants.PREF_ANALOG_MODE: setAnalogMode(builder); break; case ZeTimeConstants.PREF_ACTIVITY_TRACKING: setActivityTracking(builder); break; case ZeTimeConstants.PREF_HANDMOVE_DISPLAY: setDisplayOnMovement(builder); break; case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO: case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START: case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END: setDoNotDisturb(builder); break; case ZeTimeConstants.PREF_CALORIES_TYPE: setCaloriesType(builder); break; case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT: setTimeFormate(builder); break; case ZeTimeConstants.PREF_DATE_FORMAT: setDateFormate(builder); break; case DeviceSettingsPreferenceConst.PREF_INACTIVITY_KEY: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_START: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_END: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU: setInactivityAlert(builder); break; case ZeTimeConstants.PREF_SMS_SIGNALING: case ZeTimeConstants.PREF_CALL_SIGNALING: case ZeTimeConstants.PREF_MISSED_CALL_SIGNALING: case ZeTimeConstants.PREF_EMAIL_SIGNALING: case ZeTimeConstants.PREF_SOCIAL_SIGNALING: case ZeTimeConstants.PREF_CALENDAR_SIGNALING: case ZeTimeConstants.PREF_INACTIVITY_SIGNALING: case ZeTimeConstants.PREF_LOW_POWER_SIGNALING: case ZeTimeConstants.PREF_ANTI_LOSS_SIGNALING: setSignaling(builder, config); break; case ZeTimeConstants.PREF_SHOCK_STRENGTH: setShockStrength(builder); case ZeTimeConstants.PREF_ZETIME_HEARTRATE_ALARM: case ZeTimeConstants.PREF_ZETIME_MAX_HEARTRATE: case ZeTimeConstants.PREF_ZETIME_MIN_HEARTRATE: setHeartRateLimits(builder); break; case DeviceSettingsPreferenceConst.PREF_USER_FITNESS_GOAL: case ActivityUser.PREF_USER_SLEEP_DURATION: case ActivityUser.PREF_USER_CALORIES_BURNT: case ActivityUser.PREF_USER_DISTANCE_METERS: case ActivityUser.PREF_USER_ACTIVETIME_MINUTES: setUserGoals(builder); break; } builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error sending configuration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @Override public void onFindDevice(boolean start) { try { TransactionBuilder builder = performInitialized("onFindDevice"); byte[] testSignaling = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_TEST_SIGNALING, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) (start ? 1 : 0), ZeTimeConstants.CMD_END }; sendMsgToWatch(builder, testSignaling); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error on function onFindDevice: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @Override public boolean useAutoConnect() { return false; } @Override public void onSetHeartRateMeasurementInterval(int seconds) { int heartRateMeasurementIntervall; // 0 means off heartRateMeasurementIntervall = seconds / 60; // zetime accepts only minutes byte[] heartrate = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_AUTO_HEARTRATE, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) heartRateMeasurementIntervall, ZeTimeConstants.CMD_END}; try { TransactionBuilder builder = performInitialized("enableAutoHeartRate"); sendMsgToWatch(builder, heartrate); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error enable auto heart rate measurement: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @Override public void onSetAlarms(ArrayList alarms) { byte[] alarmMessage = null; try { TransactionBuilder builder = performInitialized("setAlarms"); Prefs prefs = getDevicePrefs(); for (Alarm alarm : alarms) { if (remindersOnWatch[alarm.getPosition()][0] == 0) { alarmMessage = new byte[]{ ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_REMINDERS, ZeTimeConstants.CMD_SEND, (byte) 0xb, (byte) 0x0, (byte) 0x0,//(byte) alarm.getPosition(), // index ZeTimeConstants.REMINDER_ALARM, (byte) 0x0, // year low byte (byte) 0x0, // year high byte (byte) 0x0, // month (byte) 0x0, // day (byte) AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY), (byte) AlarmUtils.toCalendar(alarm).get(Calendar.MINUTE), (byte) alarm.getRepetition(), (byte) (alarm.getEnabled() ? 1 : 0), (byte) prefs.getInt(ZeTimeConstants.PREF_ALARM_SIGNALING, 11), // reminder signaling ZeTimeConstants.CMD_END }; System.arraycopy(alarmMessage, 6, remindersOnWatch[alarm.getPosition()], 0, 10); } else { alarmMessage = new byte[]{ ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_REMINDERS, ZeTimeConstants.CMD_SEND, (byte) 0x15, (byte) 0x0, (byte) 0x1, // edit alarm remindersOnWatch[alarm.getPosition()][0], remindersOnWatch[alarm.getPosition()][1], remindersOnWatch[alarm.getPosition()][2], remindersOnWatch[alarm.getPosition()][3], remindersOnWatch[alarm.getPosition()][4], remindersOnWatch[alarm.getPosition()][5], remindersOnWatch[alarm.getPosition()][6], remindersOnWatch[alarm.getPosition()][7], remindersOnWatch[alarm.getPosition()][8], remindersOnWatch[alarm.getPosition()][9], ZeTimeConstants.REMINDER_ALARM, (byte) 0x0, // year low byte (byte) 0x0, // year high byte (byte) 0x0, // month (byte) 0x0, // day (byte) AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY), (byte) AlarmUtils.toCalendar(alarm).get(Calendar.MINUTE), (byte) alarm.getRepetition(), (byte) (alarm.getEnabled() ? 1 : 0), (byte) prefs.getInt(ZeTimeConstants.PREF_ALARM_SIGNALING, 11), // reminder signaling ZeTimeConstants.CMD_END }; System.arraycopy(alarmMessage, 16, remindersOnWatch[alarm.getPosition()], 0, 10); } sendMsgToWatch(builder, alarmMessage); } builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error set alarms: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } @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; 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; 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; } 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 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() { // byte[] strength = { // ZeTimeConstants.CMD_PREAMBLE, // ZeTimeConstants.CMD_SHOCK_STRENGTH, // ZeTimeConstants.CMD_REQUEST, // (byte)0x1, // (byte)0x0, // (byte)0x0, // ZeTimeConstants.CMD_END // }; // try { // TransactionBuilder builder = performInitialized("testNewFunction"); // sendMsgToWatch(builder, strength); // builder.queue(getQueue()); // } catch (IOException e) { // GB.toast(getContext(), "Error on testing new function: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); // } } @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; 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) { // This is not used currently since we cannot add and remove calendar event one by one pebble style. byte[] calendarEvent = encodeCalendarEvent(calendarEventSpec.title, calendarEventSpec.timestamp, (byte) (calendarEventSpec.type + 1)); // HACK) try { TransactionBuilder builder = performInitialized("sendCalendarEvent"); sendMsgToWatch(builder, calendarEvent); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error sending calendar event: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private byte[] encodeCalendarEvent(String title, int timestamp, byte opcode) { Calendar time = GregorianCalendar.getInstance(); byte[] calendarEvent = new byte[title.getBytes(StandardCharsets.UTF_8).length + 16]; // 26 bytes for calendar and overhead time.setTimeInMillis(timestamp * 1000L); calendarEvent[0] = ZeTimeConstants.CMD_PREAMBLE; calendarEvent[1] = ZeTimeConstants.CMD_PUSH_CALENDAR_DAY; calendarEvent[2] = ZeTimeConstants.CMD_SEND; calendarEvent[3] = (byte) ((title.getBytes(StandardCharsets.UTF_8).length + 10) & 0xff); calendarEvent[4] = (byte) ((title.getBytes(StandardCharsets.UTF_8).length + 10) >> 8); // 0 = delete all expect the new one?. 4 = delete all?, 2 = add event?, 1 = first?, 3=last? calendarEvent[5] = opcode; 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.MINUTE) & 0xff); calendarEvent[12] = 0; // ? calendarEvent[13] = 0; // ? calendarEvent[14] = (byte) title.getBytes(StandardCharsets.UTF_8).length; System.arraycopy(title.getBytes(StandardCharsets.UTF_8), 0, calendarEvent, 15, title.getBytes(StandardCharsets.UTF_8).length); calendarEvent[calendarEvent.length - 1] = ZeTimeConstants.CMD_END; return calendarEvent; } private void sendUpcomingCalendarEvents(TransactionBuilder builder) { boolean syncCalendar = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(PREF_SYNC_CALENDAR, false); if (!syncCalendar) { return; } CalendarManager upcomingEvents = new CalendarManager(getContext(), getDevice().getAddress()); List calendarEvents = upcomingEvents.getCalendarEventList(); int eventCount = 0; for (CalendarEvent calendarEvent : calendarEvents) { if (calendarEvent.isAllDay()) { continue; } String body = calendarEvent.getTitle(); if (body == null) { body = ""; } String description = calendarEvent.getDescription(); if (description != null) { body += " : " + description; } byte opcode = 2; if (eventCount == 0) opcode = 1; byte[] message = encodeCalendarEvent(body, calendarEvent.getBeginSeconds(), opcode); sendMsgToWatch(builder, message); builder.add(new WaitAction(300)); // Urgh, seems it is a general problem when sending data too fast if (eventCount++ == 16) { // limit this to 16 for now break; } } } @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 onSendWeather(ArrayList weatherSpecs) { WeatherSpec weatherSpec = weatherSpecs.get(0); 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); boolean newWeather = false; if (versionCmd.fwVersion.length() >= 24) { String release = versionCmd.fwVersion.substring(8, 12); String buildnumber = versionCmd.fwVersion.substring(versionCmd.fwVersion.length() - 4); // if using firmware 1.7 Build 41 and above use newer icons if (release.compareTo("R1.7") > 0 || (release.compareTo("R1.7") == 0 && buildnumber.compareTo("B4.1") >= 0)) { newWeather = true; } } else { LOG.warn("We do not have a sane fw version string available, firmware too old/new?"); } if (newWeather) { weather[9] = Weather.mapToZeTimeCondition(weatherSpec.currentConditionCode); } else { weather[9] = Weather.mapToZeTimeConditionOld(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); if (newWeather) { weather[14 + (forecast * 5)] = Weather.mapToZeTimeCondition(weatherSpec.forecasts.get(forecast).conditionCode); } else { weather[14 + (forecast * 5)] = Weather.mapToZeTimeConditionOld(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; 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 onNotification(NotificationSpec notificationSpec) { int subject_length = 0; int body_length = notificationSpec.body.getBytes(StandardCharsets.UTF_8).length; if (body_length > 255) { body_length = 255; } int notification_length = body_length; byte[] subject = null; byte[] notification; //noinspection ExtractMethodRecommender 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 WIRE: 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; } 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_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; case ZeTimeConstants.CMD_TIME_SURFACE_SETTINGS: getDateTimeFormat(data); break; case ZeTimeConstants.CMD_SHOCK_MODE: getSignaling(data); break; case ZeTimeConstants.CMD_DO_NOT_DISTURB: getDoNotDisturb(data); break; case ZeTimeConstants.CMD_ANALOG_MODE: getAnalogMode(data); break; case ZeTimeConstants.CMD_CONTROL_DEVICE: getActivityTracking(data); break; case ZeTimeConstants.CMD_DISPLAY_TIMEOUT: getScreenTime(data); break; case ZeTimeConstants.CMD_USAGE_HABITS: getWrist(data); break; case ZeTimeConstants.CMD_AUTO_HEARTRATE: getHeartRateMeasurement(data); break; case ZeTimeConstants.CMD_HEARTRATE_ALARM_LIMITS: getHeartRateLimits(data); break; case ZeTimeConstants.CMD_INACTIVITY_ALERT: getInactivityAlert(data); break; case ZeTimeConstants.CMD_CALORIES_TYPE: getCaloriesType(data); break; case ZeTimeConstants.CMD_SWITCH_SETTINGS: getDisplayOnMovement(data); break; case ZeTimeConstants.CMD_REMINDERS: storeActualReminders(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; case ZeTimeConstants.CMD_CALL_CONTROL: handleCallControl(data); break; } return true; } } else { LOG.warn("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 void 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}); } private void 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}); } private void 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}); } 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); TransactionBuilder builder = new TransactionBuilder("setDeviceInitialized"); builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); try { performConnected(builder.getTransaction()); } catch (IOException e) { LOG.error("could not set device to initzialized: ", e); } LOG.info("Initialization Done"); } private void handleActivityFetching(byte[] msg) { availableStepsData = (msg[5] & 0xff) | (msg[6] << 8) & 0xff00; availableSleepData = (msg[7] & 0xff) | (msg[8] << 8) & 0xff00; availableHeartRateData = (msg[9] & 0xff) | (msg[10] << 8) & 0xff00; if (availableStepsData > 0) { getStepData(); } else if (availableHeartRateData > 0) { getHeartRateData(); } else if (availableSleepData > 0) { getSleepData(); } else { GB.signalActivityDataFinish(getDevice()); } } 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 deleteStepData() { try { TransactionBuilder builder = performInitialized("deleteStepData"); sendMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_DELETE_STEP_COUNT, ZeTimeConstants.CMD_SEND, 0x01, 0x00, 0x00, ZeTimeConstants.CMD_END}); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error deleting activity data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void getHeartRateData() { try { TransactionBuilder builder = performInitialized("fetchHeartRateData"); 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 heart rate data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void deleteHeartRateData() { try { TransactionBuilder builder = performInitialized("deleteHeartRateData"); sendMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_DELETE_HEARTRATE_DATA, ZeTimeConstants.CMD_SEND, 0x01, 0x00, 0x00, ZeTimeConstants.CMD_END}); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error deleting heart rate data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void getSleepData() { try { TransactionBuilder builder = performInitialized("fetchSleepData"); 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 sleep data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void deleteSleepData() { try { TransactionBuilder builder = performInitialized("deleteSleepData"); sendMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_DELETE_SLEEP_DATA, ZeTimeConstants.CMD_SEND, 0x01, 0x00, 0x00, ZeTimeConstants.CMD_END}); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error deleting sleep data: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void handleStepsData(byte[] msg) { ZeTimeActivitySample sample = new ZeTimeActivitySample(); Calendar now = GregorianCalendar.getInstance(); int timestamp = (msg[10] << 24) & 0xff000000 | (msg[9] << 16) & 0xff0000 | (msg[8] << 8) & 0xff00 | (msg[7] & 0xff); timestamp += eightHourOffset; // the timestamp from the watch has an offset of eight hours, do not know why... timestamp -= ((now.get(Calendar.ZONE_OFFSET) / 1000) + (now.get(Calendar.DST_OFFSET) / 1000)); // TimeZone hour + daylight saving 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.ACTIVITY.getCode()); 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, progressSteps * 100 / availableStepsData, getContext()); if (progressSteps == availableStepsData) { Prefs prefs = getDevicePrefs(); progressSteps = 0; availableStepsData = 0; GB.updateTransferNotification(null, "", false, 100, getContext()); if (getDevice().isBusy()) { getDevice().unsetBusyTask(); getDevice().sendDeviceUpdateIntent(getContext()); } if (!prefs.getBoolean(ZeTimeConstants.PREF_ZETIME_DONT_DEL_ACTDATA, false)) { deleteStepData(); } if (availableHeartRateData > 0) { getHeartRateData(); } else if (availableSleepData > 0) { getSleepData(); } else { GB.signalActivityDataFinish(getDevice()); } } } private void handleSleepData(byte[] msg) { ZeTimeActivitySample sample = new ZeTimeActivitySample(); Calendar now = GregorianCalendar.getInstance(); int timestamp = (msg[10] << 24) & 0xff000000 | (msg[9] << 16) & 0xff0000 | (msg[8] << 8) & 0xff00 | (msg[7] & 0xff); timestamp += eightHourOffset; // the timestamp from the watch has an offset of eight hours, do not know why... timestamp -= ((now.get(Calendar.ZONE_OFFSET) / 1000) + (now.get(Calendar.DST_OFFSET) / 1000)); // TimeZone hour + daylight saving sample.setTimestamp(timestamp); if (msg[11] == 0) { sample.setRawKind(ActivityKind.DEEP_SLEEP.getCode()); } else if (msg[11] == 1) { sample.setRawKind(ActivityKind.LIGHT_SLEEP.getCode()); } else { sample.setRawKind(ActivityKind.UNKNOWN.getCode()); } 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, progressSleep * 100 / availableSleepData, getContext()); if (progressSleep == availableSleepData) { Prefs prefs = getDevicePrefs(); progressSleep = 0; availableSleepData = 0; GB.updateTransferNotification(null, "", false, 100, getContext()); if (getDevice().isBusy()) { getDevice().unsetBusyTask(); GB.signalActivityDataFinish(getDevice()); } if (!prefs.getBoolean(ZeTimeConstants.PREF_ZETIME_DONT_DEL_ACTDATA, false)) { deleteSleepData(); } } } private void handleHeartRateData(byte[] msg) { ZeTimeActivitySample sample = new ZeTimeActivitySample(); Calendar now = GregorianCalendar.getInstance(); int timestamp = (msg[10] << 24) & 0xff000000 | (msg[9] << 16) & 0xff0000 | (msg[8] << 8) & 0xff00 | (msg[7] & 0xff); timestamp += eightHourOffset; // the timestamp from the watch has an offset of eight hours, do not know why... timestamp -= ((now.get(Calendar.ZONE_OFFSET) / 1000) + (now.get(Calendar.DST_OFFSET) / 1000)); // TimeZone hour + daylight saving 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, progressHeartRate * 100 / availableHeartRateData, getContext()); if (((msg[4] << 8) & 0xff00 | (msg[3] & 0xff)) == 0xe) // if the message is longer than 0x7, than it has two measurements (payload = 0xe) { timestamp = (msg[17] << 24) & 0xff000000 | (msg[16] << 16) & 0xff0000 | (msg[15] << 8) & 0xff00 | (msg[14] & 0xff); timestamp += eightHourOffset; // the timestamp from the watch has an offset of eight hours, do not know why... timestamp -= ((now.get(Calendar.ZONE_OFFSET) / 1000) + (now.get(Calendar.DST_OFFSET) / 1000)); // TimeZone hour + daylight saving sample.setHeartRate(msg[18]); 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[12] & 0xff) | ((msg[13] << 8) & 0xff00); GB.updateTransferNotification(null, getContext().getString(R.string.busy_task_fetch_activity_data), true, (int) (progressHeartRate * 100 / availableHeartRateData), getContext()); } if (progressHeartRate == availableHeartRateData) { Prefs prefs = getDevicePrefs(); progressHeartRate = 0; availableHeartRateData = 0; GB.updateTransferNotification(null, "", false, 100, getContext()); if (getDevice().isBusy()) { getDevice().unsetBusyTask(); getDevice().sendDeviceUpdateIntent(getContext()); } if (!prefs.getBoolean(ZeTimeConstants.PREF_ZETIME_DONT_DEL_ACTDATA, false)) { deleteHeartRateData(); } if (availableSleepData > 0) { getSleepData(); } else { GB.signalActivityDataFinish(getDevice()); } } } 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 += (byte) 10; } } else { musicCmd.event = GBDeviceEventMusicControl.Event.VOLUMEDOWN; if (volume > 10) { volume -= (byte) 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 handleCallControl(byte[] callControlMsg) { if (callControlMsg.length == 7 && callControlMsg[2] == ZeTimeConstants.CMD_SEND && callControlMsg[3] == 0x01 && callControlMsg[4] == 0x00 && callControlMsg[5] == 0x01) { callCmd.event = GBDeviceEventCallControl.Event.REJECT; evaluateGBDeviceEvent(callCmd); } } private void replyMsgToWatch(TransactionBuilder builder, byte[] msg) { if (msg.length > maxMsgLength) { int msgpartlength = 0; byte[] msgpart; 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); } // function serving the settings private void setWrist(TransactionBuilder builder) { String value = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"); byte[] wrist = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_USAGE_HABITS, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, ZeTimeConstants.WEAR_ON_LEFT_WRIST, ZeTimeConstants.CMD_END}; if ("right".equals(value)) { wrist[5] = ZeTimeConstants.WEAR_ON_RIGHT_WRIST; } LOG.warn("Wrist: " + wrist[5]); sendMsgToWatch(builder, wrist); } private void setScreenTime(TransactionBuilder builder) { int value = getDevicePrefs().getInt(ZeTimeConstants.PREF_SCREENTIME, 30); if (value > ZeTimeConstants.MAX_SCREEN_ON_TIME) { GB.toast(getContext(), "Value for screen on time is greater than 18h! ", Toast.LENGTH_LONG, GB.ERROR); value = ZeTimeConstants.MAX_SCREEN_ON_TIME; } else if (value < ZeTimeConstants.MIN_SCREEN_ON_TIME) { GB.toast(getContext(), "Value for screen on time is lesser than 10s! ", Toast.LENGTH_LONG, GB.ERROR); value = ZeTimeConstants.MIN_SCREEN_ON_TIME; } byte[] screentime = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_DISPLAY_TIMEOUT, ZeTimeConstants.CMD_SEND, (byte) 0x2, (byte) 0x0, (byte) (value & 0xff), (byte) (value >> 8), ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, screentime); } private void setUserInfo(TransactionBuilder builder) { ActivityUser activityUser = new ActivityUser(); byte gender = (byte) activityUser.getGender(); int age = activityUser.getAge(); int height = activityUser.getHeightCm(); int weight = activityUser.getWeightKg() * 10; // weight is set and get in 100g granularity if (gender == ActivityUser.GENDER_MALE) // translate gender for zetime { gender = 0; } else if (gender == ActivityUser.GENDER_FEMALE) { gender = 1; } else { gender = 2; } byte[] userinfo = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_USER_INFO, ZeTimeConstants.CMD_SEND, (byte) 0x5, (byte) 0x0, gender, (byte) age, (byte) height, (byte) (weight & 0xff), (byte) (weight >> 8), ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, userinfo); } private void setUserGoals(TransactionBuilder builder) { ActivityUser activityUser = new ActivityUser(); int steps = activityUser.getStepsGoal() / 100; // ZeTime expect the steps in 100 increment int calories = activityUser.getCaloriesBurntGoal(); int distance = activityUser.getDistanceGoalMeters() / 1000; // ZeTime only accepts km goals int sleep = activityUser.getSleepDurationGoal(); int activeTime = activityUser.getActiveTimeGoalMinutes(); // set steps goal byte[] goal_steps = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_GOALS, ZeTimeConstants.CMD_SEND, (byte) 0x4, (byte) 0x0, (byte) 0x0, (byte) (steps & 0xff), (byte) (steps >> 8), (byte) 0x1, ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, goal_steps); byte[] goal_calories = new byte[goal_steps.length]; System.arraycopy(goal_steps, 0, goal_calories, 0, goal_steps.length); // set calories goal goal_calories[5] = (byte) 0x1; goal_calories[6] = (byte) (calories & 0xff); goal_calories[7] = (byte) (calories >> 8); sendMsgToWatch(builder, goal_calories); byte[] goal_distance = new byte[goal_steps.length]; System.arraycopy(goal_steps, 0, goal_distance, 0, goal_steps.length); // set distance goal goal_distance[5] = (byte) 0x2; goal_distance[6] = (byte) (distance & 0xff); goal_distance[7] = (byte) (distance >> 8); sendMsgToWatch(builder, goal_distance); byte[] goal_sleep = new byte[goal_steps.length]; System.arraycopy(goal_steps, 0, goal_sleep, 0, goal_steps.length); // set sleep goal goal_sleep[5] = (byte) 0x3; goal_sleep[6] = (byte) (sleep & 0xff); goal_sleep[7] = (byte) (sleep >> 8); sendMsgToWatch(builder, goal_sleep); byte[] goal_activeTime = new byte[goal_steps.length]; System.arraycopy(goal_steps, 0, goal_activeTime, 0, goal_steps.length); // set active time goal goal_activeTime[5] = (byte) 0x4; goal_activeTime[6] = (byte) (activeTime & 0xff); goal_activeTime[7] = (byte) (activeTime >> 8); sendMsgToWatch(builder, goal_activeTime); } private void setHeartRateLimits(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); boolean alarmEnabled = prefs.getBoolean(ZeTimeConstants.PREF_ZETIME_HEARTRATE_ALARM, false); int maxHR = prefs.getInt(ZeTimeConstants.PREF_ZETIME_MAX_HEARTRATE, 180); int minHR = prefs.getInt(ZeTimeConstants.PREF_ZETIME_MIN_HEARTRATE, 60); byte[] heartrateAlarm = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_HEARTRATE_ALARM_LIMITS, ZeTimeConstants.CMD_SEND, (byte) 0x3, (byte) 0x0, (byte) (maxHR & 0xff), (byte) (minHR & 0xff), (byte) (alarmEnabled ? 1 : 0), // activate alarm ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, heartrateAlarm); } private void initMusicVolume(TransactionBuilder builder) { replyMsgToWatch(builder, new byte[]{ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_MUSIC_CONTROL, ZeTimeConstants.CMD_REQUEST_RESPOND, 0x02, 0x00, 0x02, volume, ZeTimeConstants.CMD_END}); } private void setAnalogMode(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); int mode = prefs.getInt(ZeTimeConstants.PREF_ANALOG_MODE, 0); byte[] analog = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_ANALOG_MODE, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) mode, ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, analog); } private void setActivityTracking(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); boolean tracking = prefs.getBoolean(ZeTimeConstants.PREF_ACTIVITY_TRACKING, false); byte[] activity = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_CONTROL_DEVICE, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) 0x9, ZeTimeConstants.CMD_END}; if (tracking) { activity[5] = (byte) 0xa; } sendMsgToWatch(builder, activity); } private void setDisplayOnMovement(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); boolean movement = prefs.getBoolean(ZeTimeConstants.PREF_HANDMOVE_DISPLAY, false); byte[] handmove = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_SWITCH_SETTINGS, ZeTimeConstants.CMD_SEND, (byte) 0x3, (byte) 0x0, (byte) 0x1, (byte) 0xe, (byte) 0x0, ZeTimeConstants.CMD_END}; if (movement) { handmove[7] = (byte) 0x1; } sendMsgToWatch(builder, handmove); } private void setDoNotDisturb(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); String scheduled = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO, "off"); String dndScheduled = getContext().getString(R.string.p_scheduled); String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START, "22:00"); String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END, "07:00"); DateFormat df_start = new SimpleDateFormat("HH:mm"); DateFormat df_end = new SimpleDateFormat("HH:mm"); Calendar calendar = GregorianCalendar.getInstance(); Calendar calendar_end = GregorianCalendar.getInstance(); try { calendar.setTime(df_start.parse(start)); try { calendar_end.setTime(df_end.parse(end)); byte[] doNotDisturb = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_DO_NOT_DISTURB, ZeTimeConstants.CMD_SEND, (byte) 0x5, (byte) 0x0, (byte) 0x0, (byte) calendar.get(Calendar.HOUR_OF_DAY), (byte) calendar.get(Calendar.MINUTE), (byte) calendar_end.get(Calendar.HOUR_OF_DAY), (byte) calendar_end.get(Calendar.MINUTE), ZeTimeConstants.CMD_END}; if (scheduled.equals(dndScheduled)) { doNotDisturb[5] = (byte) 0x1; } sendMsgToWatch(builder, doNotDisturb); } catch (Exception e) { LOG.error("Unexpected exception in ZeTimeDeviceSupport.setDoNotDisturb: " + e.getMessage()); } } catch (Exception e) { LOG.error("Unexpected exception in ZeTimeDeviceSupport.setDoNotDisturb: " + e.getMessage()); } } private void setCaloriesType(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); int type = prefs.getInt(ZeTimeConstants.PREF_CALORIES_TYPE, 0); byte[] calories = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_CALORIES_TYPE, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) type, ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, calories); } private void setTimeFormate(TransactionBuilder builder) { String timeFormat = getDevicePrefs().getTimeFormat(); int type = 1; if ("am/pm".equals(timeFormat)) { type = 2; } byte[] timeformat = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_TIME_SURFACE_SETTINGS, ZeTimeConstants.CMD_SEND, (byte) 0x8, (byte) 0x0, (byte) 0xff, // set to ff to not change anything on the watch (byte) type, (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, timeformat); } private void setDateFormate(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); int type = prefs.getInt(ZeTimeConstants.PREF_DATE_FORMAT, 0); byte[] dateformat = {ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_TIME_SURFACE_SETTINGS, ZeTimeConstants.CMD_SEND, (byte) 0x8, (byte) 0x0, (byte) type, (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch (byte) 0xff, // set to ff to not change anything on the watch ZeTimeConstants.CMD_END}; sendMsgToWatch(builder, dateformat); } private void setInactivityAlert(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); boolean enabled = prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE, false); int threshold = prefs.getInt(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, 60); if (threshold > 0xff) { threshold = 0xff; GB.toast(getContext(), "Value for inactivity threshold is greater than 255min! ", Toast.LENGTH_LONG, GB.ERROR); } byte[] inactivity = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_INACTIVITY_ALERT, ZeTimeConstants.CMD_SEND, (byte) 0x8, (byte) 0x0, (byte) 0x0, (byte) threshold, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x0, (byte) 0x64, (byte) 0x0, ZeTimeConstants.CMD_END }; if (enabled) { String start = prefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, "06:00"); String end = prefs.getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, "22:00"); DateFormat df_start = new SimpleDateFormat("HH:mm"); DateFormat df_end = new SimpleDateFormat("HH:mm"); Calendar calendar = GregorianCalendar.getInstance(); Calendar calendar_end = GregorianCalendar.getInstance(); int reps = (1 << 7); // set inactivity active: set bit 7 reps |= (prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO, false) ? 1 : 0); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU, false) ? 1 : 0) << 1); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE, false) ? 1 : 0) << 2); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH, false) ? 1 : 0) << 3); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR, false) ? 1 : 0) << 4); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA, false) ? 1 : 0) << 5); reps |= ((prefs.getBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU, false) ? 1 : 0) << 6); inactivity[5] = (byte) reps; try { calendar.setTime(df_start.parse(start)); try { calendar_end.setTime(df_end.parse(end)); inactivity[7] = (byte) calendar.get(Calendar.HOUR_OF_DAY); inactivity[8] = (byte) calendar.get(Calendar.MINUTE); inactivity[9] = (byte) calendar_end.get(Calendar.HOUR_OF_DAY); inactivity[10] = (byte) calendar_end.get(Calendar.MINUTE); } catch (Exception e) { LOG.error("Unexpected exception in ZeTimeDeviceSupport.setInactivityAlert: " + e.getMessage()); } } catch (Exception e) { LOG.error("Unexpected exception in ZeTimeDeviceSupport.setInactivityAlert: " + e.getMessage()); } } sendMsgToWatch(builder, inactivity); } private void setShockStrength(TransactionBuilder builder) { Prefs prefs = getDevicePrefs(); int shockStrength = prefs.getInt(ZeTimeConstants.PREF_SHOCK_STRENGTH, 255); byte[] strength = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_SHOCK_STRENGTH, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) shockStrength, ZeTimeConstants.CMD_END }; sendMsgToWatch(builder, strength); } private void setSignaling(final TransactionBuilder builder, final String signalingType) { final Prefs prefs = getDevicePrefs(); final byte typeByte; final int signalTypeDefault; switch (signalingType) { case ZeTimeConstants.PREF_SMS_SIGNALING: typeByte = ZeTimeConstants.SMS_TYPE; signalTypeDefault = 2; break; case ZeTimeConstants.PREF_CALL_SIGNALING: typeByte = ZeTimeConstants.CALL_TYPE; signalTypeDefault = 13; break; case ZeTimeConstants.PREF_MISSED_CALL_SIGNALING: typeByte = ZeTimeConstants.MISSED_CALL_TYPE; signalTypeDefault = 12; break; case ZeTimeConstants.PREF_EMAIL_SIGNALING: typeByte = ZeTimeConstants.EMAIL_TYPE; signalTypeDefault = 12; break; case ZeTimeConstants.PREF_SOCIAL_SIGNALING: typeByte = ZeTimeConstants.SOCIAL_TYPE; signalTypeDefault = 12; break; case ZeTimeConstants.PREF_CALENDAR_SIGNALING: typeByte = ZeTimeConstants.CALENDAR_TYPE; signalTypeDefault = 12; break; case ZeTimeConstants.PREF_INACTIVITY_SIGNALING: typeByte = ZeTimeConstants.INACTIVITY_TYPE; signalTypeDefault = 12; break; case ZeTimeConstants.PREF_LOW_POWER_SIGNALING: typeByte = ZeTimeConstants.LOW_POWER_TYPE; signalTypeDefault = 4; break; case ZeTimeConstants.PREF_ANTI_LOSS_SIGNALING: typeByte = ZeTimeConstants.ANTI_LOSS_TYPE; signalTypeDefault = 13; break; default: LOG.error("Unknown signaling type {}", signalingType); return; } final int signalType = prefs.getInt(signalingType, signalTypeDefault); final byte[] signaling = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_SHOCK_MODE, ZeTimeConstants.CMD_SEND, (byte) 0x2, (byte) 0x0, typeByte, (byte) signalType, ZeTimeConstants.CMD_END }; sendMsgToWatch(builder, signaling); } @Override public void onReadConfiguration(String config) { try { TransactionBuilder builder = performInitialized("readConfiguration"); if (!getDevice().isBusy()) { getDevice().setBusyTask("readConfiguration"); } byte[] configRead1 = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_TIME_SURFACE_SETTINGS, ZeTimeConstants.CMD_REQUEST, (byte) 0x1, (byte) 0x0, (byte) 0x0, ZeTimeConstants.CMD_END }; sendMsgToWatch(builder, configRead1); byte[] configRead2 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead2, 0, configRead1.length); configRead2[1] = ZeTimeConstants.CMD_SHOCK_MODE; sendMsgToWatch(builder, configRead2); byte[] configRead3 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead3, 0, configRead1.length); configRead3[1] = ZeTimeConstants.CMD_DO_NOT_DISTURB; sendMsgToWatch(builder, configRead3); byte[] configRead4 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead4, 0, configRead1.length); configRead4[1] = ZeTimeConstants.CMD_ANALOG_MODE; sendMsgToWatch(builder, configRead4); byte[] configRead5 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead5, 0, configRead1.length); configRead5[1] = ZeTimeConstants.CMD_CONTROL_DEVICE; sendMsgToWatch(builder, configRead5); byte[] configRead6 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead6, 0, configRead1.length); configRead6[1] = ZeTimeConstants.CMD_DISPLAY_TIMEOUT; sendMsgToWatch(builder, configRead6); byte[] configRead7 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead7, 0, configRead1.length); configRead7[1] = ZeTimeConstants.CMD_USAGE_HABITS; sendMsgToWatch(builder, configRead7); byte[] configRead8 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead8, 0, configRead1.length); configRead8[1] = ZeTimeConstants.CMD_AUTO_HEARTRATE; sendMsgToWatch(builder, configRead8); byte[] configRead9 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead9, 0, configRead1.length); configRead9[1] = ZeTimeConstants.CMD_HEARTRATE_ALARM_LIMITS; sendMsgToWatch(builder, configRead9); byte[] configRead10 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead10, 0, configRead1.length); configRead10[1] = ZeTimeConstants.CMD_INACTIVITY_ALERT; sendMsgToWatch(builder, configRead10); byte[] configRead11 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead11, 0, configRead1.length); configRead11[1] = ZeTimeConstants.CMD_CALORIES_TYPE; sendMsgToWatch(builder, configRead11); byte[] configRead12 = new byte[configRead1.length]; System.arraycopy(configRead1, 0, configRead12, 0, configRead1.length); configRead12[1] = ZeTimeConstants.CMD_SWITCH_SETTINGS; sendMsgToWatch(builder, configRead12); builder.queue(getQueue()); } catch (IOException e) { GB.toast(getContext(), "Error reading configuration: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); } } private void onReadReminders(TransactionBuilder builder) { // try { // TransactionBuilder builder = performInitialized("readReminders"); byte[] reminders = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_REMINDERS, ZeTimeConstants.CMD_REQUEST, (byte) 0x1, (byte) 0x0, (byte) 0x0, ZeTimeConstants.CMD_END }; sendMsgToWatch(builder, reminders); //builder.queue(getQueue()); // } catch (IOException e) { // GB.toast(getContext(), "Error reading reminders: " + e.getLocalizedMessage(), Toast.LENGTH_LONG, GB.ERROR); // } } private void getDateTimeFormat(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putString(ZeTimeConstants.PREF_DATE_FORMAT, Integer.toString(msg[5])); String timeFormat = "24h"; if (msg[6] == 2) { timeFormat = "am/pm"; } prefs.putString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, timeFormat); prefs.apply(); } private void getSignaling(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putString(ZeTimeConstants.PREF_ANTI_LOSS_SIGNALING, Integer.toString(msg[5])); prefs.putString(ZeTimeConstants.PREF_CALL_SIGNALING, Integer.toString(msg[7])); prefs.putString(ZeTimeConstants.PREF_MISSED_CALL_SIGNALING, Integer.toString(msg[8])); prefs.putString(ZeTimeConstants.PREF_SMS_SIGNALING, Integer.toString(msg[9])); prefs.putString(ZeTimeConstants.PREF_SOCIAL_SIGNALING, Integer.toString(msg[10])); prefs.putString(ZeTimeConstants.PREF_EMAIL_SIGNALING, Integer.toString(msg[11])); prefs.putString(ZeTimeConstants.PREF_CALENDAR_SIGNALING, Integer.toString(msg[12])); prefs.putString(ZeTimeConstants.PREF_INACTIVITY_SIGNALING, Integer.toString(msg[13])); prefs.putString(ZeTimeConstants.PREF_LOW_POWER_SIGNALING, Integer.toString(msg[14])); prefs.apply(); } private void getDoNotDisturb(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); String starttime = String.format("%02d:%02d", msg[6], msg[7]); String endtime = String.format("%02d:%02d", msg[8], msg[9]); if (0x1 == msg[5]) { prefs.putString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO, "scheduled"); } else { prefs.putString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO, "off"); } prefs.putString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_START, starttime); prefs.putString(DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO_END, endtime); prefs.apply(); } private void getAnalogMode(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putString(ZeTimeConstants.PREF_ANALOG_MODE, Integer.toString(msg[5])); prefs.apply(); } private void getActivityTracking(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putBoolean(ZeTimeConstants.PREF_ACTIVITY_TRACKING, 0x1 != msg[6]); prefs.apply(); } private void getScreenTime(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putString(ZeTimeConstants.PREF_SCREENTIME, Integer.toString((msg[5] | (msg[6] << 8)))); prefs.apply(); } private void getWrist(byte[] msg) { SharedPreferences.Editor prefs = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).edit(); if (ZeTimeConstants.WEAR_ON_LEFT_WRIST == msg[5]) { prefs.putString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "left"); } else if (ZeTimeConstants.WEAR_ON_RIGHT_WRIST == msg[5]) { prefs.putString(DeviceSettingsPreferenceConst.PREF_WEARLOCATION, "right"); } prefs.apply(); } private void getHeartRateMeasurement(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.putString(ZeTimeConstants.PREF_ZETIME_HEARTRATE_INTERVAL, Integer.toString((msg[5] * 60))); // multiply with 60 because of the conversion from minutes to seconds prefs.apply(); } private void getHeartRateLimits(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); prefs.apply(); } private void getInactivityAlert(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); String starttime = String.format("%02d:%02d", msg[7], msg[8]); String endtime = String.format("%02d:%02d", msg[9], msg[10]); prefs.putString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, Integer.toString(msg[6])); if (0 != msg[5]) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE, true); prefs.putString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_START, starttime); prefs.putString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_END, endtime); if (0 != (msg[5] & (1 << 0))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_MO, false); } if (0 != (msg[5] & (1 << 1))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TU, false); } if (0 != (msg[5] & (1 << 2))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_WE, false); } if (0 != (msg[5] & (1 << 3))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_TH, false); } if (0 != (msg[5] & (1 << 4))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_FR, false); } if (0 != (msg[5] & (1 << 5))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SA, false); } if (0 != (msg[5] & (1 << 6))) { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU, true); } else { prefs.putBoolean(DeviceSettingsPreferenceConst.PREF_INACTIVITY_SU, false); } } prefs.apply(); } private void getCaloriesType(byte[] msg) { SharedPreferences prefs = getDevicePrefs().getPreferences(); SharedPreferences.Editor myedit = prefs.edit(); myedit.putString(ZeTimeConstants.PREF_CALORIES_TYPE, Integer.toString(msg[5])); myedit.apply(); } private void getDisplayOnMovement(byte[] msg) { SharedPreferences.Editor prefs = getDevicePrefs().getPreferences().edit(); if (0 != (msg[6] & (1 << 6))) { prefs.putBoolean(ZeTimeConstants.PREF_HANDMOVE_DISPLAY, true); } else { prefs.putBoolean(ZeTimeConstants.PREF_HANDMOVE_DISPLAY, false); } prefs.apply(); if (getDevice().isBusy()) { getDevice().unsetBusyTask(); getDevice().sendDeviceUpdateIntent(getContext()); } } private void storeActualReminders(byte[] msg) { if (msg[3] == 0xb) // there is a reminder on the watch { System.arraycopy(msg, 6, remindersOnWatch[msg[5] - 1], 0, 10); } } private void setLanguage(TransactionBuilder builder) { String localeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, "auto"); byte[] languageMsg = { ZeTimeConstants.CMD_PREAMBLE, ZeTimeConstants.CMD_LANGUAGE_SETTINGS, ZeTimeConstants.CMD_SEND, (byte) 0x1, (byte) 0x0, (byte) 0x0, ZeTimeConstants.CMD_END }; if (localeString == null || localeString.equals("auto")) { String language = Locale.getDefault().getLanguage(); String country = Locale.getDefault().getCountry(); if (country == null) { // sometimes country is null, no idea why, guess it. country = language; } localeString = language + "_" + country.toUpperCase(); } switch (localeString.substring(0, 2)) { case "zh": if (localeString.equals("zh_CN")) { languageMsg[5] = 1; } else { languageMsg[5] = 2; } break; case "ko": languageMsg[5] = 3; break; case "th": languageMsg[5] = 4; break; case "ja": languageMsg[5] = 5; break; case "fr": languageMsg[5] = 6; break; case "es": languageMsg[5] = 7; break; case "de": languageMsg[5] = 8; break; case "it": languageMsg[5] = 9; break; case "pl": languageMsg[5] = 10; break; case "pt": languageMsg[5] = 11; break; case "ru": languageMsg[5] = 12; break; case "nl": languageMsg[5] = 13; break; case "ar": languageMsg[5] = 14; break; case "el": languageMsg[5] = 15; break; case "he": languageMsg[5] = 16; break; case "sv": languageMsg[5] = 17; break; case "cs": languageMsg[5] = 18; break; case "en": default: languageMsg[5] = 0; } sendMsgToWatch(builder, languageMsg); } @Override public boolean getImplicitCallbackModify() { return true; } @Override public boolean getSendWriteRequestResponse() { return false; } }