package com.eveningoutpost.dexdrip.watch.miband; import android.annotation.SuppressLint; import android.app.PendingIntent; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattService; import android.content.Intent; import android.media.MediaPlayer; import android.os.PowerManager; import android.util.Pair; import com.eveningoutpost.dexdrip.Models.ActiveBgAlert; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.HeartRate; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer; import com.eveningoutpost.dexdrip.UtilityModels.AlertPlayer; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; import com.eveningoutpost.dexdrip.utils.bt.Subscription; import com.eveningoutpost.dexdrip.utils.framework.PoorMansConcurrentLinkedDeque; import com.eveningoutpost.dexdrip.utils.framework.WakeLockTrampoline; import com.eveningoutpost.dexdrip.watch.PrefBindingFactory; import com.eveningoutpost.dexdrip.watch.miband.Firmware.FirmwareOperations; import com.eveningoutpost.dexdrip.watch.miband.Firmware.WatchFaceGenerator; import com.eveningoutpost.dexdrip.watch.miband.message.AlertLevelMessage; import com.eveningoutpost.dexdrip.watch.miband.message.AlertMessage; import com.eveningoutpost.dexdrip.watch.miband.message.AuthMessages; import com.eveningoutpost.dexdrip.watch.miband.message.DeviceEvent; import com.eveningoutpost.dexdrip.watch.miband.message.DisplayControllMessage; import com.eveningoutpost.dexdrip.watch.miband.message.DisplayControllMessageMiBand2; import com.eveningoutpost.dexdrip.watch.miband.message.DisplayControllMessageMiband3_4; import com.eveningoutpost.dexdrip.watch.miband.message.FeaturesControllMessage; import com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes; import com.eveningoutpost.dexdrip.xdrip; import com.polidea.rxandroidble2.RxBleConnection; import com.polidea.rxandroidble2.RxBleDeviceServices; import com.polidea.rxandroidble2.exceptions.BleCannotSetCharacteristicNotificationException; import com.polidea.rxandroidble2.exceptions.BleCharacteristicNotFoundException; import com.polidea.rxandroidble2.exceptions.BleDisconnectedException; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.List; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import io.reactivex.schedulers.Schedulers; import lombok.Getter; import static com.eveningoutpost.dexdrip.Models.JoH.bytesToHex; import static com.eveningoutpost.dexdrip.Models.JoH.emptyString; import static com.eveningoutpost.dexdrip.Models.JoH.getResourceURI; import static com.eveningoutpost.dexdrip.Models.JoH.msTill; import static com.eveningoutpost.dexdrip.Models.JoH.niceTimeScalar; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSE; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.CLOSED; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.INIT; import static com.eveningoutpost.dexdrip.Services.JamBaseBluetoothSequencer.BaseState.SLEEP; import static com.eveningoutpost.dexdrip.watch.miband.Const.MIBAND_NOTIFY_TYPE_ALARM; import static com.eveningoutpost.dexdrip.watch.miband.Const.MIBAND_NOTIFY_TYPE_CALL; import static com.eveningoutpost.dexdrip.watch.miband.Const.MIBAND_NOTIFY_TYPE_CANCEL; import static com.eveningoutpost.dexdrip.watch.miband.Const.MIBAND_NOTIFY_TYPE_MESSAGE; import static com.eveningoutpost.dexdrip.watch.miband.Const.PREFERRED_MTU_SIZE; import static com.eveningoutpost.dexdrip.watch.miband.MiBand.MiBandType.MI_BAND2; import static com.eveningoutpost.dexdrip.watch.miband.MiBand.MiBandType.MI_BAND4; import static com.eveningoutpost.dexdrip.watch.miband.MiBand.MiBandType.UNKNOWN; import static com.eveningoutpost.dexdrip.watch.miband.MiBandService.MiBandState.AUTHORIZE_FAILED; import static com.eveningoutpost.dexdrip.watch.miband.message.DisplayControllMessageMiband3_4.NightMode.Sheduled; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_FAIL; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_MIBAND4_CODE_FAIL; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_MIBAND4_FAIL; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_REQUEST_RANDOM_AUTH_NUMBER; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_RESPONSE; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_SEND_ENCRYPTED_AUTH_NUMBER; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_SEND_KEY; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.AUTH_SUCCESS; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.COMMAND_ACK_FIND_PHONE_IN_PROGRESS; import static com.eveningoutpost.dexdrip.watch.miband.message.OperationCodes.COMMAND_DISABLE_CALL; /** * <p> * Data communication with MiBand compatible bands/watches */ public class MiBandService extends JamBaseBluetoothSequencer { private static final boolean d = true; private static final long RETRY_PERIOD_MS = Constants.SECOND_IN_MS * 30; // sleep for max ms if we have had no signal private static final long BG_UPDATE_INTERVAL = 30 * Constants.MINUTE_IN_MS; //minutes private static final long CONNECTION_TIMEOUT = 5 * Constants.MINUTE_IN_MS; //minutes private static final long RESTORE_NIGHT_MODE_DELAY = (Constants.SECOND_IN_MS * 7); private static final int QUEUE_EXPIRED_TIME = 30; //second private static final int QUEUE_DELAY = 0; //ms private static final int CALL_ALERT_DELAY = (int) (Constants.SECOND_IN_MS * 10); private Subscription authSubscription; private Subscription notifSubscriptionDeviceEvent; private Subscription notifSubscriptionHeartRateMeasurement; private AuthMessages authorisation; private Boolean isNeedToCheckRevision = true; private Boolean isNeedToAuthenticate = true; private boolean isWaitingCallResponce = false; private Boolean isWaitingSnoozeResponce = false; private Boolean isNeedToRestoreNightMode = false; private boolean isNightMode = false; static BatteryInfo batteryInfo = new BatteryInfo(); private FirmwareOperations firmware; private Subscription watchfaceSubscription; private MediaPlayer player; private PendingIntent bgServiceIntent; static private long bgWakeupTime; private MiBand.MiBandType prevDeviceType = UNKNOWN; private final PoorMansConcurrentLinkedDeque<QueueMessage> messageQueue = new PoorMansConcurrentLinkedDeque<>(); private QueueMessage queueItem; private int defaultSnoozle; public class QueueMessage { @Getter private String functionName; @Getter private String message_type = ""; @Getter private String message = ""; @Getter private String title = ""; @Getter private int defaultSnoozle = 0; public QueueMessage(String functionName) { this.functionName = functionName; } public QueueMessage(String functionName, String message_type, String message, String title, int defaultSnoozle) { this(functionName); this.message_type = message_type; this.message = message; this.title = title; this.defaultSnoozle = defaultSnoozle; } } public enum MIBAND_INTEND_STATES { UPDATE_PREF_SCREEN, UPDATE_PREF_DATA } { mState = new MiBandState().setLI(I); I.backgroundStepDelay = 0; //I.autoConnect = true; //I.playSounds = true; I.connectTimeoutMinutes = (int) CONNECTION_TIMEOUT; startBgTimer(); } private Class getPrefBinder() { MiBand.MiBandType type = MiBand.getMibandType(); if (type == MI_BAND2) return Miband2PrefBinding.class; else return Miband3_4PrefBinding.class; } @Override public void onCreate() { super.onCreate(); } private boolean readyToProcessCommand() { boolean result = I.state.equals(MiBandState.SLEEP) || I.state.equals(MiBandState.CLOSED) || I.state.equals(MiBandState.CLOSE) || I.state.equals(MiBandState.INIT) || I.state.equals(MiBandState.CONNECT_NOW); if (!result) UserError.Log.d(TAG, "readyToProcessCommand not ready because state :" + I.state.toString()); return result; } @Override public int onStartCommand(Intent intent, int flags, int startId) { final PowerManager.WakeLock wl = JoH.getWakeLock("Miband service", 60000); try { if (shouldServiceRun()) { final String authMac = MiBand.getPersistentAuthMac(); String mac = MiBand.getMac(); MiBand.MiBandType currDevice = MiBand.getMibandType(); if ((currDevice != prevDeviceType) && currDevice != UNKNOWN) { prevDeviceType = currDevice; UserError.Log.d(TAG, "Found new device: " + currDevice.toString()); MiBandEntry.sendPrefIntent(MIBAND_INTEND_STATES.UPDATE_PREF_SCREEN, 0, ""); } if (!authMac.equalsIgnoreCase(mac) || authMac.isEmpty()) { prevDeviceType = MiBand.getMibandType(); if (!authMac.isEmpty()) { String model = MiBand.getModel(); MiBand.setPersistentAuthMac(""); //flush old auth info MiBand.setModel(model, mac); } isNeedToAuthenticate = true; } if (emptyString(mac)) { // if mac not set then start up a scan and do nothing else new FindNearby().scan(); } else { setAddress(mac); if (intent != null) { final String function = intent.getStringExtra("function"); if (function != null) { UserError.Log.d(TAG, "onStartCommand was called with function:" + function); String message_type = intent.getStringExtra("message_type"); String message = intent.getStringExtra("message"); String title = intent.getStringExtra("title"); String defaultSnoozle = intent.getStringExtra("default_snoozle"); message = message != null ? message : ""; message_type = message_type != null ? message_type : ""; title = title != null ? title : ""; defaultSnoozle = defaultSnoozle != null ? defaultSnoozle : "0"; if (function.equals("refresh") && !JoH.pratelimit("miband-set-time-via-refresh-" + MiBand.getMac(), 5)) { return START_STICKY; } else { messageQueue.add(new QueueMessage(function, message_type, message, title, Integer.parseInt(defaultSnoozle))); if (readyToProcessCommand()) handleCommand(); } } else { // no specific function } } } return START_STICKY; } else { UserError.Log.d(TAG, "Service is NOT set be active - shutting down"); stopBgUpdateTimer(); stopConnection(); changeState(CLOSE); stopSelf(); return START_NOT_STICKY; } } finally { JoH.releaseWakeLock(wl); } } private void handleCommand() { if (messageQueue.isEmpty()) return; queueItem = messageQueue.poll(); switch (queueItem.functionName) { case "refresh": whenToRetryNextBgTimer(); //recalculate isNightMode ((MiBandState) mState).setSettingsSequence(); changeState(INIT); break; case "message": ((MiBandState) mState).setQueueSequence(); defaultSnoozle = queueItem.defaultSnoozle; changeState(INIT); break; case "glucose_after": if (!isWaitingSnoozeResponce) break; ((MiBandState) mState).setQueueSequence(); changeState(INIT); break; case "update_bg": if (isNightMode) break; startBgTimer(); ((MiBandState) mState).setSendReadingSequence(); changeState(INIT); break; case "update_bg_force": startBgTimer(); ((MiBandState) mState).setSendReadingSequence(); changeState(INIT); break; case "update_bg_as_notification": ((MiBandState) mState).setSendReadingSequence(); changeState(INIT); break; } } private static final boolean isBetweenValidTime(Date startTime, Date endTime, Date currentTime) { //Start Time Calendar StartTime = Calendar.getInstance(); StartTime.setTime(startTime); StartTime.set(1, 1, 1); Calendar EndTime = Calendar.getInstance(); EndTime.setTime(endTime); EndTime.set(1, 1, 1); //Current Time Calendar CurrentTime = Calendar.getInstance(); CurrentTime.setTime(currentTime); CurrentTime.set(1, 1, 1); if (EndTime.compareTo(StartTime) > 0) { if ((CurrentTime.compareTo(StartTime) >= 0) && (CurrentTime.compareTo(EndTime) <= 0)) { return true; } else { return false; } } else if (EndTime.compareTo(StartTime) < 0) { if ((CurrentTime.compareTo(EndTime) >= 0) && (CurrentTime.compareTo(StartTime) <= 0)) { return false; } else { return true; } } else { return false; } } private static final int compareTime(Date date1, Date date2) { //Start Time Calendar StartTime = Calendar.getInstance(); StartTime.setTime(date1); StartTime.set(1, 1, 1); Calendar EndTime = Calendar.getInstance(); EndTime.setTime(date2); EndTime.set(1, 1, 1); return EndTime.compareTo(StartTime); } private long whenToRetryNextBgTimer() { final long bg_time; Calendar expireDate = Calendar.getInstance(); expireDate.setTimeInMillis(System.currentTimeMillis() + BG_UPDATE_INTERVAL); isNightMode = false; if (MiBandEntry.isNightModeEnabled()) { int nightModeInterval = MiBandEntry.getNightModeInterval(); if (nightModeInterval != MiBandEntry.NIGHT_MODE_INTERVAL_STEP) { Date curr = Calendar.getInstance().getTime(); Date start = MiBandEntry.getNightModeStart(); Date end = MiBandEntry.getNightModeEnd(); Boolean result = isBetweenValidTime(start, end, curr); UserError.Log.d(TAG, "isBetweenValidTime: " + result); if (result) { Calendar calTimeCal = Calendar.getInstance(); calTimeCal.setTimeInMillis(System.currentTimeMillis() + nightModeInterval * Constants.MINUTE_IN_MS); if (compareTime(end, calTimeCal.getTime()) >= 0) { Calendar calEndCal = Calendar.getInstance(); calEndCal.setTime(end); calTimeCal.set(Calendar.HOUR_OF_DAY, calEndCal.get(Calendar.HOUR_OF_DAY)); calTimeCal.set(Calendar.MINUTE, calEndCal.get(Calendar.MINUTE)); } expireDate.setTimeInMillis(calTimeCal.getTimeInMillis()); isNightMode = true; } } } bg_time = expireDate.getTimeInMillis() - JoH.tsl(); return bg_time; } private void stopBgUpdateTimer() { JoH.cancelAlarm(xdrip.getAppContext(), bgServiceIntent); bgWakeupTime = 0; isNightMode = false; } private void startBgTimer() { stopBgUpdateTimer(); if (shouldServiceRun() && MiBand.isAuthenticated() && !MiBandEntry.isNeedSendReadingAsNotification()) { final long retry_in = whenToRetryNextBgTimer(); UserError.Log.d(TAG, "Scheduling next BgTimer in: " + JoH.niceTimeScalar(retry_in) + " @ " + JoH.dateTimeText(retry_in + JoH.tsl())); String function = "update_bg"; if (isNightMode) function = "update_bg_force"; bgServiceIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.MIBAND_SERVICE_BG_RETRY_ID, function); JoH.wakeUpIntent(xdrip.getAppContext(), retry_in, bgServiceIntent); bgWakeupTime = JoH.tsl() + retry_in; } else { UserError.Log.d(TAG, "Not setting retry timer as service should not be running"); } } private void acknowledgeFindPhone() { UserError.Log.d(TAG, "acknowledgeFindPhone"); I.connection.writeCharacteristic(Const.UUID_CHARACTERISTIC_3_CONFIGURATION, COMMAND_ACK_FIND_PHONE_IN_PROGRESS) .subscribe(val -> { if (d) UserError.Log.d(TAG, "Wrote acknowledgeFindPhone: " + JoH.bytesToHex(val)); }, throwable -> { UserError.Log.e(TAG, "Could not write acknowledgeFindPhone: " + throwable); }); } private void handleDeviceEvent(byte[] value) { if (value == null || value.length == 0) { return; } switch (value[0]) { case DeviceEvent.CALL_REJECT: UserError.Log.d(TAG, "call rejected"); if (ActiveBgAlert.currentlyAlerting() && isWaitingSnoozeResponce) { try { isWaitingSnoozeResponce = false; AlertPlayer.getPlayer().Snooze(xdrip.getAppContext(), defaultSnoozle, true); String msgText = "Alert snoozed for " + defaultSnoozle + " min"; UserError.Log.d(TAG, msgText); messageQueue.addFirst(new QueueMessage("message", MIBAND_NOTIFY_TYPE_MESSAGE, msgText, "Alert snoozed", 0)); if (readyToProcessCommand()) handleCommand(); } catch (NumberFormatException e) { UserError.Log.d(TAG, "Alert was attempted to be snoozed by watch, but snoozleVal was wrong"); } } isWaitingCallResponce = false; break; case DeviceEvent.CALL_IGNORE: UserError.Log.d(TAG, "call ignored"); isWaitingSnoozeResponce = false; isWaitingCallResponce = false; break; case DeviceEvent.BUTTON_PRESSED: UserError.Log.d(TAG, "button pressed"); break; case DeviceEvent.BUTTON_PRESSED_LONG: UserError.Log.d(TAG, "button long-pressed "); break; case DeviceEvent.START_NONWEAR: UserError.Log.d(TAG, "non-wear start detected"); break; case DeviceEvent.ALARM_TOGGLED: UserError.Log.d(TAG, "An alarm was toggled"); break; case DeviceEvent.FELL_ASLEEP: UserError.Log.d(TAG, "Fell asleep"); break; case DeviceEvent.WOKE_UP: UserError.Log.d(TAG, "Woke up"); break; case DeviceEvent.STEPSGOAL_REACHED: UserError.Log.d(TAG, "Steps goal reached"); break; case DeviceEvent.TICK_30MIN: UserError.Log.d(TAG, "Tick 30 min (?)"); break; case DeviceEvent.FIND_PHONE_START: UserError.Log.d(TAG, "find phone started"); if ((JoH.ratelimit("band_find phone_sound", 3))) { player = JoH.playSoundUri(getResourceURI(R.raw.default_alert)); } acknowledgeFindPhone(); break; case DeviceEvent.FIND_PHONE_STOP: UserError.Log.d(TAG, "find phone stopped"); if (player != null && player.isPlaying()) player.stop(); break; case DeviceEvent.MUSIC_CONTROL: UserError.Log.d(TAG, "got music control"); switch (value[1]) { case 0: UserError.Log.d(TAG, "Music app Event.PLAY"); break; case 1: UserError.Log.d(TAG, "Music app Event.PAUSE"); break; case 3: UserError.Log.d(TAG, "Music app Event.NEXT"); break; case 4: UserError.Log.d(TAG, "Music app Event.PREVIOUS"); break; case 5: UserError.Log.d(TAG, "Music app Event.VOLUMEUP"); break; case 6: UserError.Log.d(TAG, "Music app Event.VOLUMEDOWN"); break; case (byte) 224: UserError.Log.d(TAG, "Music app started"); break; case (byte) 225: UserError.Log.d(TAG, "Music app terminated"); break; default: UserError.Log.d(TAG, "unhandled music control event " + value[1]); return; } break; case DeviceEvent.MTU_REQUEST: int mtu = (value[2] & 0xff) << 8 | value[1] & 0xff; UserError.Log.d(TAG, "device announced MTU of " + mtu); break; default: UserError.Log.d(TAG, "unhandled event " + value[0]); } } static final List<UUID> huntCharacterstics = new ArrayList<>(); static { huntCharacterstics.add(Const.UUID_CHAR_NEW_ALERT); } @Override protected void onServicesDiscovered(RxBleDeviceServices services) { boolean found = false; for (BluetoothGattService service : services.getBluetoothGattServices()) { UserError.Log.d(TAG, "Service: " + getUUIDName(service.getUuid())); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { UserError.Log.d(TAG, "-- Character: " + getUUIDName(characteristic.getUuid())); for (final UUID check : huntCharacterstics) { if (characteristic.getUuid().equals(check)) { I.readCharacteristic = check; found = true; } } } } if (found) { I.isDiscoveryComplete = true; I.discoverOnce = true; changeNextState(); } else { UserError.Log.e(TAG, "Could not find characteristic during service discovery. This is very unusual"); } } @SuppressLint("CheckResult") private void getSoftwareRevision() { I.connection.readCharacteristic(Const.UUID_CHAR_SOFTWARE_REVISION_STRING).subscribe( readValue -> { String revision = new String(readValue); UserError.Log.d(TAG, "Got software revision: " + revision); MiBand.setVersion(revision, MiBand.getPersistentAuthMac()); isNeedToCheckRevision = false; changeNextState(); }, throwable -> { UserError.Log.e(TAG, "Could not read software revision: " + throwable); changeNextState(); }); } @SuppressLint("CheckResult") private void getBatteryInfo() { I.connection.readCharacteristic(Const.UUID_CHARACTERISTIC_6_BATTERY_INFO).subscribe( readValue -> { if (d) UserError.Log.d(TAG, "Got battery info: " + JoH.bytesToHex(readValue)); batteryInfo = new BatteryInfo(readValue); }, throwable -> { if (d) UserError.Log.e(TAG, "Could not read battery info: " + throwable); }); } @SuppressLint("CheckResult") private void getModelName() { I.connection.readCharacteristic(Const.UUID_CHAR_DEVICE_NAME).subscribe( readValue -> { String name = new String(readValue); if (d) UserError.Log.d(TAG, "Got device name: " + name); MiBand.setModel(name, MiBand.getPersistentAuthMac()); changeNextState(); }, throwable -> { if (d) UserError.Log.e(TAG, "Could not read device name: " + throwable); changeNextState(); }); } private Boolean sendBG() { BgReading last = BgReading.last(); AlertMessage message = new AlertMessage(); if (last == null || last.isStale()) { return false; } else { String messageText = "BG: " + last.displayValue(null) + " " + last.displaySlopeArrow(); UserError.Log.uel(TAG, "Send alert msg: " + messageText); if (MiBand.getMibandType() == MI_BAND2) { new QueueMe() .setBytes(message.getAlertMessageOld(messageText.toUpperCase(), AlertMessage.AlertCategory.SMS_MMS)) .setDescription("Send alert msg: " + messageText) .setQueueWriteCharacterstic(message.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } else { new QueueMe() .setBytes(message.getAlertMessage(messageText.toUpperCase(), AlertMessage.AlertCategory.CustomHuami, AlertMessage.CustomIcon.APP_11, messageText.toUpperCase())) .setDescription("Send alert msg: " + messageText) .setQueueWriteCharacterstic(message.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } } return true; } private void vibrateAlert(AlertLevelMessage.AlertLevelType level) { if (level == AlertLevelMessage.AlertLevelType.NoAlert) { new QueueMe() .setBytes(COMMAND_DISABLE_CALL) .setDescription("Send specific disable command for" + level) .setQueueWriteCharacterstic(Const.UUID_CHARACTERISTIC_CHUNKEDTRANSFER) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } AlertLevelMessage message = new AlertLevelMessage(); new QueueMe() .setBytes(message.getAlertLevelMessage(level)) .setDescription("Send vibrateAlert: " + level) .setQueueWriteCharacterstic(message.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } private void periodicVibrateAlert(int count, int activeVibrationTime, int pauseVibrationTime) { AlertLevelMessage message = new AlertLevelMessage(); new QueueMe() .setBytes(message.getPeriodicVibrationMessage((byte) count, (short) activeVibrationTime, (short) pauseVibrationTime)) .setDescription(String.format("Send periodicVibrateAlert c:%d a:%d p:%d", count, activeVibrationTime, pauseVibrationTime)) .setQueueWriteCharacterstic(message.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs((activeVibrationTime + pauseVibrationTime) * count) .queue(); } private void sendSettings() { List<Pair<Integer, Boolean>> features = PrefBindingFactory.getInstance(getPrefBinder()).getStates("miband_feature_"); FeaturesControllMessage featureMessage = new FeaturesControllMessage(); for (Pair<Integer, Boolean> item : features) { byte[] message = featureMessage.getMessage(item); if (message.length != 0) { new QueueMe() .setBytes(message) .setQueueWriteCharacterstic(featureMessage.getCharacteristicUUID()) .setDescription("Set feature:" + item.first + ":" + item.second) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } } List<Integer> screenOpt = PrefBindingFactory.getInstance(getPrefBinder()).getEnabled("miband_screen"); DisplayControllMessage dispMessage; MiBand.MiBandType type = MiBand.getMibandType(); if (type == MI_BAND2) dispMessage = new DisplayControllMessageMiBand2(); else dispMessage = new DisplayControllMessageMiband3_4(); new QueueMe() .setBytes(dispMessage.getDisplayItemsCmd(screenOpt)) .setQueueWriteCharacterstic(dispMessage.getCharacteristicUUID()) .setDescription("Set screens") .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); setNightMode(); } private void queueMessage() { String message = queueItem.message; if (d) UserError.Log.d(TAG, "Queuing message alert: " + message); if (isWaitingSnoozeResponce) { vibrateAlert(AlertLevelMessage.AlertLevelType.NoAlert); //disable call isWaitingSnoozeResponce = false; } AlertMessage alertMessage = new AlertMessage(); switch (queueItem.message_type != null ? queueItem.message_type : "null") { case MIBAND_NOTIFY_TYPE_CALL: new QueueMe() .setBytes(alertMessage.getAlertMessageOld(message, AlertMessage.AlertCategory.Call)) .setDescription("Send call alert: " + message) .setQueueWriteCharacterstic(alertMessage.getCharacteristicUUID()) .setRunnable(() -> isWaitingCallResponce = true) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); if (d) UserError.Log.d(TAG, "Queued call alert: " + message); break; case MIBAND_NOTIFY_TYPE_CANCEL: if (isWaitingCallResponce) { vibrateAlert(AlertLevelMessage.AlertLevelType.NoAlert); //disable call isWaitingCallResponce = false; if (d) UserError.Log.d(TAG, "Call disabled"); } break; case MIBAND_NOTIFY_TYPE_ALARM: new QueueMe() .setBytes(alertMessage.getAlertMessageOld(message, AlertMessage.AlertCategory.Call)) .setDescription("Sent glucose alert: " + message) .setQueueWriteCharacterstic(alertMessage.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setRunnable(() -> isWaitingSnoozeResponce = true) .setDelayMs(QUEUE_DELAY) .queue(); bgServiceIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.MIBAND_SERVICE_BG_RETRY_ID, "glucose_after"); JoH.wakeUpIntent(xdrip.getAppContext(), CALL_ALERT_DELAY, bgServiceIntent); break; case MIBAND_NOTIFY_TYPE_MESSAGE: if (MiBand.getMibandType() == MI_BAND2) { new QueueMe() .setBytes(alertMessage.getAlertMessageOld(message, AlertMessage.AlertCategory.SMS_MMS)) .setDescription("Sent message: " + message) .setQueueWriteCharacterstic(alertMessage.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } else { new QueueMe() .setBytes(alertMessage.getAlertMessage(message, AlertMessage.AlertCategory.CustomHuami, AlertMessage.CustomIcon.RED_WHITE_FIRE_8, queueItem.title)) .setDescription("Sent message: " + message) .setQueueWriteCharacterstic(alertMessage.getCharacteristicUUID()) .expireInSeconds(QUEUE_EXPIRED_TIME) .setDelayMs(QUEUE_DELAY) .queue(); } break; default: // glucose break; } // this parent method might get called multiple times Inevitable.task("miband-s-queue", 200, () -> changeState(mState.next())); } @SuppressLint("CheckResult") private void authPhase() { extendWakeLock(30000); RxBleConnection connection = I.connection; if (d) UserError.Log.d(TAG, "Authorizing"); if (I.connection == null) { if (d) UserError.Log.d(TAG, "Cannot enable as connection is null!"); return; } String authKey = MiBand.getPersistentAuthKey(); if (MiBand.getMibandType() == MI_BAND4) { if (authKey.isEmpty()) { authKey = MiBand.getAuthKey(); if (authKey.isEmpty()) { authKey = AuthMessages.getAuthCodeFromFilesSystem(MiBand.getMac()); } if (!AuthMessages.isValidAuthKey(authKey)) { JoH.static_toast_long("Wrong miband authorization key, please recheck a key and try to reconnect again"); changeState(AUTHORIZE_FAILED); return; } else { MiBand.setAuthKey(authKey); } } } if (!AuthMessages.isValidAuthKey(authKey)) { authKey = ""; } if (d) UserError.Log.d(TAG, "authKey: " + authKey); authorisation = new AuthMessages(MiBand.getMibandType(), authKey); if (d) UserError.Log.d(TAG, "localKey: " + JoH.bytesToHex(authorisation.getLocalKey())); authSubscription = new Subscription( connection.setupNotification(authorisation.getCharacteristicUUID()) .timeout(20, TimeUnit.SECONDS) // WARN // .observeOn(Schedulers.newThread()) // needed? .doOnNext(notificationObservable -> { if (d) UserError.Log.d(TAG, "Notification for auth enabled"); if (MiBand.isAuthenticated()) { connection.writeCharacteristic(authorisation.getCharacteristicUUID(), authorisation.getAuthKeyRequest()) //get random key from band .subscribe(val -> { if (d) UserError.Log.d(TAG, "Wrote getAuthKeyRequest: " + JoH.bytesToHex(val)); }, throwable -> { UserError.Log.e(TAG, "Could not getAuthKeyRequest: " + throwable); }); } else { connection.writeCharacteristic(authorisation.getCharacteristicUUID(), authorisation.getAuthCommand()) .subscribe(characteristicValue -> { UserError.Log.d(TAG, "Wrote getAuthCommand, got: " + JoH.bytesToHex(characteristicValue)); }, throwable -> { UserError.Log.e(TAG, "Could not write getAuthCommand: " + throwable); } ); } } ) .flatMap(notificationObservable -> notificationObservable) .subscribe(bytes -> { // incoming notifications if (d) UserError.Log.d(TAG, "Received auth notification bytes: " + bytesToHex(bytes)); ProcessAuthCommands(connection, bytes); // changeNextState(); }, throwable -> { UserError.Log.d(TAG, "Throwable in Record Notification: " + throwable); if (throwable instanceof BleCharacteristicNotFoundException) { // maybe legacy - ignore for now but needs better handling UserError.Log.d(TAG, "Characteristic not found for notification"); } else if (throwable instanceof BleCannotSetCharacteristicNotificationException) { UserError.Log.e(TAG, "Problems setting notifications - disconnecting"); } else if (throwable instanceof BleDisconnectedException) { UserError.Log.d(TAG, "Disconnected while enabling notifications"); } else if (throwable instanceof TimeoutException) { //check if it is normal timeout if (!MiBand.isAuthenticated()) { String errorText = "MiBand authentication failed due to authentication timeout. When your Mi Band vibrates and blinks, tap it a few times in a row."; UserError.Log.d(TAG, errorText); JoH.static_toast_long(errorText); } } if (authSubscription != null) { authSubscription.unsubscribe(); } changeState(CLOSE); })); } @SuppressLint("CheckResult") private void ProcessAuthCommands(RxBleConnection connection, byte[] value) { if (value[0] == AUTH_RESPONSE && value[1] == AUTH_SEND_KEY && (value[2] & 0x0f) == AUTH_SUCCESS) { connection.writeCharacteristic(authorisation.getCharacteristicUUID(), authorisation.getAuthKeyRequest()) //get random key from band .subscribe(val -> { if (d) UserError.Log.d(TAG, "Wrote OPCODE_AUTH_REQ1: " + JoH.bytesToHex(val)); }, throwable -> { UserError.Log.e(TAG, "Could not write OPCODE_AUTH_REQ1: " + throwable); }); } else if (value[0] == AUTH_RESPONSE && (value[1] & 0x0f) == AUTH_REQUEST_RANDOM_AUTH_NUMBER && value[2] == AUTH_SUCCESS) { byte[] tmpValue = Arrays.copyOfRange(value, 3, 19); try { byte[] authReply = authorisation.calculateAuthReply(tmpValue); connection.writeCharacteristic(authorisation.getCharacteristicUUID(), authReply) //get random key from band .subscribe(val -> { if (d) UserError.Log.d(TAG, "Wrote OPCODE_AUTH_REQ2: " + JoH.bytesToHex(val)); }, throwable -> { UserError.Log.e(TAG, "Could not write OPCODE_AUTH_REQ2: " + throwable); }); }catch (Exception e){ JoH.static_toast_long(e.getMessage()); UserError.Log.e(TAG, (e.getMessage())); changeState(AUTHORIZE_FAILED); } } else if (value[0] == AUTH_RESPONSE && (value[1] & 0x0f) == AUTH_SEND_ENCRYPTED_AUTH_NUMBER && value[2] == AUTH_SUCCESS) { isNeedToAuthenticate = false; if (MiBand.getPersistentAuthMac().isEmpty()) { MiBand.setPersistentAuthMac(MiBand.getMac()); MiBand.setPersistentAuthKey(JoH.bytesToHex(authorisation.getLocalKey()), MiBand.getPersistentAuthMac()); String msg = "MiBand was successfully authenticated"; JoH.static_toast_long(msg); UserError.Log.e(TAG, msg); } if (authSubscription != null) { authSubscription.unsubscribe(); } changeNextState(); } else if (value[0] == AUTH_RESPONSE && (((value[2] & 0x0f) == AUTH_FAIL) || (value[2] == AUTH_MIBAND4_FAIL) || (value[2] == AUTH_MIBAND4_CODE_FAIL))) { MiBand.setPersistentAuthKey("", MiBand.getPersistentAuthMac()); if (authSubscription != null) { authSubscription.unsubscribe(); } String msg = "Cannot authorize miband, please recheck Auth code"; JoH.static_toast_long(msg); UserError.Log.e(TAG, msg); changeState(AUTHORIZE_FAILED); } } @SuppressLint("CheckResult") private void installWatchface() { //TODO decrease display brightness before uploading watchface to minimize battery consumption RxBleConnection connection = I.connection; if (d) UserError.Log.d(TAG, "Install WatchFace"); if (I.connection == null) { if (d) UserError.Log.d(TAG, "Cannot enable as connection is null!"); return; } try { WatchFaceGenerator wfGen = new WatchFaceGenerator(getBaseContext().getAssets()); byte[] fwArray = wfGen.genWatchFace(); if (fwArray == null || fwArray.length == 0) { resetFirmwareState(false, "Empty image"); return; } firmware = new FirmwareOperations(fwArray); } catch (Exception e) { resetFirmwareState(false, "FirmwareOperations error " + e.getMessage()); return; } if (d) UserError.Log.d(TAG, "Begin uploading Watchface, lenght: " + firmware.getSize()); if (d) UserError.Log.d(TAG, "Requesting to enable notifications for installWatchface"); watchfaceSubscription = new Subscription( connection.setupNotification(firmware.getFirmwareCharacteristicUUID()) .timeout(400, TimeUnit.SECONDS) // WARN .doOnNext(notificationObservable -> { if (d) UserError.Log.d(TAG, "Notification for firmware enabled"); firmware.nextSequence(); processFirmwareCommands(null, true); } ) .flatMap(notificationObservable -> notificationObservable) .subscribe(bytes -> { // incoming notifications if (d) UserError.Log.d(TAG, "Received firmware notification bytes: " + bytesToHex(bytes)); processFirmwareCommands(bytes, false); }, throwable -> { UserError.Log.d(TAG, "Throwable in firmware Notification: " + throwable); if (throwable instanceof BleCharacteristicNotFoundException) { // maybe legacy - ignore for now but needs better handling UserError.Log.d(TAG, "Characteristic not found for notification"); } else if (throwable instanceof BleCannotSetCharacteristicNotificationException) { UserError.Log.e(TAG, "Problems setting notifications - disconnecting"); } else if (throwable instanceof BleDisconnectedException) { UserError.Log.d(TAG, "Disconnected while enabling notifications"); } else if (throwable instanceof TimeoutException) { UserError.Log.d(TAG, "Timeout"); } resetFirmwareState(false); })); } @SuppressLint("CheckResult") private void processFirmwareCommands(byte[] value, boolean isSeqCommand) { RxBleConnection connection = I.connection; FirmwareOperations.SequenceType seq = firmware.getSequence(); if (d) UserError.Log.d(TAG, "processFirmwareCommands: " + bytesToHex(value) + ": seq:" + seq.toString()); if (isSeqCommand) { switch (seq) { case SET_NIGHTMODE: { if (true) { isNeedToRestoreNightMode = true; DisplayControllMessageMiband3_4 dispControl = new DisplayControllMessageMiband3_4(); Calendar sheduledCalendar = Calendar.getInstance(); sheduledCalendar.set(Calendar.HOUR_OF_DAY, 0); sheduledCalendar.set(Calendar.MINUTE, 0); Date sheduledDate = sheduledCalendar.getTime(); connection.writeCharacteristic(dispControl.getCharacteristicUUID(), dispControl.setNightModeCmd(Sheduled, sheduledDate, sheduledDate)) .subscribe(valB -> { UserError.Log.d(TAG, "Wrote nigntmode, got: " + JoH.bytesToHex(valB)); firmware.nextSequence(); processFirmwareCommands(null, true); }, throwable -> { UserError.Log.e(TAG, "Could not write nigntmode: " + throwable); firmware.nextSequence(); processFirmwareCommands(null, true); } ); } else { firmware.nextSequence(); processFirmwareCommands(null, true); } break; } case PREPARE_UPLOAD: { connection.writeCharacteristic(firmware.getFirmwareCharacteristicUUID(), firmware.prepareFWUploadInitCommand()) .subscribe(valB -> { UserError.Log.d(TAG, "Wrote prepareFWUploadInitCommand, got: " + JoH.bytesToHex(valB)); }, throwable -> { UserError.Log.e(TAG, "Could not write prepareFWUploadInitCommand: " + throwable); resetFirmwareState(false); } ); firmware.nextSequence(); break; } } return; } else { if (value.length != 3 && value.length != 11) { UserError.Log.e(TAG, "Notifications should be 3 or 11 bytes long."); return; } boolean success = value[2] == OperationCodes.SUCCESS; if (value[0] == OperationCodes.RESPONSE && success) { try { switch (value[1]) { case OperationCodes.COMMAND_FIRMWARE_INIT: { if (seq == FirmwareOperations.SequenceType.TRANSFER_FW_START) { connection.writeCharacteristic(firmware.getFirmwareCharacteristicUUID(), firmware.getFirmwareStartCommand()) .subscribe(valB -> { UserError.Log.d(TAG, "Wrote Start command, got: " + JoH.bytesToHex(valB)); }, throwable -> { UserError.Log.e(TAG, "Could not write Start command: " + throwable); resetFirmwareState(false); } ); firmware.nextSequence(); } else if (seq == FirmwareOperations.SequenceType.TRANSFER_SEND_WF_INFO) { connection.writeCharacteristic(firmware.getFirmwareCharacteristicUUID(), firmware.sendFwInfo()) .subscribe(valB -> { UserError.Log.d(TAG, "Wrote sendFwInfo, got: " + JoH.bytesToHex(valB)); }, throwable -> { UserError.Log.e(TAG, "Could not write firmware info: " + throwable); resetFirmwareState(false); } ); firmware.nextSequence(); break; } break; } case OperationCodes.COMMAND_FIRMWARE_START_DATA: { sendFirmwareData(); break; } case OperationCodes.COMMAND_FIRMWARE_CHECKSUM: { firmware.nextSequence(); if (firmware.getFirmwareType() == FirmwareOperations.FirmwareType.FIRMWARE) { //send reboot } else { UserError.Log.e(TAG, "Watch Face has been installed successfully"); resetFirmwareState(true); } break; } case OperationCodes.COMMAND_FIRMWARE_REBOOT: { UserError.Log.e(TAG, "Reboot command successfully sent."); resetFirmwareState(true); break; } default: { resetFirmwareState(false, "Unexpected response during firmware update"); } } } catch (Exception ex) { resetFirmwareState(false); } } else { String errorMessage = null; Boolean sendBGNotification = false; if (value[2] == OperationCodes.LOW_BATTERY_ERROR) { errorMessage = "Cannot upload watchface, low battery, please charge device"; sendBGNotification = true; } else if (value[2] == OperationCodes.TIMER_RUNNING) { errorMessage = "Cannot upload watchface, timer running on band"; } else if (value[2] == OperationCodes.ON_CALL) { errorMessage = "Cannot upload watchface, call in progress"; } else { errorMessage = "Unexpected notification during firmware update:" + JoH.bytesToHex(value); } resetFirmwareState(false, errorMessage); if (sendBGNotification) { emptyQueue(); JoH.startService(MiBandService.class, "function", "update_bg_as_notification"); changeState(SLEEP); } } } } private void resetFirmwareState(Boolean result) { resetFirmwareState(result, null); } private void resetFirmwareState(Boolean result, String customText) { if (watchfaceSubscription != null) { watchfaceSubscription.unsubscribe(); watchfaceSubscription = null; } String finishText = customText; if (customText == null) { if (!result) finishText = xdrip.getAppContext().getResources().getString(R.string.miband_watchface_istall_error); else finishText = xdrip.getAppContext().getResources().getString(R.string.miband_watchface_istall_success); } UserError.Log.d(TAG, "resetFirmwareState result:" + result + ":" + finishText); if (isNeedToRestoreNightMode) { JoH.threadSleep(RESTORE_NIGHT_MODE_DELAY); setNightMode(); } if (result) { changeNextState(); return; } changeState(SLEEP); } private void sendFirmwareData() { byte[] fwbytes = firmware.getBytes(); int len = firmware.getSize(); int mtu = I.connection.getMtu(); if (!MiBandEntry.isNeedToDisableHightMTU()) firmware.setMTU(mtu); final int packetLength = firmware.getPackeLenght(); if (d) UserError.Log.d(TAG, "Firmware packet lengh: " + packetLength); int packets = len / packetLength; // going from 0 to len int firmwareProgress = 0; for (int i = 0; i < packets; i++) { byte[] fwChunk = Arrays.copyOfRange(fwbytes, i * packetLength, i * packetLength + packetLength); sendFirmwareCommand(firmware.getFirmwareDataCharacteristicUUID(), fwChunk, "Chunk:" + i).queue(); firmwareProgress += packetLength; int progressPercent = (int) ((((float) firmwareProgress) / len) * 100); if ((i > 0) && (i % 30 == 0)) { sendFirmwareCommand(firmware.getFirmwareCharacteristicUUID(), firmware.sendSync(), "Sync " + progressPercent + "%").queue(); } } if (firmwareProgress < len) { //last chunk int progressPercent = 100; byte[] fwChunk = Arrays.copyOfRange(fwbytes, packets * packetLength, len); sendFirmwareCommand(firmware.getFirmwareDataCharacteristicUUID(), fwChunk, "Last chunk").queue(); } sendFirmwareCommand(firmware.getFirmwareCharacteristicUUID(), firmware.sendChecksum(), "sendChecksum").setRunnable(new Runnable() { @Override public void run() { firmware.nextSequence(); } }).send(); } QueueMe sendFirmwareCommand(final UUID uuid, final byte[] bytes, String info) { return new QueueMe() .setBytes(bytes) .setDescription(info) .setQueueWriteCharacterstic(uuid) .expireInSeconds(400) .setDelayMs(0); } private void setNightMode() { if (d) UserError.Log.d(TAG, "Restore night mode"); Date start = null, end = null; DisplayControllMessageMiband3_4.NightMode nightMode = DisplayControllMessageMiband3_4.NightMode.Off; if (MiBandEntry.isNightModeEnabled()) { nightMode = DisplayControllMessageMiband3_4.NightMode.Sheduled; start = MiBandEntry.getNightModeStart(); end = MiBandEntry.getNightModeEnd(); } RxBleConnection connection = I.connection; DisplayControllMessageMiband3_4 dispControl = new DisplayControllMessageMiband3_4(); connection.writeCharacteristic(dispControl.getCharacteristicUUID(), dispControl.setNightModeCmd(nightMode, start, end)) .subscribe(valB -> { if (d) UserError.Log.d(TAG, "Wrote nightmode"); isNeedToRestoreNightMode = false; }, throwable -> { if (d) UserError.Log.e(TAG, "Could not write nightmode: " + throwable); } ); } private void handleHeartrate(byte[] value) { if (value.length == 2 && value[0] == 0) { int hrValue = (value[1] & 0xff); if (d) UserError.Log.d(TAG, "heart rate: " + hrValue); HeartRate.create(JoH.tsl(), hrValue, 1); } } @SuppressLint("CheckResult") private void enableNotification() { if (d) UserError.Log.d(TAG, "enableNotifications called"); if (I.connection == null) { if (d) UserError.Log.d(TAG, "Cannot enable as connection is null!"); return; } enableHeartRateNotification(); if (I.isNotificationEnabled) { if (d) UserError.Log.d(TAG, "Notifications already enabled"); changeNextState(); return; } if (notifSubscriptionDeviceEvent != null) { notifSubscriptionDeviceEvent.unsubscribe(); } if (notifSubscriptionHeartRateMeasurement != null) { notifSubscriptionHeartRateMeasurement.unsubscribe(); } if (d) UserError.Log.d(TAG, "Requesting to enable device event notifications"); I.connection.requestMtu(PREFERRED_MTU_SIZE).subscribe(); notifSubscriptionDeviceEvent = new Subscription(I.connection.setupNotification(Const.UUID_CHARACTERISTIC_DEVICEEVENT) .doOnNext(notificationObservable -> { I.isNotificationEnabled = true; changeNextState(); }).flatMap(notificationObservable -> notificationObservable) //.timeout(5, TimeUnit.SECONDS) .observeOn(Schedulers.newThread()) .subscribe(bytes -> { // incoming notifications if (d) UserError.Log.d(TAG, "Received device notification bytes: " + bytesToHex(bytes)); handleDeviceEvent(bytes); }, throwable -> { UserError.Log.d(TAG, "Throwable in Record Notification: " + throwable); I.isNotificationEnabled = false; if (throwable instanceof BleCharacteristicNotFoundException) { // maybe legacy - ignore for now but needs better handling UserError.Log.d(TAG, "Characteristic not found for notification"); changeNextState(); } else { UserError.Log.d(TAG, "Disconnected exception"); isNeedToAuthenticate = true; messageQueue.clear(); changeState(CLOSE); } } )); } private void enableHeartRateNotification() { if (MiBandEntry.isNeedToCollectHR()) { if (notifSubscriptionHeartRateMeasurement != null) return; } else { if (notifSubscriptionHeartRateMeasurement != null) { notifSubscriptionHeartRateMeasurement.unsubscribe(); notifSubscriptionHeartRateMeasurement = null; return; } } if (d) UserError.Log.d(TAG, "Requesting to enable HR notifications"); notifSubscriptionHeartRateMeasurement = new Subscription(I.connection.setupNotification(Const.UUID_CHAR_HEART_RATE_MEASUREMENT) .flatMap(notificationObservable -> notificationObservable) .observeOn(Schedulers.newThread()) .subscribe(bytes -> { // incoming notifications if (d) UserError.Log.d(TAG, "Received HR notification bytes: " + bytesToHex(bytes)); handleHeartrate(bytes); }, throwable -> { notifSubscriptionHeartRateMeasurement.unsubscribe(); notifSubscriptionHeartRateMeasurement = null; UserError.Log.d(TAG, "HR Throwable in Record Notification: " + throwable); if (throwable instanceof BleCharacteristicNotFoundException) { UserError.Log.d(TAG, "HR Characteristic not found for notification"); } else { UserError.Log.d(TAG, "HR Disconnected exception"); } } )); } @Override protected synchronized boolean automata() { if (d) UserError.Log.d(TAG, "Automata called in" + TAG); extendWakeLock(2000); if (shouldServiceRun()) { switch (I.state) { case INIT: // connect by default changeNextState(); break; case MiBandState.GET_MODEL_NAME: cancelRetryTimer(); if (isNeedToRestoreNightMode) { setNightMode(); } if (MiBand.getModel().isEmpty()) { getModelName(); } else changeNextState(); break; case MiBandState.GET_SOFT_REVISION: if (MiBand.getVersion().isEmpty() || isNeedToCheckRevision) getSoftwareRevision(); else changeNextState(); break; case MiBandState.AUTHENTICATE: if (isNeedToAuthenticate) { changeNextState(); } else { changeState(MiBandState.ENABLE_NOTIFICATIONS); } break; case MiBandState.AUTHORIZE: authPhase(); break; case MiBandState.ENABLE_NOTIFICATIONS: enableNotification(); break; case MiBandState.SEND_SETTINGS: sendSettings(); changeNextState(); break; case MiBandState.SEND_BG: if (!MiBandEntry.isNeedSendReading()) { changeState(MiBandState.SEND_QUEUE); break; } if (isWaitingSnoozeResponce) { vibrateAlert(AlertLevelMessage.AlertLevelType.NoAlert); //disable call isWaitingSnoozeResponce = false; } final String bgAsNotification = queueItem.functionName; if (MiBand.getMibandType() != MI_BAND4 || MiBandEntry.isNeedSendReadingAsNotification() || bgAsNotification.equals("update_bg_as_notification")) { Boolean result = sendBG(); if (result) changeState(MiBandState.VIBRATE_AFTER_READING); else changeState(MiBandState.SEND_QUEUE); break; } changeState(MiBandState.INSTALL_WATCHFACE); break; case MiBandState.INSTALL_WATCHFACE: installWatchface(); changeNextState(); break; case MiBandState.INSTALL_WATCHFACE_IN_PROGRESS: break; case MiBandState.INSTALL_WATCHFACE_FINISHED: break; case MiBandState.VIBRATE_AFTER_READING: if (MiBandEntry.isVibrateOnReadings() && !MiBandEntry.isNeedSendReadingAsNotification()) vibrateAlert(AlertLevelMessage.AlertLevelType.VibrateAlert); changeNextState(); break; case MiBandState.GET_BATTERY_INFO: getBatteryInfo(); changeNextState(); break; case MiBandState.QUEUE_MESSAGE: queueMessage(); changeNextState(); break; case SLEEP: handleCommand(); break; case CLOSED: stopConnection(); return super.automata(); default: return super.automata(); } } else { UserError.Log.d(TAG, "Service should not be running inside automata"); stopSelf(); } return true; // lies } private void stopConnection() { isNeedToAuthenticate = true; isWaitingCallResponce = false; isWaitingSnoozeResponce = false; messageQueue.clear(); setRetryTimerReal(); // local retry strategy } @Override public void resetBluetoothIfWeSeemToAlreadyBeConnected(String mac) { //super.resetBluetoothIfWeSeemToAlreadyBeConnected(mac); //do not reset } private boolean shouldServiceRun() { return MiBandEntry.isEnabled(); } @Override protected void setRetryTimerReal() { if (shouldServiceRun() && MiBand.isAuthenticated()) { final long retry_in = whenToRetryNext(); UserError.Log.d(TAG, "setRetryTimerReal: Restarting in: " + (retry_in / Constants.SECOND_IN_MS) + " seconds"); I.serviceIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.MIBAND_SERVICE_RETRY_ID, "message"); I.retry_time = JoH.wakeUpIntent(xdrip.getAppContext(), retry_in, I.serviceIntent); I.wakeup_time = JoH.tsl() + retry_in; } else { UserError.Log.d(TAG, "Not setting retry timer as service should not be running"); } } private void cancelRetryTimer() { JoH.cancelAlarm(xdrip.getAppContext(), I.serviceIntent); I.wakeup_time = 0; } private long whenToRetryNext() { I.retry_backoff = RETRY_PERIOD_MS; return I.retry_backoff; } static class MiBandState extends JamBaseBluetoothSequencer.BaseState { static final String SEND_BG = "Setting Time"; static final String SEND_SETTINGS = "Updating Settings"; static final String QUEUE_MESSAGE = "Queue message"; static final String AUTHENTICATE = "Authenticate"; static final String AUTHORIZE = "Authorize phase"; static final String AUTHORIZE_FAILED = "Authorization failed"; static final String GET_MODEL_NAME = "Getting model name"; static final String GET_SOFT_REVISION = "Getting software revision"; static final String ENABLE_NOTIFICATIONS = "Enable notification"; static final String GET_BATTERY_INFO = "Getting battery info"; static final String INSTALL_WATCHFACE = "Watchface installation"; static final String INSTALL_WATCHFACE_IN_PROGRESS = "Watchface installation in progress"; static final String INSTALL_WATCHFACE_FINISHED = "Watchface installation finished"; static final String VIBRATE_AFTER_READING = "Vibrate"; private static final String TAG = "MiBandStateSequence"; void prepareInitialSequences() { sequence.clear(); sequence.add(INIT); sequence.add(CONNECT_NOW); sequence.add(GET_MODEL_NAME); sequence.add(GET_SOFT_REVISION); sequence.add(AUTHENTICATE); sequence.add(AUTHORIZE); sequence.add(ENABLE_NOTIFICATIONS); } void prepareFinalSequences() { sequence.add(SEND_QUEUE); sequence.add(GET_BATTERY_INFO); sequence.add(SLEEP); sequence.add(AUTHORIZE_FAILED); } void setSendReadingSequence() { UserError.Log.d(TAG, "SET UPDATE WATCHFACE DATA SEQUENCE"); prepareInitialSequences(); sequence.add(SEND_BG); sequence.add(INSTALL_WATCHFACE); sequence.add(INSTALL_WATCHFACE_IN_PROGRESS); sequence.add(INSTALL_WATCHFACE_FINISHED); sequence.add(VIBRATE_AFTER_READING); prepareFinalSequences(); } void setQueueSequence() { UserError.Log.d(TAG, "SET QUEUE SEQUENCE"); prepareInitialSequences(); sequence.add(QUEUE_MESSAGE); prepareFinalSequences(); } void setSettingsSequence() { UserError.Log.d(TAG, "SET SETTINGS SEQUENCE"); prepareInitialSequences(); sequence.add(SEND_SETTINGS); prepareFinalSequences(); } } // Mega Status public static List<StatusItem> megaStatus() { final List<StatusItem> l = new ArrayList<>(); final Inst II = Inst.get(MiBandService.class.getSimpleName()); l.add(new StatusItem("Model", MiBand.getModel())); l.add(new StatusItem("Software version", MiBand.getVersion())); l.add(new StatusItem("Mac address", MiBand.getMac())); l.add(new StatusItem("Connected", II.isConnected ? "Yes" : "No")); l.add(new StatusItem("Is authenticated", MiBand.isAuthenticated() ? "Yes" : "No")); if (II.isConnected) { int levelInPercent = batteryInfo.getLevelInPercent(); String levelInPercentText; if (levelInPercent == 1000) levelInPercentText = "Unknown"; else levelInPercentText = levelInPercent + "%"; l.add(new StatusItem("Battery", levelInPercentText)); } if (II.wakeup_time != 0) { final long till = msTill(II.wakeup_time); if (till > 0) l.add(new StatusItem("Wake Up", niceTimeScalar(till))); } if (bgWakeupTime != 0) { final long till = msTill(bgWakeupTime); if (till > 0) l.add(new StatusItem("Next time update", niceTimeScalar(till))); } l.add(new StatusItem("State", II.state)); final int qsize = II.getQueueSize(); if (qsize > 0) { l.add(new StatusItem("Queue", qsize + " items")); } return l; } }