/* Copyright (C) 2019-2024 Andreas Böhler, Andreas Shimokawa, Arjan Schrijver, Damien Gaignon, mamucho, mkusnierz, Taavi Eomäe 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.lenovo.watchxplus; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCharacteristic; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Handler; import android.widget.Toast; import androidx.annotation.IntRange; import androidx.localbroadcastmanager.content.LocalBroadcastManager; 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.Date; import java.util.GregorianCalendar; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.TimeZone; import java.util.UUID; import nodomain.freeyourgadget.gadgetbridge.GBApplication; import nodomain.freeyourgadget.gadgetbridge.R; import nodomain.freeyourgadget.gadgetbridge.activities.SettingsActivity; 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.GBDeviceEventFindPhone; import nodomain.freeyourgadget.gadgetbridge.deviceevents.GBDeviceEventVersionInfo; import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.DataType; import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusConstants; import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusDeviceCoordinator; import nodomain.freeyourgadget.gadgetbridge.devices.lenovo.watchxplus.WatchXPlusSampleProvider; import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusActivitySample; import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusHealthActivityOverlay; import nodomain.freeyourgadget.gadgetbridge.entities.WatchXPlusHealthActivityOverlayDao; import nodomain.freeyourgadget.gadgetbridge.impl.GBDevice; import nodomain.freeyourgadget.gadgetbridge.model.ActivityKind; import nodomain.freeyourgadget.gadgetbridge.model.ActivitySample; import nodomain.freeyourgadget.gadgetbridge.model.ActivityUser; import nodomain.freeyourgadget.gadgetbridge.model.Alarm; import nodomain.freeyourgadget.gadgetbridge.model.BatteryState; import nodomain.freeyourgadget.gadgetbridge.model.CallSpec; import nodomain.freeyourgadget.gadgetbridge.model.NotificationSpec; import nodomain.freeyourgadget.gadgetbridge.model.WeatherSpec; import nodomain.freeyourgadget.gadgetbridge.service.btle.AbstractBTLEDeviceSupport; import nodomain.freeyourgadget.gadgetbridge.service.btle.BLETypeConversions; 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.devices.lenovo.operations.InitOperation; import nodomain.freeyourgadget.gadgetbridge.util.AlarmUtils; import nodomain.freeyourgadget.gadgetbridge.util.ArrayUtils; import nodomain.freeyourgadget.gadgetbridge.util.BcdUtil; import nodomain.freeyourgadget.gadgetbridge.util.GB; import nodomain.freeyourgadget.gadgetbridge.util.StringUtils; public class WatchXPlusDeviceSupport extends AbstractBTLEDeviceSupport { private boolean needsAuth; private int sequenceNumber = 0; private boolean isCalibrationActive = false; private final Map dataToFetch = new LinkedHashMap<>(); private int requestedDataTimestamp; private int dataSlots = 0; private DataType currentDataType; private byte ACK_CALIBRATION = 0; private final GBDeviceEventVersionInfo versionInfo = new GBDeviceEventVersionInfo(); private final GBDeviceEventBatteryInfo batteryInfo = new GBDeviceEventBatteryInfo(); private static final Logger LOG = LoggerFactory.getLogger(WatchXPlusDeviceSupport.class); private final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { String broadcastAction = intent.getAction(); assert broadcastAction != null; switch (broadcastAction) { case WatchXPlusConstants.ACTION_CALIBRATION: enableCalibration(intent.getBooleanExtra(WatchXPlusConstants.ACTION_ENABLE, false)); break; case WatchXPlusConstants.ACTION_CALIBRATION_SEND: int hour = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_HOUR, -1); int minute = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_MINUTE, -1); int second = intent.getIntExtra(WatchXPlusConstants.VALUE_CALIBRATION_SECOND, -1); if (hour != -1 && minute != -1 && second != -1) { sendCalibrationData(hour, minute, second); } break; case WatchXPlusConstants.ACTION_CALIBRATION_HOLD: holdCalibration(); break; } } }; public WatchXPlusDeviceSupport() { super(LOG); addSupportedService(GattService.UUID_SERVICE_GENERIC_ACCESS); addSupportedService(GattService.UUID_SERVICE_GENERIC_ATTRIBUTE); addSupportedService(WatchXPlusConstants.UUID_SERVICE_WATCHXPLUS); LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); IntentFilter intentFilter = new IntentFilter(); intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION); intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_SEND); intentFilter.addAction(WatchXPlusConstants.ACTION_CALIBRATION_HOLD); broadcastManager.registerReceiver(broadcastReceiver, intentFilter); } @Override protected TransactionBuilder initializeDevice(TransactionBuilder builder) { try { boolean auth = needsAuth; needsAuth = false; new InitOperation(auth, this, builder).perform(); } catch (IOException e) { e.printStackTrace(); } return builder; } @Override public boolean connectFirstTime() { needsAuth = true; return super.connect(); } @Override public boolean useAutoConnect() { return true; } @Override public void onNotification(NotificationSpec notificationSpec) { String senderOrTitle = StringUtils.getFirstOf(notificationSpec.sender, notificationSpec.title); String message = StringUtils.truncate(senderOrTitle, 14) + "\0"; if (notificationSpec.subject != null) { message += StringUtils.truncate(notificationSpec.subject, 20) + ": "; } if (notificationSpec.body != null) { message += StringUtils.truncate(notificationSpec.body, 64); } sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, message); } /** Cancel notification * cancel watch notification - stop vibration and turn off screen * on watch - clear phone icon near bluetooth */ private void cancelNotification() { try { if (getQueue() == null) { LOG.warn("Unable to cancel notification, queue is null"); return; } getQueue().clear(); TransactionBuilder builder = performInitialized("cancelNotification"); int mPosition = 1024; // all positions int mMessageId = 0xFF; // all messages byte[] bArr = new byte[6]; bArr[0] = (byte) ((int) (mPosition >> 24)); bArr[1] = (byte) ((int) (mPosition >> 16)); bArr[2] = (byte) ((int) (mPosition >> 8)); bArr[3] = (byte) ((int) mPosition); bArr[4] = (byte) (mMessageId >> 8); bArr[5] = (byte) mMessageId; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_NOTIFICATION_CANCEL, WatchXPlusConstants.WRITE_VALUE, bArr)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to cancel notification ", e); } } private String transliterate(String inputText) { String outText = ""; String returnText = ""; for (int messageIndex = 0; messageIndex < inputText.length(); messageIndex++) { String checkLetter = inputText.substring(messageIndex, messageIndex + 1); returnText = checkLetter; switch (checkLetter) { case "а": returnText = "А"; break; case "б": returnText = "Б"; break; case "в": returnText = "В"; break; case "г": returnText = "Г"; break; case "д": returnText = "Д"; break; case "е": returnText = "Е"; break; case "ж": returnText = "Ж"; break; case "з": returnText = "З"; break; case "и": returnText = "И"; break; case "й": returnText = "Й"; break; case "к": returnText = "К"; break; case "л": returnText = "Л"; break; case "м": returnText = "М"; break; case "н": returnText = "Н"; break; case "о": returnText = "О"; break; case "п": returnText = "П"; break; case "Р": returnText = "р"; break; case "С": returnText = "с"; break; case "Т": returnText = "т"; break; case "У": returnText = "у"; break; case "Ф": returnText = "ф"; break; case "Х": returnText = "х"; break; case "Ц": returnText = "ц"; break; case "Ч": returnText = "ч"; break; case "Ш": returnText = "ш"; break; case "Щ": returnText = "щ"; break; case "Ъ": returnText = "ъ"; break; case "Ь": returnText = "ь"; break; case "Ю": returnText = "ю"; break; case "Я": returnText = "я"; break; case "Ы": returnText = "ы"; break; case "Э": returnText = "э"; break; } outText = outText + returnText; } return outText; } /** Format text and send it to watch * @param notificationChannel - text or call * @param notificationText - text to show */ private void sendNotification(int notificationChannel, String notificationText) { try { TransactionBuilder builder = performInitialized("showNotification"); byte[] command = WatchXPlusConstants.CMD_NOTIFICATION_TEXT_TASK; //byte[] text = notificationText.getBytes(StandardCharsets.UTF_8); byte[] text = transliterate(notificationText).getBytes(StandardCharsets.UTF_8); byte[] messagePart; int messageLength = text.length; int parts = messageLength / 9; int remainder = messageLength % 9; // Increment parts quantity if message length is not multiple of 9 if (remainder != 0) { parts++; } for (int messageIndex = 0; messageIndex < parts; messageIndex++) { if (messageIndex + 1 != parts || remainder == 0) { messagePart = new byte[11]; } else { messagePart = new byte[remainder + 2]; } System.arraycopy(text, messageIndex * 9, messagePart, 2, messagePart.length - 2); if (messageIndex + 1 == parts) { messageIndex = 0xFF; } messagePart[0] = (byte) notificationChannel; messagePart[1] = (byte) messageIndex; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.KEEP_ALIVE, messagePart)); } builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to send notification ", e); } } /** enable notification channels on watch * @param builder * enable all notification channels * TODO add settings to choose notification channels */ private WatchXPlusDeviceSupport enableNotificationChannels(TransactionBuilder builder) { builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_NOTIFICATION_SETTINGS, WatchXPlusConstants.WRITE_VALUE, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF})); return this; } public void authorizationRequest(TransactionBuilder builder, boolean firstConnect) { builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_AUTHORIZATION_TASK, WatchXPlusConstants.TASK, new byte[]{(byte) (firstConnect ? 0x00 : 0x01)})); //possibly not the correct meaning } private void enableCalibration(boolean enable) { try { TransactionBuilder builder = performInitialized("enableCalibration"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_CALIBRATION_INIT_TASK, WatchXPlusConstants.TASK, new byte[]{(byte) (enable ? 0x01 : 0x00)})); performImmediately(builder); } catch (IOException e) { LOG.warn(" Unable to start/stop calibration mode ", e); } } private void holdCalibration() { try { TransactionBuilder builder = performInitialized("holdCalibration"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_CALIBRATION_KEEP_ALIVE, WatchXPlusConstants.KEEP_ALIVE)); performImmediately(builder); } catch (IOException e) { LOG.warn(" Unable to keep calibration mode alive ", e); } } private void sendCalibrationData(@IntRange(from = 0, to = 23) int hour, @IntRange(from = 0, to = 59) int minute, @IntRange(from = 0, to = 59) int second) { try { isCalibrationActive = true; TransactionBuilder builder = performInitialized("calibrate"); int handsPosition = ((hour % 12) * 60 + minute) * 60 + second; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_CALIBRATION_TASK, WatchXPlusConstants.TASK, Conversion.toByteArr16(handsPosition))); performImmediately(builder); } catch (IOException e) { isCalibrationActive = false; LOG.warn(" Unable to send calibration data ", e); } } private WatchXPlusDeviceSupport checkInitTime(TransactionBuilder builder) { LOG.info(" Check init time "); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS, WatchXPlusConstants.READ_VALUE)); return this; } private void getTime() { try { TransactionBuilder builder = performInitialized("getTime"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS, WatchXPlusConstants.READ_VALUE)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to get device time ", e); } } private void handleTime(byte[] time) { GregorianCalendar now = BLETypeConversions.createCalendar(); GregorianCalendar nowDevice = BLETypeConversions.createCalendar(); int year = (nowDevice.get(Calendar.YEAR) / 100) * 100 + BcdUtil.fromBcd8(time[8]); nowDevice.set(year, BcdUtil.fromBcd8(time[9]) - 1, BcdUtil.fromBcd8(time[10]), BcdUtil.fromBcd8(time[11]), BcdUtil.fromBcd8(time[12]), BcdUtil.fromBcd8(time[13])); nowDevice.set(Calendar.DAY_OF_WEEK, BcdUtil.fromBcd8(time[16]) + 1); long timeDiff = (Math.abs(now.getTimeInMillis() - nowDevice.getTimeInMillis())) / 1000; LOG.info(" Time diff: " + timeDiff); if (10 < timeDiff && timeDiff < 120) { LOG.info(" Auto set time "); enableCalibration(true); setTime(BLETypeConversions.createCalendar()); enableCalibration(false); } else if (timeDiff > 120) { LOG.info(" Time diff is too big "); GB.toast("Manual time calibration needed!", Toast.LENGTH_LONG, GB.WARN); sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, "Calibrate time"); boolean forceTime = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_FORCE_TIME, false); if (forceTime) { LOG.info(" Force set time "); enableCalibration(true); setTime(BLETypeConversions.createCalendar()); enableCalibration(false); GB.toast("Check analog time!", Toast.LENGTH_LONG, GB.WARN); sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_DEFAULT, "Check analog time"); } } } // set only digital time private void setTime(Calendar calendar) { try { TransactionBuilder builder = performInitialized("setTime"); int timezoneOffsetMinutes = (calendar.get(Calendar.ZONE_OFFSET) + calendar.get(Calendar.DST_OFFSET)) / (60 * 1000); int timezoneOffsetIndustrialMinutes = Math.round((Math.abs(timezoneOffsetMinutes) % 60) * 100f / 60f); byte[] time = new byte[]{BcdUtil.toBcd8(calendar.get(Calendar.YEAR) % 100), BcdUtil.toBcd8(calendar.get(Calendar.MONTH) + 1), BcdUtil.toBcd8(calendar.get(Calendar.DAY_OF_MONTH)), BcdUtil.toBcd8(calendar.get(Calendar.HOUR_OF_DAY)), BcdUtil.toBcd8(calendar.get(Calendar.MINUTE)), BcdUtil.toBcd8(calendar.get(Calendar.SECOND)), (byte) (timezoneOffsetMinutes / 60), (byte) timezoneOffsetIndustrialMinutes, (byte) (calendar.get(Calendar.DAY_OF_WEEK) - 1) }; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_TIME_SETTINGS, WatchXPlusConstants.WRITE_VALUE, time)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to set time ", e); } } /** send command to request watch firmware version * @param builder - transaction builder */ private WatchXPlusDeviceSupport getFirmwareVersion(TransactionBuilder builder) { builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_FIRMWARE_INFO, WatchXPlusConstants.READ_VALUE)); return this; } /** send command to request watch battery state * @param builder - transaction builder */ private WatchXPlusDeviceSupport getBatteryState(TransactionBuilder builder) { builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO, WatchXPlusConstants.READ_VALUE)); return this; } /** initialize device on connect * @param builder - transaction builder */ public WatchXPlusDeviceSupport initialize(TransactionBuilder builder) { getFirmwareVersion(builder) .getBatteryState(builder) .enableNotificationChannels(builder) .setFitnessGoal(builder) // set steps per day .getBloodPressureCalibrationStatus(builder) // request blood pressure calibration //.setUnitsSettings() // set metric/imperial units .checkInitTime(builder) .syncPreferences(builder); // read preferences from app and set them to watch builder.add(new SetDeviceStateAction(getDevice(), GBDevice.State.INITIALIZED, getContext())); builder.setCallback(this); return this; } @Override public void onDeleteNotification(int id) { isMissedCall = false; cancelNotification(); } @Override public void onSetTime() { LOG.info(" Get time "); getTime(); } @Override public void onSetAlarms(ArrayList alarms) { try { TransactionBuilder builder = performInitialized("setAlarms"); for (Alarm alarm : alarms) { setAlarm(alarm, alarm.getPosition() + 1, builder); } builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to set alarms ", e); } } // No useful use case at the moment, used to clear alarm slots for testing. private void deleteAlarm(TransactionBuilder builder, int index) { if (0 < index && index < 4) { byte[] alarmValue = new byte[]{(byte) index, 0x00, 0x00, 0x00, 0x00, 0x00}; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS, WatchXPlusConstants.WRITE_VALUE, alarmValue)); } } private void setAlarm(Alarm alarm, int index, TransactionBuilder builder) { // Shift the GB internal repetition mask to match the device specific one. byte repetitionMask = (byte) ((alarm.getRepetition() << 1) | (alarm.isRepetitive() ? 0x80 : 0x00)); repetitionMask |= (alarm.getRepetition(Alarm.ALARM_SUN) ? 0x01 : 0x00); if (0 < index && index < 4) { byte[] alarmValue = new byte[]{(byte) index, BcdUtil.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.HOUR_OF_DAY)), BcdUtil.toBcd8(AlarmUtils.toCalendar(alarm).get(Calendar.MINUTE)), repetitionMask, (byte) (alarm.getEnabled() ? 0x01 : 0x00), 0x00 // TODO: Unknown }; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_ALARM_SETTINGS, WatchXPlusConstants.WRITE_VALUE, alarmValue)); } } private boolean isRinging = false; // store ringing state private boolean isMissedCall = false; // missed call state private int remainingRepeats = 0; // initialize call notification reminds private int remainingMissedRepeats = 0; // initialize missed call notification reminds /** send notification on watch when phone rings * @param callSpec - phone state * send notification on incoming call, cancel notification when call is answered, ignored or rejected * send missed call notification (if enabled from settings) when phone state changed from ringing to end call * TODO add missed call reminder (send notification to watch at desired period) */ // variables to handle ring notifications @Override public void onSetCallState(final CallSpec callSpec) { final int repeatDelay = 5000; // repeat delay of 5 sec (watch show call notifications for about 5 sec.) final int repeatMissedDelay = 60000; // repeat missed call delay of 60 sec // set settings for missed call //int repeatCount = WatchXPlusDeviceCoordinator.getRepeatOnCall(); final boolean continuousRing = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_CONTINIOUS_RING, false); int repeatCount = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(WatchXPlusConstants.PREF_REPEAT_RING, 0); final boolean enableMissedCall = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_MISSED_CALL_ENABLE, false); int repeatCountMissed = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getInt(WatchXPlusConstants.PREF_MISSED_CALL_REPEAT, 0); switch (callSpec.command) { case CallSpec.CALL_INCOMING: isRinging = true; remainingRepeats = repeatCount; LOG.info(" Incoming call "); if (("Phone".equals(callSpec.name)) || (callSpec.name.contains("ropusn")) || (callSpec.name.contains("issed"))) { // do nothing for notifications without caller name, e.g. system call event } else { // possible missed call isMissedCall = true; // send first notification sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name); // init repeat handler final Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { // Actions to do after repeatDelay seconds if (((isRinging) && (remainingRepeats > 0)) || ((isRinging) && (continuousRing))) { remainingRepeats = remainingRepeats - 1; sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, callSpec.name); // re-run handler handler.postDelayed(this, repeatDelay); } else { remainingRepeats = 0; // stop handler handler.removeCallbacks(this); cancelNotification(); } } }, repeatDelay); } break; case CallSpec.CALL_START: isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Call start "); break; case CallSpec.CALL_REJECT: isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Call reject "); break; case CallSpec.CALL_ACCEPT: isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Call accept "); break; case CallSpec.CALL_OUTGOING: isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Outgoing call "); break; case CallSpec.CALL_END: LOG.info(" End call "); isRinging = false; // it's a missed call, don't clear notification to preserve small icon near bluetooth if (isMissedCall) { remainingMissedRepeats = repeatCountMissed; // send missed call notification if enabled in settings if (enableMissedCall) { LOG.info(" Missed call reminder "); sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, "Missed call"); // repeat missed call notification final Handler handler = new Handler(); handler.postDelayed(new Runnable() { public void run() { // Actions to do after repeatDelay seconds if ((isMissedCall) && (remainingMissedRepeats > 0)) { remainingMissedRepeats = remainingMissedRepeats - 1; sendNotification(WatchXPlusConstants.NOTIFICATION_CHANNEL_PHONE_CALL, "Missed call"); LOG.info(" Missed call reminder repeats to go: " + remainingMissedRepeats); // re-run handler handler.postDelayed(this, repeatMissedDelay); } else { remainingMissedRepeats = 0; LOG.info(" Missed call reminder repeats to go: " + remainingMissedRepeats); isMissedCall = false; // stop handler handler.removeCallbacks(this); cancelNotification(); } } }, repeatMissedDelay); } else { remainingMissedRepeats = 0; isMissedCall = false; cancelNotification(); } } else { isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Outgoing call end "); } break; default: isRinging = false; isMissedCall = false; cancelNotification(); LOG.info(" Call default "); break; } } /** handle button press while ringing */ private void handleButtonWhenRing() { GBDeviceEventCallControl callCmd = new GBDeviceEventCallControl(); // get saved settings if true - reject call, otherwise ignore call boolean buttonReject = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_BUTTON_REJECT, false); if (buttonReject) { LOG.info(" call rejected "); isRinging = false; remainingRepeats = 0; isMissedCall = false; callCmd.event = GBDeviceEventCallControl.Event.REJECT; evaluateGBDeviceEvent(callCmd); cancelNotification(); } else { LOG.info(" call ignored "); isRinging = false; remainingRepeats = 0; isMissedCall = false; callCmd.event = GBDeviceEventCallControl.Event.IGNORE; evaluateGBDeviceEvent(callCmd); cancelNotification(); } } private WatchXPlusDeviceSupport setFitnessGoal(TransactionBuilder builder) { int fitnessGoal = new ActivityUser().getStepsGoal(); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_FITNESS_GOAL_SETTINGS, WatchXPlusConstants.WRITE_VALUE, Conversion.toByteArr16(fitnessGoal))); return this; } /** set personal info - read it from About me * @param builder - transaction builder * @param height - user height in meters * @param weight - user weight in kg * @param age - user age * @param gender - user age */ private void setPersonalInformation(TransactionBuilder builder, int height, int weight, int age, int gender) { LOG.info(" Setting Personal Information... height:"+height+" weight:"+weight+" age:"+age+" gender:"+gender); byte[] command = WatchXPlusConstants.CMD_SET_PERSONAL_INFO; byte[] bArr = new byte[4]; bArr[0] = (byte) height; // byte[08] bArr[1] = (byte) weight; // byte[09] bArr[2] = (byte) age; // byte[10] bArr[3] = (byte) gender; // byte[11] builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.WRITE_VALUE, bArr)); } /** handle get/set personal info * @param value - reply from watch * actual do nothing (for test purposes only) */ private void handlePersonalInfo(byte[] value) { int height = Conversion.fromByteArr16(value[8]); int weight = Conversion.fromByteArr16(value[9]); int age = Conversion.fromByteArr16(value[10]); int gender = Conversion.fromByteArr16(value[11]); LOG.info(" Personal info - height:" + height + ", weight:" + weight + ", age:" + age + ", gender:" + gender); } @Override public void onFetchRecordedData(int dataTypes) { TransactionBuilder builder; // get battery state try { builder = performInitialized("getBatteryInfo"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_BATTERY_INFO, WatchXPlusConstants.READ_VALUE)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to retrieve battery data ", e); } try { builder = performInitialized("fetchData"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_DAY_STEPS_INFO, WatchXPlusConstants.READ_VALUE)); // Fetch heart rate data samples count requestDataCount(DataType.HEART_RATE); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to retrieve recorded data ", e); } } @Override public void onReset(int flags) { // testNewCommands(); } @Override public void onHeartRateTest() { //requestHeartRateMeasurement(); } @Override public void onScreenshotReq() { sendBloodPressureCalibration(); } @Override public void onSendConfiguration(String config) { TransactionBuilder builder; SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs(this.getDevice().getAddress()); LOG.info(" onSendConfiguration: " + config); try { builder = performInitialized("sendConfig: " + config); switch (config) { // settings from App Settings case SettingsActivity.PREF_MEASUREMENT_SYSTEM: setUnitsSettings(); break; case ActivityUser.PREF_USER_STEPS_GOAL: setFitnessGoal(builder); break; // settings from App Settings -> WatchXPlus settings case DeviceSettingsPreferenceConst.PREF_POWER_MODE: setPowerMode(); break; case DeviceSettingsPreferenceConst.PREF_LANGUAGE: setLanguageAndTimeFormat(builder); break; case DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD: case DeviceSettingsPreferenceConst.PREF_INACTIVITY_ENABLE: setLongSitHours(builder); break; // calibrations case DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE: setAltitude(builder); break; case DeviceSettingsPreferenceConst.PREF_BUTTON_BP_CALIBRATE: sendBloodPressureCalibration(); break; // settings from device card case DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED: setHeadsUpScreen(builder); getShakeStatus(builder); break; case DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED: setDisconnectReminder(builder); break; case DeviceSettingsPreferenceConst.PREF_TIMEFORMAT: setLanguageAndTimeFormat(builder); break; case DeviceSettingsPreferenceConst.PREF_DO_NOT_DISTURB_NOAUTO: setDNDHours(builder); break; } builder.queue(getQueue()); } catch (IOException e) { e.printStackTrace(); } } @Override public void onReadConfiguration(String config) { LOG.info(" onReadConfiguration : " + config); } @Override public void onTestNewFunction() { requestBloodPressureMeasurement(); } /** set long sit reminder time * @param builder - transaction builder * @param enable - state (true - enabled or false - disabled) * @param hourStart - begin hour * @param minuteStart - begin minute * @param hourEnd - end hour * @param minuteEnd - end minute */ private void setLongSitHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd, int period) { LOG.info(" Setting Long sit reminder... Enabled:"+enable+" Period:"+period); LOG.info(" Setting Long sit time... Hs:"+hourStart+" Ms:"+minuteStart+" He:"+hourEnd+" Me:"+minuteEnd); // set Long Sit reminder time byte[] command = WatchXPlusConstants.CMD_INACTIVITY_REMINDER_SET; byte[] bArr = new byte[10]; // do not remind bArr[0] = (byte) hourEnd; // byte[08] bArr[1] = (byte) minuteEnd; // byte[09] bArr[2] = (byte) hourStart; // byte[10] bArr[3] = (byte) minuteStart; // byte[11] // remind bArr[4] = (byte) hourStart; // byte[12] bArr[5] = (byte) minuteStart; // byte[13] bArr[6] = (byte) hourEnd; // byte[14] bArr[7] = (byte) minuteEnd; // byte[15] bArr[8] = (byte) (period >> 8); // byte[16] bArr[9] = (byte) period; // byte[17] builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.WRITE_VALUE, bArr)); // set long sit reminder state (enabled, disabled) setLongSitSwitch(builder, enable); } /** get Long sit settings from app, and send it to watch * @param builder - transaction builder */ private void setLongSitHours(TransactionBuilder builder) { Calendar start = new GregorianCalendar(); Calendar end = new GregorianCalendar(); boolean enable = WatchXPlusDeviceCoordinator.getLongSitHours(gbDevice.getAddress(), start, end); if (enable) { String periodString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_INACTIVITY_THRESHOLD, "60"); int period = Integer.parseInt(periodString); this.setLongSitHours(builder, enable, start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE), end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE), period); } else { // disable Long sit reminder LOG.info(" Long sit reminder are disabled "); this.setLongSitSwitch(builder, enable); } } /** set long sit reminder switch * @param tbuilder - transaction builder * @param enable - true or false */ private void setLongSitSwitch(TransactionBuilder tbuilder, boolean enable) { LOG.info(" Setting Long sit reminder switch to: " + enable); tbuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_INACTIVITY_REMINDER_SWITCH, WatchXPlusConstants.WRITE_VALUE, new byte[]{(byte) (enable ? 0x01 : 0x00)})); } /** set do not disturb time * @param builder - transaction builder * @param enable - state (true - enabled or false - disabled) * @param hourStart - begin hour * @param minuteStart - begin minute * @param hourEnd - end hour * @param minuteEnd - end minute */ private void setDNDHours(TransactionBuilder builder, boolean enable, int hourStart, int minuteStart, int hourEnd, int minuteEnd) { LOG.info(" Setting DND time... Hs:"+hourStart+" Ms:"+minuteStart+" He:"+hourEnd+" Me:"+minuteEnd); // set DND time byte[] command = WatchXPlusConstants.CMD_SET_DND_HOURS_TIME; byte[] bArr = new byte[4]; bArr[0] = (byte) hourStart; // byte[08] bArr[1] = (byte) minuteStart; // byte[09] bArr[2] = (byte) hourEnd; // byte[10] bArr[3] = (byte) minuteEnd; // byte[11] builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.WRITE_VALUE, bArr)); // set DND state (enabled, disabled) setDNDHoursSwitch(builder, enable); } /** set do not disturb switch * @param tbuilder - transaction builder * @param enable - true or false */ private void setDNDHoursSwitch(TransactionBuilder tbuilder, boolean enable) { LOG.info(" Setting DND switch to: " + enable); tbuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_SET_DND_HOURS_SWITCH, WatchXPlusConstants.WRITE_VALUE, new byte[]{(byte) (enable ? 0x01 : 0x00)})); } /** get DND settings from app, and send it to watch * @param builder - transaction builder */ private void setDNDHours(TransactionBuilder builder) { Calendar start = new GregorianCalendar(); Calendar end = new GregorianCalendar(); boolean enable = WatchXPlusDeviceCoordinator.getDNDHours(gbDevice.getAddress(), start, end); if (enable) { this.setDNDHours(builder, enable, start.get(Calendar.HOUR_OF_DAY), start.get(Calendar.MINUTE), end.get(Calendar.HOUR_OF_DAY), end.get(Calendar.MINUTE)); } else { // disable DND LOG.info(" Quiet hours are disabled "); this.setDNDHoursSwitch(builder, enable); } } /** set watch power * switch watch power mode * modes (0- normal, 1- energysaving, 2- only watch) */ private void setPowerMode() { byte setWatchPowerMode = 0x00; String powermodeStr = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_POWER_MODE, "0"); if (powermodeStr.equals("0")) { setWatchPowerMode = 0x00; } else if (powermodeStr.equals("1")) { setWatchPowerMode = 0x01; } else if (powermodeStr.equals("2")) { setWatchPowerMode = 0x02; } byte[] bArr = new byte[1]; bArr[0] = (byte) setWatchPowerMode; LOG.info(" setting power mode to: " + setWatchPowerMode); try { TransactionBuilder builder = performInitialized("setWatchPowerMode"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_POWER_MODE, WatchXPlusConstants.TASK, bArr)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to set power mode ", e); } //prefs.getPreferences().edit().putInt("PREF_POWER_MODE", 0).apply(); } /** request watch units * for testing purposes only */ private WatchXPlusDeviceSupport getUnitsSettings() { LOG.info(" Get units from watch... "); try { TransactionBuilder builder = performInitialized("getUnits"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_SET_UNITS, WatchXPlusConstants.READ_VALUE)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to get units ", e); } return this; } /** * Set watch units */ private void setUnitsSettings() { int units = 0; String unitsPref = GBApplication.getPrefs().getString(SettingsActivity.PREF_MEASUREMENT_SYSTEM, GBApplication.getContext().getString(R.string.p_unit_metric)); if (unitsPref.equals(GBApplication.getContext().getString(R.string.p_unit_imperial))) { units = 1; LOG.info(" Changed units: imperial "); } else { LOG.info(" Changed units: metric "); } byte[] bArr = new byte[3]; bArr[0] = (byte) units; // metric - 0/imperial - 1 bArr[1] = (byte) 0x00; // time unit 12/24h (there is a separate command for this) bArr[2] = (byte) 0x00; // temperature unit (do nothing) try { TransactionBuilder builder = performInitialized("setUnits"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_SET_UNITS, WatchXPlusConstants.WRITE_VALUE, bArr)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to set units ", e); } } /** request status of blood pressure calibration * @param builder - transaction builder */ private WatchXPlusDeviceSupport getBloodPressureCalibrationStatus(TransactionBuilder builder) { builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_IS_BP_CALIBRATED, WatchXPlusConstants.READ_VALUE)); return this; } /** send blood pressure calibration to watch * TODO add better error handling if blood pressure calibration is failed */ private void sendBloodPressureCalibration() { try { String beginCalibration = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_BUTTON_BP_CALIBRATE, "0"); if (beginCalibration.equals("1")) { LOG.info(" Calibrating BP - cancel " + beginCalibration); return; } String mLowPString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(WatchXPlusConstants.PREF_BP_CAL_LOW, "80"); int mLowP = Integer.parseInt(mLowPString); String mHighPString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(WatchXPlusConstants.PREF_BP_CAL_HIGH, "130"); int mHighP = Integer.parseInt(mHighPString); LOG.warn(" Calibrating BP ... LowP=" + mLowP + " HighP="+mHighP); GB.toast("Calibrating BP...", Toast.LENGTH_LONG, GB.INFO); TransactionBuilder builder = performInitialized("bpCalibrate"); byte[] command = WatchXPlusConstants.CMD_BP_CALIBRATION; byte mStart = 0x01; // initiate calibration byte[] bArr = new byte[5]; bArr[0] = mStart; // byte[08] bArr[1] = (byte) (mHighP >> 8); // byte[09] bArr[2] = (byte) mHighP; // byte[10] bArr[3] = (byte) (mLowP >> 8); // byte[11] bArr[4] = (byte) mLowP; // byte[12] builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.TASK, bArr)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to send BP Calibration ", e); } } /** handle watch response if blood pressure is calibrated * @param value - watch response * save result to global variable (uses for BP measurement) */ private void handleBloodPressureCalibrationStatus(byte[] value) { WatchXPlusDeviceCoordinator.isBPCalibrated = Conversion.fromByteArr16(value[8]) == 0; } /** handle watch response for result of blood pressure calibration * @param value - watch response */ private void handleBloodPressureCalibrationResult(byte[] value) { if (Conversion.fromByteArr16(value[8]) != 0x00) { WatchXPlusDeviceCoordinator.isBPCalibrated = false; GB.toast(" Calibrating BP fail ", Toast.LENGTH_LONG, GB.ERROR); } else { WatchXPlusDeviceCoordinator.isBPCalibrated = true; int high = Conversion.fromByteArr16(value[9], value[10]); int low = Conversion.fromByteArr16(value[11], value[12]); GB.toast("OK. Measured Low:"+low+" high:"+high, Toast.LENGTH_LONG, GB.INFO); } } /** request blood pressure measurement * first check if blood pressure is calibrated */ private void requestBloodPressureMeasurement() { if (!WatchXPlusDeviceCoordinator.isBPCalibrated) { LOG.info(" BP is NOT calibrated "); GB.toast("BP is not calibrated", Toast.LENGTH_LONG, GB.WARN); return; } try { TransactionBuilder builder = performInitialized("bpMeasure"); byte[] command = WatchXPlusConstants.CMD_BLOOD_PRESSURE_MEASURE; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.TASK, new byte[]{0x01})); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to request BP Measure ", e); } } @Override public void onSendWeather(ArrayList weatherSpecs) { WeatherSpec weatherSpec = weatherSpecs.get(0); try { TransactionBuilder builder = performInitialized("setWeather"); int currentTemp; int todayMinTemp; int todayMaxTemp; byte[] command = WatchXPlusConstants.CMD_WEATHER_SET; byte[] weatherInfo = new byte[5]; int currentCondition = weatherSpec.currentConditionCode; // set weather icon int currentConditionCode = 0; // 0 is sunny switch (currentCondition) { //Group 2xx: Thunderstorm case 200: //thunderstorm with light rain: //11d case 201: //thunderstorm with rain: //11d case 202: //thunderstorm with heavy rain: //11d currentConditionCode = 1024; break; case 210: //light thunderstorm:: //11d case 211: //thunderstorm: //11d case 212: //heavy thunderstorm: //11d case 221: //ragged thunderstorm: //11d case 230: //thunderstorm with light drizzle: //11d case 231: //thunderstorm with drizzle: //11d case 232: //thunderstorm with heavy drizzle: //11d currentConditionCode = 1025; break; //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 500: //light rain: //10d currentConditionCode = 256; break; 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 case 501: //moderate rain: //10d currentConditionCode = 1280; break; //Group 5xx: Rain case 511: //freezing rain: //13d case 520: //light intensity shower rain: //09d case 521: //shower rain: //09d case 502: //heavy intensity rain: //10d case 503: //very heavy rain: //10d case 504: //extreme rain: //10d case 522: //heavy intensity shower rain: //09d case 531: //ragged shower rain: //09d currentConditionCode = 258; break; //Group 6xx: Snow case 600: //light snow: case 601: //snow: //[[file:13d.png]] currentConditionCode = 513; break; case 620: //light shower snow: //[[file:13d.png]] currentConditionCode = 514; break; case 602: //heavy snow: //[[file:13d.png]] case 621: //shower snow: //[[file:13d.png]] case 622: //heavy shower snow: //[[file:13d.png]] currentConditionCode = 515; break; case 611: //sleet: //[[file:13d.png]] case 612: //shower sleet: //[[file:13d.png]] currentConditionCode = 1026; break; case 615: //light rain and snow: //[[file:13d.png]] case 616: //rain and snow: //[[file:13d.png]] currentConditionCode = 4; break; //Group 7xx: Atmosphere case 741: //fog: //[[file:50d.png]] case 701: //mist: //[[file:50d.png]] case 711: //smoke: //[[file:50d.png]] currentConditionCode = 5; break; case 721: //haze: //[[file:50d.png]] currentConditionCode = 3; break; case 731: //sandcase dust whirls: //[[file:50d.png]] currentConditionCode = 771; break; case 751: //sand: //[[file:50d.png]] case 761: //dust: //[[file:50d.png]] case 762: //volcanic ash: //[[file:50d.png]] case 771: //squalls: //[[file:50d.png]] currentConditionCode = 769; break; case 781: //tornado: //[[file:50d.png]] case 900: //tornado currentConditionCode = 1283; break; //Group 800: Clear case 800: //clear sky currentConditionCode = 0; break; //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]] currentConditionCode = 1; break; case 804: //overcast clouds: //[[file:04d.png]] [[file:04d.png]] currentConditionCode = 2; break; //Group 90x: Extreme case 901: //tropical storm case 903: //cold case 904: //hot case 905: //windy case 906: //hail currentConditionCode = 1027; break; //Group 9xx: Additional 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 case 960: //storm case 961: //violent storm case 902: //hurricane case 962: //hurricane currentConditionCode = 261; break; } LOG.info( " Weather cond: " + currentCondition + " icon: " + currentConditionCode); // calculate for temps under 0 currentTemp = (Math.abs(weatherSpec.currentTemp)) - 273; if (currentTemp < 0) { currentTemp = (Math.abs(currentTemp) ^ 255) + 1; } todayMinTemp = (Math.abs(weatherSpec.todayMinTemp)) - 273; if (todayMinTemp < 0) { todayMinTemp = (Math.abs(todayMinTemp) ^ 255) + 1; } todayMaxTemp = (Math.abs(weatherSpec.todayMaxTemp)) - 273; if (todayMaxTemp < 0) { todayMaxTemp = (Math.abs(todayMaxTemp) ^ 255) + 1; } LOG.info(" Set weather min: " + todayMinTemp + " max: " + todayMaxTemp + " current: " + currentTemp + " icon: " + currentCondition); // First two bytes are controlling the icon weatherInfo[0] = (byte )(currentConditionCode >> 8); weatherInfo[1] = (byte )currentConditionCode; weatherInfo[2] = (byte) todayMinTemp; weatherInfo[3] = (byte) todayMaxTemp; weatherInfo[4] = (byte) currentTemp; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.KEEP_ALIVE, weatherInfo)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to set weather ", e); } } @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); UUID characteristicUUID = characteristic.getUuid(); byte[] value = characteristic.getValue(); if (WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE.equals(characteristicUUID)) { if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_FIRMWARE_INFO, 5)) { handleFirmwareInfo(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_SHAKE_SWITCH, 5)) { handleShakeState(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_SET_PERSONAL_INFO, 5)) { handlePersonalInfo(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_WHILE_RING, 5)) { handleButtonWhenRing(); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DISCONNECT_REMIND, 5)) { handleDisconnectReminderState(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BATTERY_INFO, 5)) { handleBatteryState(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_GOAL_AIM_STATUS, 5)) { handleSportAimStatus(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_TIME_SETTINGS, 5)) { handleTime(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_IS_BP_CALIBRATED, 5)) { handleBloodPressureCalibrationStatus(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BP_CALIBRATION, 5)) { handleBloodPressureCalibrationResult(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BUTTON_INDICATOR, 5)) { this.onReverseFindDevice(true); // It looks like WatchXPlus doesn't send this action // WRONG: WatchXPlus send this on find phone LOG.info(" Unhandled action: Button pressed "); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_ALARM_INDICATOR, 5)) { LOG.info(" Alarm active: id=" + value[8]); } else if (isCalibrationActive && value.length == 7 && value[4] == ACK_CALIBRATION) { setTime(BLETypeConversions.createCalendar()); isCalibrationActive = false; } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DAY_STEPS_INDICATOR, 5)) { handleStepsInfo(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_COUNT, 5)) { LOG.info(" Received data count: " + value); handleDataCount(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_DETAILS, 5)) { LOG.info(" Received data details: " + value); handleDataDetails(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT, 5)) { LOG.info(" Received data content: " + value); handleDataContentAck(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_BP_MEASURE_STARTED, 5)) { handleBpMeasureResult(value); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_DATA_CONTENT_REMOVE, 5)) { handleDataContentRemove(value); } else if (value.length == 7 && value[5] == 0) { LOG.info(" Received ACK "); // Not sure if that's necessary. There is no response for ACK in original app logs // handleAck(); } else if (ArrayUtils.equals(value, WatchXPlusConstants.RESP_NOTIFICATION_SETTINGS, 5)) { LOG.info(" Received notification settings status "); } else { LOG.info(" Unhandled value change for characteristic: " + characteristicUUID); logMessageContent(characteristic.getValue()); } return true; } else if (WatchXPlusConstants.UUID_CHARACTERISTIC_DATABASE_READ.equals(characteristicUUID)) { LOG.info(" Value change for characteristic DATABASE: " + characteristicUUID + " value " + Arrays.toString(value)); handleContentDataChunk(value); return true; } else { LOG.info(" Unhandled characteristic changed: " + characteristicUUID + " value " + Arrays.toString(value)); logMessageContent(characteristic.getValue()); } return false; } private void handleDataContentRemove(byte[] value) { int dataType = Conversion.fromByteArr16(value[8], value[9]); int timestamp = Conversion.fromByteArr16(value[10], value[11], value[12], value[13]); int removed = value[14]; DataType type = DataType.getType(dataType); if( removed == 0) { LOG.info(" Removed " + type + " data for timestamp " + timestamp); } else { LOG.info(" Unsuccessful removal of " + type + " data for timestamp " + timestamp); } } /** * Heart rate history retrieve flow: * 1. Request for heart rate data slots count. CMD_RETRIEVE_DATA_COUNT, {@link WatchXPlusDeviceSupport#requestDataCount} * 2. Extract data count from response. RESP_DATA_COUNT, {@link WatchXPlusDeviceSupport#handleDataCount} * 3. Request for N data slot details. CMD_RETRIEVE_DATA_DETAILS, {@link WatchXPlusDeviceSupport#requestDataDetails} * 4. Timestamp of slot is returned, save it for later use. RESP_DATA_DETAILS, {@link WatchXPlusDeviceSupport#handleDataDetails} * 5. Repeat step 3-4 until all slots details retrieved. * 6. Request for M data content by timestamp. CMD_RETRIEVE_DATA_CONTENT, {@link WatchXPlusDeviceSupport#requestDataContentForTimestamp} * 7. Receive kind of pre-flight response. RESP_DATA_CONTENT, {@link WatchXPlusDeviceSupport#handleDataContentAck} * 8. Receive frames with content. They are different than other frames, {@link WatchXPlusDeviceSupport#handleContentDataChunk} * ie. 0000000255-4F4C48-434241434444454648474747, 0001000247-474645-434240FFFFFFFFFFFFFFFFFF */ private void requestDataCount(DataType dataType) { TransactionBuilder builder; try { builder = performInitialized("requestDataCount"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_COUNT, WatchXPlusConstants.READ_VALUE, dataType.getValue())); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to send request to retrieve recorded data ", e); } } private void handleDataCount(byte[] value) { int dataType = Conversion.fromByteArr16(value[8], value[9]); int dataCount = Conversion.fromByteArr16(value[10], value[11]); DataType type = DataType.getType(dataType); LOG.info(" Watch contains " + dataCount + " " + type + " entries"); dataSlots = dataCount; dataToFetch.clear(); if (dataCount != 0) { requestDataDetails(dataToFetch.size(), type); } } private void requestDataDetails(int i, DataType dataType) { LOG.info(" Requesting " + dataType + " details"); try { TransactionBuilder builder = performInitialized("requestDataDetails"); byte[] index = Conversion.toByteArr16(i); byte[] req = BLETypeConversions.join(dataType.getValue(), index); currentDataType = dataType; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_RETRIEVE_DATA_DETAILS, WatchXPlusConstants.READ_VALUE, req)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn( "Unable to request data details ", e); } } private void handleDataDetails(byte[] value) { LOG.info(" Got data details "); int timestamp = Conversion.fromByteArr16(value[8], value[9], value[10], value[11]); int dataLength = Conversion.fromByteArr16(value[12], value[13]); int samplingInterval = (int) onSamplingInterval(value[14] >> 4, Conversion.fromByteArr16((byte) (value[14] & 15), value[15])); int mtu = Conversion.fromByteArr16(value[16]); int parts = dataLength / 16; if (dataLength % 16 > 0) { parts++; } LOG.info(" timestamp (UTC): " + timestamp); LOG.info(" timestamp (UTC): " + new Date((long) timestamp * 1000)); LOG.info(" dataLength (data length): " + dataLength); LOG.info(" samplingInterval (per time): " + samplingInterval); LOG.info(" mtu (mtu): " + mtu); LOG.info(" parts: " + parts); dataToFetch.put(timestamp, parts); if (dataToFetch.size() == dataSlots) { Map.Entry currentValue = dataToFetch.entrySet().iterator().next(); requestedDataTimestamp = currentValue.getKey(); requestDataContentForTimestamp(requestedDataTimestamp, currentDataType); } else { requestDataDetails(dataToFetch.size(), currentDataType); } } private void requestDataContentForTimestamp(int timestamp, DataType dataType) { byte[] command = WatchXPlusConstants.CMD_RETRIEVE_DATA_CONTENT; try { TransactionBuilder builder = performInitialized("requestDataContentForTimestamp"); byte[] ts = Conversion.toByteArr32(timestamp); byte[] req = BLETypeConversions.join(dataType.getValue(), ts); req = BLETypeConversions.join(req, Conversion.toByteArr16(0)); requestedDataTimestamp = timestamp; builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.READ_VALUE, req)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to request data content ", e); } } private void removeDataContentForTimestamp(int timestamp, DataType dataType) { byte[] command = WatchXPlusConstants.CMD_REMOVE_DATA_CONTENT; try { TransactionBuilder builder = performInitialized("removeDataContentForTimestamp"); byte[] ts = Conversion.toByteArr32(timestamp); byte[] req = BLETypeConversions.join(dataType.getValue(), ts); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(command, WatchXPlusConstants.TASK, req)); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to remove data content ", e); } } private void handleDataContentAck(byte[] value) { LOG.info(" Received data content start "); // To verify: Chunks are sent if value[8] == 0, if value[8] == 1 they are not sent by watch } private void handleContentDataChunk(byte[] value) { int chunkNo = Conversion.fromByteArr16(value[0], value[1]); int dataType = Conversion.fromByteArr16(value[2], value[3]); int timezoneOffset = TimeZone.getDefault().getOffset(System.currentTimeMillis())/1000; DataType type = DataType.getType(dataType); try (DBHandler dbHandler = GBApplication.acquireDB()) { WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(getDevice(), dbHandler.getDaoSession()); List samples = new ArrayList<>(); if (DataType.SLEEP.equals(type)) { WatchXPlusHealthActivityOverlayDao overlayDao = dbHandler.getDaoSession().getWatchXPlusHealthActivityOverlayDao(); List overlayList = new ArrayList<>(); for (int i = 4; i < value.length; i+= 2) { int val = Conversion.fromByteArr16(value[i], value[i+1]); if (65535 == val) { break; } int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) / 2) + ((i - 4) / 2)) *5) * 60) - timezoneOffset; LOG.debug(" SLEEP requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", rawIntensity: " + val); WatchXPlusActivitySample sample = createSample(dbHandler, tsWithOffset); sample.setTimestamp(tsWithOffset); sample.setProvider(provider); sample.setRawIntensity(val); sample.setRawKind((val == 0 ? ActivityKind.DEEP_SLEEP : ActivityKind.LIGHT_SLEEP).getCode()); samples.add(sample); overlayList.add(new WatchXPlusHealthActivityOverlay(sample.getTimestamp(), sample.getTimestamp()+300, sample.getRawKind(), sample.getDeviceId(), sample.getUserId(), sample.getRawWatchXPlusHealthData())); } overlayDao.insertOrReplaceInTx(overlayList); provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0])); handleEndOfDataChunks(chunkNo, type); } else if (DataType.HEART_RATE.equals(type)) { for (int i = 4; i < value.length; i++) { int val = Conversion.fromByteArr16(value[i]); if (255 == val) { break; } int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) + i) - 4) * 2) * 60) - timezoneOffset; LOG.debug(" HEART RATE requested timestamp " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", value: " + val); WatchXPlusActivitySample sample = createSample(dbHandler, tsWithOffset); sample.setTimestamp(tsWithOffset); sample.setHeartRate(val); sample.setProvider(provider); sample.setRawKind(ActivityKind.ACTIVITY.getCode()); samples.add(sample); } provider.addGBActivitySamples(samples.toArray(new WatchXPlusActivitySample[0])); handleEndOfDataChunks(chunkNo, type); } else { LOG.warn(" Got unsupported data package type: " + type); for (int i = 4; i < value.length; i++) { int val = Conversion.fromByteArr16(value[i], value[i+1]); if (65535 == val) { break; } int tsWithOffset = requestedDataTimestamp + (((((chunkNo * 16) / 2) + ((i - 4) / 2)) *5) * 60) - timezoneOffset; LOG.debug(" UNSUPPORTED requested timestamp for type: " + type + " " + requestedDataTimestamp + " chunkNo " + chunkNo + " Got data: " + new Date((long) tsWithOffset * 1000) + ", rawIntensity: " + val); } } } catch (Exception ex) { LOG.warn((ex.getMessage())); } } private void handleEndOfDataChunks(int chunkNo, DataType type) { if(!dataToFetch.isEmpty() && chunkNo == dataToFetch.get(requestedDataTimestamp) - 1) { dataToFetch.remove(requestedDataTimestamp); removeDataContentForTimestamp(requestedDataTimestamp, currentDataType); if (!dataToFetch.isEmpty()) { Map.Entry currentValue = dataToFetch.entrySet().iterator().next(); requestedDataTimestamp = currentValue.getKey(); requestDataContentForTimestamp(requestedDataTimestamp, type); } else { dataSlots = 0; if(type.equals(DataType.HEART_RATE)) { currentDataType = DataType.SLEEP; requestDataCount(currentDataType); } } } else if (dataToFetch.isEmpty()) { dataSlots = 0; if(type.equals(DataType.HEART_RATE)) { currentDataType = DataType.SLEEP; requestDataCount(currentDataType); } } } private void handleBpMeasureResult(byte[] value) { if (value.length < 11) { LOG.info(" BP Measure started. Waiting for result "); GB.toast("BP Measure started. Waiting for result...", Toast.LENGTH_LONG, GB.INFO); } else { LOG.info(" Received BP live data "); int high = Conversion.fromByteArr16(value[8], value[9]); int low = Conversion.fromByteArr16(value[10], value[11]); int timestamp = Conversion.fromByteArr16(value[12], value[13], value[14], value[15]); GB.toast("Calculated BP data: low: " + low + ", high: " + high, Toast.LENGTH_LONG, GB.INFO); LOG.info(" Calculated BP data: timestamp: " + timestamp + ", high: " + high + ", low: " + low); } } private void handleAck() { try { TransactionBuilder builder = performInitialized("handleAck"); builder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand()); builder.queue(getQueue()); } catch (IOException e) { LOG.warn(" Unable to response to ACK ", e); } } // This is only for ACK response private byte[] buildCommand() { byte[] result = new byte[7]; System.arraycopy(WatchXPlusConstants.CMD_HEADER, 0, result, 0, 5); result[2] = (byte) (result.length - 6); result[3] = WatchXPlusConstants.REQUEST; result[4] = (byte) sequenceNumber++; result[5] = (byte) 0; result[result.length - 1] = calculateChecksum(result); return result; } /** handle watch response for steps goal (show steps setting) * @param value - watch reply * for test purposes only */ private void handleSportAimStatus(byte[] value) { int stepsAim = Conversion.fromByteArr16(value[8], value[9]); LOG.info(" Received goal stepsAim: " + stepsAim); } private void handleStepsInfo(byte[] value) { int steps = Conversion.fromByteArr16(value[8], value[9]); LOG.info(" Received steps count: " + steps); // This code is from MakibesHR3DeviceSupport Calendar date = GregorianCalendar.getInstance(); int timestamp = (int) (date.getTimeInMillis() / 1000); // We need to subtract the day's total step count thus far. int dayStepCount = this.getStepsOnDay(timestamp); int newSteps = (steps - dayStepCount); if (newSteps > 0) { LOG.info("adding " + newSteps + " steps"); try (DBHandler dbHandler = GBApplication.acquireDB()) { WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(getDevice(), dbHandler.getDaoSession()); WatchXPlusActivitySample sample = createSample(dbHandler, timestamp); sample.setTimestamp(timestamp); // sample.setRawKind(record.type); sample.setRawKind(ActivityKind.ACTIVITY.getCode()); sample.setSteps(newSteps); // sample.setDistance(record.distance); // sample.setCalories(record.calories); // sample.setDistance(record.distance); // sample.setHeartRate((record.maxHeartRate - record.minHeartRate) / 2); //TODO: Find an alternative approach for Day Summary Heart Rate // sample.setRawHPlusHealthData(record.getRawData()); sample.setProvider(provider); provider.addGBActivitySample(sample); } catch (Exception ex) { LOG.warn(ex.getMessage()); } } } /** * @param timeStamp Time stamp at some point during the requested day. */ private int getStepsOnDay(int timeStamp) { try (DBHandler dbHandler = GBApplication.acquireDB()) { Calendar dayStart = new GregorianCalendar(); Calendar dayEnd = new GregorianCalendar(); this.getDayStartEnd(timeStamp, dayStart, dayEnd); WatchXPlusSampleProvider provider = new WatchXPlusSampleProvider(this.getDevice(), dbHandler.getDaoSession()); List samples = provider.getAllActivitySamples( (int) (dayStart.getTimeInMillis() / 1000L), (int) (dayEnd.getTimeInMillis() / 1000L)); int totalSteps = 0; for (WatchXPlusActivitySample sample : samples) { totalSteps += sample.getSteps(); } return totalSteps; } catch (Exception ex) { LOG.warn(ex.getMessage()); return 0; } } /** * @param timeStamp seconds */ private void getDayStartEnd(int timeStamp, Calendar start, Calendar end) { final int DAY = (24 * 60 * 60); int timeStampStart = ((timeStamp / DAY) * DAY); int timeStampEnd = (timeStampStart + DAY); start.setTimeInMillis(timeStampStart * 1000L); end.setTimeInMillis(timeStampEnd * 1000L); } private WatchXPlusActivitySample createSample(DBHandler dbHandler, int timestamp) { Long userId = DBHelper.getUser(dbHandler.getDaoSession()).getId(); Long deviceId = DBHelper.getDevice(getDevice(), dbHandler.getDaoSession()).getId(); return new WatchXPlusActivitySample( timestamp, // ts deviceId, userId, // User id null, // Raw Data ActivityKind.UNKNOWN.getCode(), // rawKind ActivitySample.NOT_MEASURED, // rawIntensity ActivitySample.NOT_MEASURED, // Steps ActivitySample.NOT_MEASURED, // HR ActivitySample.NOT_MEASURED, // Distance ActivitySample.NOT_MEASURED // Calories ); } private byte[] buildCommand(byte[] command, byte action) { return buildCommand(command, action, null); } private byte[] buildCommand(byte[] command, byte action, byte[] value) { if (Arrays.equals(command, WatchXPlusConstants.CMD_CALIBRATION_TASK)) { ACK_CALIBRATION = (byte) sequenceNumber; } command = BLETypeConversions.join(command, value); byte[] result = new byte[7 + command.length]; System.arraycopy(WatchXPlusConstants.CMD_HEADER, 0, result, 0, 5); System.arraycopy(command, 0, result, 6, command.length); result[2] = (byte) (command.length + 1); result[3] = WatchXPlusConstants.REQUEST; result[4] = (byte) sequenceNumber++; result[5] = action; result[result.length - 1] = calculateChecksum(result); return result; } private byte calculateChecksum(byte[] bytes) { byte checksum = 0x00; for (int i = 0; i < bytes.length - 1; i++) { checksum += (bytes[i] ^ i) & 0xFF; } return (byte) (checksum & 0xFF); } /** handle watch response for firmware version * @param value - watch response */ private void handleFirmwareInfo(byte[] value) { versionInfo.fwVersion = String.format(Locale.US, "%d.%d.%d", value[8], value[9], value[10]); handleGBDeviceEvent(versionInfo); } /** handle watch response for battery level * @param value - returned value */ private void handleBatteryState(byte[] value) { batteryInfo.state = value[8] == 1 ? BatteryState.BATTERY_NORMAL : BatteryState.BATTERY_LOW; batteryInfo.level = value[9]; handleGBDeviceEvent(batteryInfo); } /** handle watch response for lift wrist, and shake to refuse/ignore call * @param value - watch response * for test purposes only */ private void handleShakeState(byte[] value) { String light = "lightScreen"; if ((value[11] & 1) == 1) { light = light + " on"; } else { light = light + " off"; } String refuse = "refuseCall"; if ((((value[11] & 2) >> 1) & 1) != 1) { //z = false; refuse = refuse + " off"; } else { refuse = refuse + " on"; } LOG.info(" handleShakeState: " + light + " " + refuse); } /** handle disconnect reminder (lost device) status * @param value - watch response * for test purposes only */ private void handleDisconnectReminderState(byte[] value) { boolean z = true; if (1 != value[8]) { z = false; } LOG.info(" disconnectReminder: " + z + " val: " + value[8]); } // read preferences private void syncPreferences(TransactionBuilder transaction) { this.setHeadsUpScreen(transaction); // lift wirst to screen on this.setDNDHours(transaction); // DND this.setDisconnectReminder(transaction); // disconnect reminder this.setLanguageAndTimeFormat(transaction); // set time mode 12/24h this.setAltitude(transaction); // set altitude calibration this.setLongSitHours(transaction); // set Long sit reminder ActivityUser activityUser = new ActivityUser(); this.setPersonalInformation(transaction, activityUser.getHeightCm(), activityUser.getWeightKg(), activityUser.getAge(),activityUser.getGender()); } private final Handler mFindPhoneHandler = new Handler(); private void onReverseFindDevice(boolean start) { if (start) { SharedPreferences sharedPreferences = GBApplication.getDeviceSpecificSharedPrefs( this.getDevice().getAddress()); int findPhone = WatchXPlusDeviceCoordinator.getFindPhone(sharedPreferences); if (findPhone != WatchXPlusDeviceCoordinator.FindPhone_OFF) { GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); findPhoneEvent.event = GBDeviceEventFindPhone.Event.START; evaluateGBDeviceEvent(findPhoneEvent); if (findPhone > 0) { this.mFindPhoneHandler.postDelayed(new Runnable() { @Override public void run() { onReverseFindDevice(false); } }, findPhone * 1000); } } } else { // Always send stop, ignore preferences. GBDeviceEventFindPhone findPhoneEvent = new GBDeviceEventFindPhone(); findPhoneEvent.event = GBDeviceEventFindPhone.Event.STOP; evaluateGBDeviceEvent(findPhoneEvent); } } // Command to toggle Lift Wrist to Light Screen, and shake to ignore/reject call private void setHeadsUpScreen(TransactionBuilder transactionBuilder) { boolean enable = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_LIFTWRIST_NOSHED, false); boolean shakeReject = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(WatchXPlusConstants.PREF_SHAKE_REJECT, false); byte refuseCall = 0x00; // force shake wrist to ignore/reject call to OFF // returned characteristic is equal with button press while ringing if (shakeReject) refuseCall = 0x01; byte lightScreen = 0x00; if (enable) { lightScreen = 0x01; } byte b = (byte) (lightScreen + (refuseCall << 1)); byte[] liftScreen = new byte[4]; liftScreen[0] = 0x00; liftScreen[1] = 0x00; liftScreen[2] = 0x00; liftScreen[3] = b; //byte[11] transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_SHAKE_SWITCH, WatchXPlusConstants.WRITE_VALUE, liftScreen)); } // command to set disconnect reminder private void setDisconnectReminder(TransactionBuilder transactionBuilder) { boolean enable = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getBoolean(DeviceSettingsPreferenceConst.PREF_DISCONNECTNOTIF_NOSHED, false); transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_DISCONNECT_REMIND, WatchXPlusConstants.WRITE_VALUE, new byte[]{(byte) (enable ? 0x01 : 0x00)})); } // Request status of Lift Wrist to Light Screen, and Shake to Ignore/Reject Call private void getShakeStatus(TransactionBuilder transactionBuilder) { transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_SHAKE_SWITCH, WatchXPlusConstants.READ_VALUE)); } // calibrate altitude private void setAltitude(TransactionBuilder transactionBuilder) { String altitudeString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_ALTITUDE_CALIBRATE, "200"); int mAltitude = Integer.parseInt(altitudeString); if (mAltitude < 0) { mAltitude = (Math.abs(mAltitude) ^ 65535) + 1; } int mAirPressure = Math.abs(0); // air pressure 0 ??? byte[] bArr = new byte[4]; bArr[0] = (byte) (mAltitude >> 8); // bytr[8] bArr[1] = (byte) mAltitude; // bytr[9] bArr[2] = (byte) (mAirPressure >> 8); // bytr[10] bArr[3] = (byte) mAirPressure; // bytr[11] transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_ALTITUDE, WatchXPlusConstants.WRITE_VALUE, bArr)); LOG.info(" setAltitude: " + mAltitude); } // set time format private void setLanguageAndTimeFormat(TransactionBuilder transactionBuilder) { byte setLanguage, setTimeMode; String languageString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_LANGUAGE, "1"); if (languageString == null || languageString.equals("1")) { setLanguage = 0x01; } else { setLanguage = 0x00; } String timeformatString = GBApplication.getDeviceSpecificSharedPrefs(gbDevice.getAddress()).getString(DeviceSettingsPreferenceConst.PREF_TIMEFORMAT, "1"); assert timeformatString != null; if (timeformatString.equals(getContext().getString(R.string.p_timeformat_24h))) { setTimeMode = WatchXPlusConstants.ARG_SET_TIMEMODE_24H; } else { setTimeMode = WatchXPlusConstants.ARG_SET_TIMEMODE_12H; } byte[] bArr = new byte[2]; bArr[0] = setLanguage; //byte[08] language bArr[1] = setTimeMode; //byte[09] time transactionBuilder.write(getCharacteristic(WatchXPlusConstants.UUID_CHARACTERISTIC_WRITE), buildCommand(WatchXPlusConstants.CMD_TIME_LANGUAGE, WatchXPlusConstants.WRITE_VALUE, bArr)); } @Override public void dispose() { LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(getContext()); broadcastManager.unregisterReceiver(broadcastReceiver); super.dispose(); } private static double onSamplingInterval(int i, int i2) { switch (i) { case 1: return 1.0d * Math.pow(10.0d, -6.0d) * ((double) i2); case 2: return 1.0d * Math.pow(10.0d, -3.0d) * ((double) i2); case 3: return (double) (i2); case 4: return 10.0d * Math.pow(10.0d, -6.0d) * ((double) i2); case 5: return 10.0d * Math.pow(10.0d, -3.0d) * ((double) i2); case 6: return (double) (10 * i2); default: return (double) (10 * i2); } } @Override public boolean getImplicitCallbackModify() { return true; } @Override public boolean getSendWriteRequestResponse() { return false; } private static class Conversion { static byte[] toByteArr16(int value) { return new byte[]{(byte) (value >> 8), (byte) value}; } static int fromByteArr16(byte... value) { // equals calculateHigh int intValue = 0; for (int i2 = 0; i2 < value.length; i2++) { intValue += (value[i2] & 255) << (((value.length - 1) - i2) * 8); } return intValue; } static byte[] toByteArr32(int value) { return new byte[]{(byte) (value >> 24), (byte) (value >> 16), (byte) (value >> 8), (byte) value}; } // --Commented out by Inspection START (13.12.2019 г. 23:38): // static int calculateLow(byte... bArr) { // int i = 0; // int i2 = 0; // while (i < bArr.length) { // i2 += (bArr[i] & 255) << (i * 8); // i++; // } // return i2; // } // --Commented out by Inspection STOP (13.12.2019 г. 23:38) } }