package com.eveningoutpost.dexdrip.Services; import android.annotation.TargetApi; import android.app.PendingIntent; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.SharedPreferences; import android.content.res.Configuration; import android.os.Build; import android.os.IBinder; import android.os.PowerManager; import android.preference.PreferenceManager; import android.support.annotation.NonNull; import android.text.SpannableString; import android.text.SpannableStringBuilder; import com.eveningoutpost.dexdrip.AddCalibration; import com.eveningoutpost.dexdrip.DoubleCalibrationActivity; import com.eveningoutpost.dexdrip.G5Model.BatteryInfoRxMessage; import com.eveningoutpost.dexdrip.G5Model.BluetoothServices; import com.eveningoutpost.dexdrip.G5Model.CalibrationState; import com.eveningoutpost.dexdrip.G5Model.DexSyncKeeper; import com.eveningoutpost.dexdrip.G5Model.DexTimeKeeper; import com.eveningoutpost.dexdrip.G5Model.FirmwareCapability; import com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine; import com.eveningoutpost.dexdrip.G5Model.TransmitterStatus; import com.eveningoutpost.dexdrip.G5Model.VersionRequest1RxMessage; import com.eveningoutpost.dexdrip.G5Model.VersionRequest2RxMessage; import com.eveningoutpost.dexdrip.G5Model.VersionRequestRxMessage; import com.eveningoutpost.dexdrip.Home; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.Sensor; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.UtilityModels.BroadcastGlucose; import com.eveningoutpost.dexdrip.UtilityModels.CollectionServiceStarter; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.UtilityModels.RxBleProvider; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight; import com.eveningoutpost.dexdrip.UtilityModels.WholeHouse; import com.eveningoutpost.dexdrip.ui.helpers.Span; import com.eveningoutpost.dexdrip.utils.DexCollectionType; import com.eveningoutpost.dexdrip.utils.bt.Subscription; import com.eveningoutpost.dexdrip.utils.framework.WakeLockTrampoline; import com.eveningoutpost.dexdrip.xdrip; import com.google.common.collect.Sets; import com.polidea.rxandroidble2.RxBleClient; import com.polidea.rxandroidble2.RxBleConnection; import com.polidea.rxandroidble2.RxBleCustomOperation; import com.polidea.rxandroidble2.RxBleDevice; import com.polidea.rxandroidble2.RxBleDeviceServices; import com.polidea.rxandroidble2.exceptions.BleGattCallbackTimeoutException; import com.polidea.rxandroidble2.exceptions.BleScanException; import com.polidea.rxandroidble2.internal.RxBleLog; import com.polidea.rxandroidble2.internal.connection.RxBleGattCallback; import com.polidea.rxandroidble2.scan.ScanFilter; import com.polidea.rxandroidble2.scan.ScanResult; import com.polidea.rxandroidble2.scan.ScanSettings; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.Locale; import java.util.Set; import java.util.concurrent.TimeUnit; import io.reactivex.Observable; import io.reactivex.Scheduler; import io.reactivex.schedulers.Schedulers; import lombok.Setter; import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.getUUIDName; import static com.eveningoutpost.dexdrip.G5Model.CalibrationState.Ok; import static com.eveningoutpost.dexdrip.G5Model.CalibrationState.Unknown; import static com.eveningoutpost.dexdrip.G5Model.G6CalibrationParameters.getCurrentSensorCode; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.CLOSED_OK_TEXT; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.evaluateG6Settings; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.pendingCalibration; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.pendingStart; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.pendingStop; import static com.eveningoutpost.dexdrip.G5Model.Ob1G5StateMachine.usingAlt; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.BOND; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.CLOSE; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.CLOSED; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.CONNECT; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.CONNECT_NOW; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.DISCOVER; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.GET_DATA; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.INIT; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.PREBOND; import static com.eveningoutpost.dexdrip.Services.Ob1G5CollectionService.STATE.SCAN; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.G5_CALIBRATION_REQUEST; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.G5_SENSOR_FAILED; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.G5_SENSOR_RESTARTED; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.G5_SENSOR_STARTED; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.HOUR_IN_MS; import static com.eveningoutpost.dexdrip.UtilityModels.Constants.SECOND_IN_MS; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.BAD; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.CRITICAL; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NORMAL; import static com.eveningoutpost.dexdrip.UtilityModels.StatusItem.Highlight.NOTICE; import static com.eveningoutpost.dexdrip.utils.DexCollectionType.DexcomG5; import static com.eveningoutpost.dexdrip.utils.bt.Subscription.addErrorHandler; import static com.eveningoutpost.dexdrip.xdrip.gs; /*import com.polidea.rxandroidble.RxBleClient; import com.polidea.rxandroidble.RxBleConnection; import com.polidea.rxandroidble.RxBleCustomOperation; import com.polidea.rxandroidble.RxBleDevice; import com.polidea.rxandroidble.RxBleDeviceServices; import com.polidea.rxandroidble.exceptions.BleGattCallbackTimeoutException; import com.polidea.rxandroidble.exceptions.BleScanException; import com.polidea.rxandroidble.internal.RxBleLog; import com.polidea.rxandroidble.internal.connection.RxBleGattCallback; import com.polidea.rxandroidble.scan.ScanFilter; import com.polidea.rxandroidble.scan.ScanResult; import com.polidea.rxandroidble.scan.ScanSettings; */ //import rx.Observable; //import rx.Scheduler; //import rx.Subscription; //import rx.schedulers.Schedulers; /** * OB1 G5/G6 collector * Created by jamorham on 16/09/2017. * * App version is master, best to avoid editing wear version directly * */ public class Ob1G5CollectionService extends G5BaseService { public static final String TAG = Ob1G5CollectionService.class.getSimpleName(); public static final String OB1G5_PREFS = "use_ob1_g5_collector_service"; private static final String OB1G5_MACSTORE = "G5-mac-for-txid-"; private static final String OB1G5_STATESTORE = "ob1-state-store-"; private static final String OB1G5_STATESTORE_TIME = "ob1-state-store-time"; private static final int DEFAULT_AUTOMATA_DELAY = 100; private static final String BUGGY_SAMSUNG_ENABLED = "buggy-samsung-enabled"; private static final String STOP_SCAN_TASK_ID = "ob1-g5-scan-timeout_scan"; private static volatile STATE state = INIT; private static volatile STATE last_automata_state = CLOSED; private static RxBleClient rxBleClient; private static volatile PendingIntent pendingIntent; private static String transmitterID; private static volatile String transmitterMAC; private static volatile String historicalTransmitterMAC; private static String transmitterIDmatchingMAC; private static String lastScanError = null; public static String lastSensorStatus = null; public static CalibrationState lastSensorState = null; public static volatile long lastUsableGlucosePacketTime = 0; private static volatile String static_connection_state = null; private static long static_last_connected = 0; @Setter private static long last_transmitter_timestamp = 0; private static long lastStateUpdated = 0; private static long wakeup_time = 0; private static long wakeup_jitter = 0; private static long max_wakeup_jitter = 0; private static volatile long connecting_time = 0; public static boolean keep_running = true; public static boolean android_wear = false; public static boolean wear_broadcast = false; private Subscription scanSubscription; private Subscription connectionSubscription; private static volatile Subscription stateSubscription; private Subscription discoverSubscription; private RxBleDevice bleDevice; private RxBleConnection connection; private PowerManager.WakeLock connection_linger; private volatile PowerManager.WakeLock scanWakeLock; private volatile PowerManager.WakeLock floatingWakeLock; private PowerManager.WakeLock fullWakeLock; private volatile boolean background_launch_waiting = false; private static long last_scan_started = -1; private static volatile long last_connect_started = -1; private static int error_count = 0; private static int connectNowFailures = 0; private static int connectFailures = 0; private static int scanTimeouts = 0; private static boolean lastConnectFailed = false; private static boolean preScanFailureMarker = false; private static boolean auth_succeeded = false; private int error_backoff_ms = 1000; private static final int max_error_backoff_ms = 10000; private static final long TOLERABLE_JITTER = 10000; private static final boolean d = false; private static boolean use_auto_connect = false; private static volatile boolean minimize_scanning = false; // set by preference private static volatile boolean always_scan = false; private static volatile boolean scan_next_run = true; private static boolean always_discover = false; private static boolean always_connect = false; private static boolean do_discovery = true; private static final boolean do_auth = true; //private static boolean initiate_bonding = false; private static final Set<String> alwaysScanModels = Sets.newHashSet("SM-N910V", "G Watch"); private static final List<String> alwaysScanModelFamilies = Arrays.asList("SM-N910"); private static final Set<String> alwaysConnectModels = Sets.newHashSet("G Watch"); private static final Set<String> alwaysBuggyWakeupModels = Sets.newHashSet("Jelly-Pro", "SmartWatch 3"); // Internal process state tracking public enum STATE { INIT("Initializing"), SCAN("Scanning"), CONNECT("Waiting connect"), CONNECT_NOW("Power connect"), DISCOVER("Examining"), CHECK_AUTH("Checking Auth"), PREBOND("Bond Prepare"), BOND("Bonding"), RESET("Reseting"), GET_DATA("Getting Data"), CLOSE("Sleeping"), CLOSED("Deep Sleeping"); private String str; STATE(String custom) { this.str = custom; } STATE() { this.str = toString(); } public String getString() { return str; } } public void authResult(boolean good) { auth_succeeded = good; } private synchronized void backoff_automata() { background_automata(error_backoff_ms); if (error_backoff_ms < max_error_backoff_ms) error_backoff_ms += 100; } public void background_automata() { background_automata(DEFAULT_AUTOMATA_DELAY); } public synchronized void background_automata(final int timeout) { if (background_launch_waiting) { UserError.Log.d(TAG, "Blocked by existing background automata pending"); return; } final PowerManager.WakeLock wl = JoH.getWakeLock("jam-g5-background", timeout + 5000); background_launch_waiting = true; new Thread(() -> { JoH.threadSleep(timeout); background_launch_waiting = false; automata(); JoH.releaseWakeLock(wl); }).start(); } private synchronized void automata() { if ((last_automata_state != state) || state == INIT || (JoH.ratelimit("jam-g5-dupe-auto", 2))) { last_automata_state = state; final PowerManager.WakeLock wl = JoH.getWakeLock("jam-g5-automata", 60000); try { switch (state) { case INIT: initialize(); break; case SCAN: // no connection? lets try a restart if (JoH.msSince(static_last_connected) > 30 * 60 * 1000) { if (JoH.pratelimit("ob1-collector-restart", 1200)) { CollectionServiceStarter.restartCollectionServiceBackground(); break; } } // TODO check if we know mac!!! Sync as part of wear sync?? - TODO preload transmitter mac?? if (useMinimizeScanningStrategy()) { UserError.Log.d(TAG, "Skipping Scanning! : Changing state due to minimize_scanning flags"); changeState(CONNECT_NOW); } else { scan_for_device(); } break; case CONNECT_NOW: connect_to_device(false); break; case CONNECT: connect_to_device(true); break; case DISCOVER: if (wear_broadcast && usingAlt()) { msg("Pausing"); UserError.Log.d(TAG, "Pausing for alt: "); JoH.threadSleep(1000); } if (do_discovery) { discover_services(); } else { UserError.Log.d(TAG, "Skipping discovery"); changeState(STATE.CHECK_AUTH); } break; case CHECK_AUTH: if (do_auth) { final PowerManager.WakeLock linger_wl_connect = JoH.getWakeLock("jam-g5-check-linger", 6000); if (!Ob1G5StateMachine.doCheckAuth(this, connection)) resetState(); } else { UserError.Log.d(TAG, "Skipping authentication"); changeState(GET_DATA); } break; case PREBOND: if (!getInitiateBondingFlag()) { final PowerManager.WakeLock linger_wl_prebond = JoH.getWakeLock("jam-g5-prebond-linger", 16000); if (!Ob1G5StateMachine.doKeepAliveAndBondRequest(this, connection)) resetState(); } else { UserError.Log.d(TAG, "Going directly to initiate bond"); changeState(BOND); } break; case BOND: if (getInitiateBondingFlag()) { UserError.Log.d(TAG, "State bond attempting to create bond"); } else { UserError.Log.d(TAG, "State bond currently does nothing as setting disabled"); } create_bond(); break; case RESET: UserError.Log.d(TAG, "Entering hard reset state"); Ob1G5StateMachine.doReset(this, connection); break; case GET_DATA: if (hardResetTransmitterNow) { send_reset_command(); } else { final PowerManager.WakeLock linger_wl_get_data = JoH.getWakeLock("jam-g5-get-linger", 6000); if (!Ob1G5StateMachine.doGetData(this, connection)) resetState(); } break; case CLOSE: prepareToWakeup(); break; case CLOSED: handleWakeup(); break; } } finally { JoH.releaseWakeLock(wl); } } else { UserError.Log.d(TAG, "Ignoring duplicate automata state within 2 seconds: " + state); } } private boolean useMinimizeScanningStrategy() { tryLoadingSavedMAC(); final int modulo = (connectNowFailures + scanTimeouts) % 2; UserError.Log.d(TAG, "minimize: " + minimize_scanning + " mac: " + transmitterMAC + " lastfailed:" + lastConnectFailed + " nowfail:" + connectNowFailures + " stimeout:" + scanTimeouts + " modulo:" + modulo); final boolean wholeHouse = WholeHouse.isLive(); boolean alwaysMinimize = false; if (wholeHouse) { estimateAnticipateFromLinkedData(); alwaysMinimize = !preScanFailureMarker; } return minimize_scanning && transmitterMAC != null && (!lastConnectFailed || (modulo == 1) || alwaysMinimize) && (DexSyncKeeper.isReady(transmitterID)); } private void estimateAnticipateFromLinkedData() { final BgReading bg = BgReading.last(); if (bg != null && bg.timestamp > static_last_connected && JoH.msSince(bg.timestamp) < HOUR_IN_MS * 3) { final long ts = bg.timestamp - Constants.SECOND_IN_MS; UserError.Log.d(TAG, "Updating Sync Keeper with network learned timestamp: " + JoH.dateTimeText(ts)); DexSyncKeeper.store(transmitterID, ts); } } private void resetState() { UserError.Log.e(TAG, "Resetting sequence state to INIT"); changeState(INIT); } public STATE getState() { return state; } public void changeState(STATE new_state) { changeState(new_state, DEFAULT_AUTOMATA_DELAY); } public void changeState(STATE new_state, int timeout) { if ((state == CLOSED || state == CLOSE) && new_state == CLOSE) { UserError.Log.d(TAG, "Not closing as already closed"); } else { UserError.Log.d(TAG, "Changing state from: " + state + " to " + new_state); state = new_state; if (android_wear && wear_broadcast) { msg(new_state.toString()); } background_automata(timeout); } } private synchronized void initialize() { if (state == INIT) { msg("Initializing"); static_connection_state = null; if (rxBleClient == null) { //rxBleClient = RxBleClient.create(xdrip.getAppContext()); rxBleClient = RxBleProvider.getSingleton(); } init_tx_id(); // load prefs etc changeState(SCAN); } else { UserError.Log.wtf(TAG, "Attempt to initialize when not in INIT state"); } } private static void init_tx_id() { transmitterID = Pref.getString("dex_txid", "NULL"); } private synchronized void scan_for_device() { if (state == SCAN) { msg(gs(R.string.scanning)); stopScan(); tryLoadingSavedMAC(); // did we already find it? if (always_scan || scan_next_run || (transmitterMAC == null) || (!transmitterID.equals(transmitterIDmatchingMAC)) || (static_last_timestamp < 1)) { scan_next_run = false; // reset if set transmitterMAC = null; // reset if set last_scan_started = JoH.tsl(); scanWakeLock = JoH.getWakeLock("xdrip-jam-g5-scan", (int) Constants.MINUTE_IN_MS * 7); historicalTransmitterMAC = PersistentStore.getString(OB1G5_MACSTORE + transmitterID); // "" if unset ScanFilter filter; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && historicalTransmitterMAC.length() > 5) { filter = new ScanFilter.Builder() .setDeviceAddress(historicalTransmitterMAC) .build(); } else { final String localTransmitterID = transmitterID; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && localTransmitterID != null && localTransmitterID.length() > 4) { filter = new ScanFilter.Builder().setDeviceName(getTransmitterBluetoothName()).build(); } else { filter = new ScanFilter.Builder().build(); } } scanSubscription = new Subscription(rxBleClient.scanBleDevices( new ScanSettings.Builder() //.setScanMode(static_last_timestamp < 1 ? ScanSettings.SCAN_MODE_LOW_LATENCY : ScanSettings.SCAN_MODE_BALANCED) //.setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH) .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) // TODO revisit scan mode .setScanMode(android_wear ? ScanSettings.SCAN_MODE_BALANCED : minimize_scanning ? ScanSettings.SCAN_MODE_BALANCED : ScanSettings.SCAN_MODE_LOW_LATENCY) // .setScanMode(ScanSettings.SCAN_MODE_BALANCED) .build(), // scan filter doesn't work reliable on android sdk 23+ filter ) // observe on? // do unsubscribe? //.doOnUnsubscribe(this::clearSubscription) .subscribeOn(Schedulers.io()) .subscribe(this::onScanResult, this::onScanFailure)); if (minimize_scanning) { // Must be less than fail over timeout Inevitable.task(STOP_SCAN_TASK_ID, 320 * Constants.SECOND_IN_MS, this::stopScanWithTimeoutAndReschedule); } UserError.Log.d(TAG, "Scanning for: " + getTransmitterBluetoothName()); } else { UserError.Log.d(TAG, "Transmitter mac already known: " + transmitterMAC); changeState(CONNECT); } } else { UserError.Log.wtf(TAG, "Attempt to scan when not in SCAN state"); } } private void stopScanWithTimeoutAndReschedule() { stopScan(); UserError.Log.d(TAG, "Stopped scan due to timeout at: " + JoH.dateTimeText(JoH.tsl())); scanTimeouts++; tryLoadingSavedMAC(); prepareToWakeup(); } private synchronized void connect_to_device(boolean auto) { if ((state == CONNECT) || (state == CONNECT_NOW)) { // TODO check mac if (transmitterMAC == null) { tryLoadingSavedMAC(); } final String localTransmitterMAC = transmitterMAC; if (localTransmitterMAC != null) { msg("Connect request"); if (state == CONNECT_NOW) { if (connection_linger != null) JoH.releaseWakeLock(connection_linger); connection_linger = JoH.getWakeLock("jam-g5-pconnect", 60000); } if (d) UserError.Log.d(TAG, "Local bonding state: " + (isDeviceLocallyBonded() ? "BONDED" : "NOT Bonded")); stopConnect(); try { bleDevice = rxBleClient.getBleDevice(localTransmitterMAC); /// / Listen for connection state changes stateSubscription = new Subscription(bleDevice.observeConnectionStateChanges() // .observeOn(AndroidSchedulers.mainThread()) .subscribeOn(Schedulers.io()) .subscribe(this::onConnectionStateChange, throwable -> { UserError.Log.wtf(TAG, "Got Error from state subscription: " + throwable); })); last_connect_started = JoH.tsl(); // Attempt to establish a connection // TODO does this need different connection timeout for auto vs normal? connectionSubscription = new Subscription(bleDevice.establishConnection(auto) .timeout(7, TimeUnit.MINUTES) // .flatMap(RxBleConnection::discoverServices) // .observeOn(AndroidSchedulers.mainThread()) // .doOnUnsubscribe(this::clearSubscription) .subscribeOn(Schedulers.io()) .subscribe(this::onConnectionReceived, this::onConnectionFailure)); } catch (IllegalArgumentException e) { UserError.Log.e(TAG, "Caught IllegalArgument Exception: " + e + " retry on next run"); // TODO if this is due to concurrent access then changing state again may be a bad idea state = SCAN; backoff_automata(); // note backoff } } else { UserError.Log.wtf(TAG, "No transmitter mac address!"); state = SCAN; backoff_automata(); // note backoff } } else { UserError.Log.wtf(TAG, "Attempt to connect when not in CONNECT state"); } } private synchronized void discover_services() { if (state == DISCOVER) { if (connection != null) { if (d) UserError.Log.d(TAG, "Local bonding state: " + (isDeviceLocallyBonded() ? "BONDED" : "NOT Bonded")); stopDiscover(); discoverSubscription = new Subscription(connection.discoverServices(10, TimeUnit.SECONDS).subscribe(this::onServicesDiscovered, this::onDiscoverFailed)); } else { UserError.Log.e(TAG, "No connection when in DISCOVER state - reset"); state = INIT; background_automata(); } } else { UserError.Log.wtf(TAG, "Attempt to discover when not in DISCOVER state"); } } @TargetApi(Build.VERSION_CODES.KITKAT) private synchronized void create_bond() { if (state == BOND) { try { msg("Bonding"); do_create_bond(); //state = STATE.CONNECT_NOW; //background_automata(15000); } catch (Exception e) { UserError.Log.wtf(TAG, "Exception creating bond: " + e); } } else { UserError.Log.wtf(TAG, "Attempt to bond when not in BOND state"); } } public synchronized void reset_bond(boolean allow) { if (allow || (JoH.pratelimit("ob1-bond-cycle", 7200))) { UserError.Log.e(TAG, "Attempting to refresh bond state"); msg("Resetting Bond"); unBond(); do_create_bond(); } } private synchronized void do_create_bond() { final boolean isDeviceLocallyBonded = isDeviceLocallyBonded(); UserError.Log.d(TAG, "Attempting to create bond, device is : " + (isDeviceLocallyBonded ? "BONDED" : "NOT Bonded")); if (isDeviceLocallyBonded && getInitiateBondingFlag()) { UserError.Log.d(TAG, "Device is marked as bonded but we are being asked to bond so attempting to unbond first"); unbondIfAllowed(); changeState(CLOSE); } else { try { Ob1G5StateMachine.doKeepAlive(this, connection, () -> { weInitiatedBondConfirmation = 1; instantCreateBondIfAllowed(); }); //background_automata(10000); } catch (Exception e) { UserError.Log.wtf(TAG, "Got exception in do_create_bond() " + e); } } } private synchronized void send_reset_command() { hardResetTransmitterNow = false; getBatteryStatusNow = true; if (JoH.ratelimit("reset-command", 1200)) { UserError.Log.e(TAG, "Issuing reset command!"); changeState(STATE.RESET); } else { UserError.Log.e(TAG, "Reset command blocked by 20 minute timer"); } } private String getTransmitterBluetoothName() { final String transmitterIdLastTwo = getLastTwoCharacters(transmitterID); // todo check for bad config return "Dexcom" + transmitterIdLastTwo; } public static String getMac() { return transmitterMAC; } private void tryLoadingSavedMAC() { if ((transmitterMAC == null) || (!transmitterIDmatchingMAC.equals(transmitterID))) { if (transmitterID != null) { final String this_mac = PersistentStore.getString(OB1G5_MACSTORE + transmitterID); if (this_mac.length() == 17) { UserError.Log.d(TAG, "Loaded stored MAC for: " + transmitterID + " " + this_mac); transmitterMAC = this_mac; transmitterIDmatchingMAC = transmitterID; } else { UserError.Log.d(TAG, "Did not find any saved MAC for: " + transmitterID); } } else { UserError.Log.e(TAG, "Could not load saved mac as transmitter id isn't set!"); } } else { UserError.Log.d(TAG, "MAC for transmitter id already populated: " + transmitterID + " " + transmitterMAC); } } // should this service be running? Used to decide when to shut down private static boolean shouldServiceRun() { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return false; if (!Pref.getBooleanDefaultFalse(OB1G5_PREFS)) return false; if (!(DexCollectionType.getDexCollectionType() == DexcomG5)) return false; if (!android_wear) { if (Home.get_forced_wear()) { if (JoH.quietratelimit("forced-wear-notice", 3)) UserError.Log.d(TAG, "Not running due to forced wear"); return false; } } else { // android wear code if (!PersistentStore.getBoolean(CollectionServiceStarter.pref_run_wear_collector)) return false; } return true; } // check required permissions and warn the user if they are wrong private static void checkPermissions() { } private static synchronized boolean isDeviceLocallyBonded() { if (transmitterMAC == null) return false; final Set<RxBleDevice> pairedDevices = rxBleClient.getBondedDevices(); if ((pairedDevices != null) && (pairedDevices.size() > 0)) { for (RxBleDevice device : pairedDevices) { if ((device.getMacAddress() != null) && (device.getMacAddress().equals(transmitterMAC))) { return true; } } } return false; } private synchronized void checkAndEnableBT() { try { if (Pref.getBoolean("automatically_turn_bluetooth_on", true)) { final BluetoothAdapter mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter(); if (!mBluetoothAdapter.isEnabled()) { if (JoH.ratelimit("g5-enabling-bluetooth", 30)) { JoH.setBluetoothEnabled(this, true); UserError.Log.e(TAG, "Enabling bluetooth"); } } } } catch (Exception e) { UserError.Log.e(TAG, "Got exception checking BT: " + e); } } public synchronized void unBond() { UserError.Log.d(TAG, "unBond() start"); if (transmitterMAC == null) return; final BluetoothAdapter mBluetoothAdapter = ((BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE)).getAdapter(); final Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); if (pairedDevices.size() > 0) { for (final BluetoothDevice device : pairedDevices) { if (device.getAddress() != null) { if (device.getAddress().equals(transmitterMAC)) { try { UserError.Log.e(TAG, "removingBond: " + transmitterMAC); final Method m = device.getClass().getMethod("removeBond", (Class[]) null); m.invoke(device, (Object[]) null); // TODO interpret boolean response break; } catch (Exception e) { UserError.Log.e(TAG, e.getMessage(), e); } } } } } UserError.Log.d(TAG, "unBond() finished"); } public static String getTransmitterID() { if (transmitterID == null) { init_tx_id(); } return transmitterID; } private void handleWakeup() { if (always_scan) { UserError.Log.d(TAG, "Always scan mode"); changeState(SCAN); } else { if (connectFailures > 0 || (!use_auto_connect && connectNowFailures > 0)) { always_scan = true; UserError.Log.e(TAG, "Switching to scan always mode due to connect failures metric: " + connectFailures); changeState(SCAN); } else if (use_auto_connect && (connectNowFailures > 1) && (connectFailures < 0)) { UserError.Log.d(TAG, "Avoiding power connect due to failure metric: " + connectNowFailures + " " + connectFailures); changeState(CONNECT); } else { changeState(CONNECT_NOW); } } } private synchronized void prepareToWakeup() { if (JoH.ratelimit("g5-wakeup-timer", 5)) { final long when = DexSyncKeeper.anticipate(transmitterID); if (when > 0) { final long when_offset = when - JoH.tsl(); UserError.Log.d(TAG, "(" + JoH.dateTimeText(JoH.tsl()) + ") Wake up time anticipated at: " + JoH.dateTimeText(when)); scheduleWakeUp(when_offset - Constants.SECOND_IN_MS * 15, "anticipate"); } else { scheduleWakeUp(Constants.SECOND_IN_MS * 285, "anticipate"); } } if ((android_wear && wakeup_jitter > TOLERABLE_JITTER) || always_connect) { // TODO should be max_wakeup_jitter perhaps or set always_connect flag UserError.Log.d(TAG, "Not stopping connect due to " + (always_connect ? "always_connect flag" : "unreliable wake up")); state = CONNECT; background_automata(6000); } else { state = CLOSED; // Don't poll automata as we want to do this on waking stopConnect(); } } private void scheduleWakeUp(long future, final String info) { if (future <= 0) future = 5000; UserError.Log.d(TAG, "Scheduling wakeup @ " + JoH.dateTimeText(JoH.tsl() + future) + " (" + info + ")"); if (pendingIntent == null) //pendingIntent = PendingIntent.getService(this, 0, new Intent(this, this.getClass()), 0); pendingIntent = WakeLockTrampoline.getPendingIntent(this.getClass()); wakeup_time = JoH.tsl() + future; JoH.wakeUpIntent(this, future, pendingIntent); } public void incrementErrors() { error_count++; if (error_count > 1) { UserError.Log.e(TAG, "Error count reached: " + error_count); } } public void clearErrors() { error_count = 0; } private void checkAlwaysScanModels() { final String this_model = Build.MODEL; UserError.Log.d(TAG, "Checking model: " + this_model); if ((JoH.isSamsung() && PersistentStore.getLong(BUGGY_SAMSUNG_ENABLED) > 4)) { UserError.Log.d(TAG, "Enabling buggy samsung due to persistent metric"); JoH.buggy_samsung = true; } always_connect = alwaysConnectModels.contains(this_model); if (alwaysBuggyWakeupModels.contains(this_model)) { UserError.Log.e(TAG, "Always buggy wakeup exact match for " + this_model); JoH.buggy_samsung = true; } if (alwaysScanModels.contains(this_model)) { UserError.Log.e(TAG, "Always scan model exact match for: " + this_model); always_scan = true; return; } for (String check : alwaysScanModelFamilies) { if (this_model.startsWith(check)) { UserError.Log.e(TAG, "Always scan model fuzzy match for: " + this_model); always_scan = true; return; } } } @Override public void onCreate() { super.onCreate(); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { UserError.Log.wtf(TAG, "Not high enough Android version to run: " + Build.VERSION.SDK_INT); } else { try { registerReceiver(mBondStateReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); } catch (Exception e) { UserError.Log.e(TAG, "Could not register bond state receiver: " + e); } final IntentFilter pairingRequestFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); pairingRequestFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); try { registerReceiver(mPairingRequestRecevier, pairingRequestFilter); } catch (Exception e) { UserError.Log.e(TAG, "Could not register pairing request receiver:" + e); } checkAlwaysScanModels(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH) { android_wear = (getResources().getConfiguration().uiMode & Configuration.UI_MODE_TYPE_MASK) == Configuration.UI_MODE_TYPE_WATCH; if (android_wear) { UserError.Log.d(TAG, "We are running on Android Wear"); wear_broadcast = Pref.getBooleanDefaultFalse("ob1_wear_broadcast"); } } } if (d) RxBleClient.setLogLevel(RxBleLog.DEBUG); addErrorHandler(TAG); listenForChangeInSettings(true); } @Override public int onStartCommand(Intent intent, int flags, int startId) { xdrip.checkAppContext(getApplicationContext()); final PowerManager.WakeLock wl = JoH.getWakeLock("g5-start-service", 310000); try { UserError.Log.d(TAG, "WAKE UP WAKE UP WAKE UP WAKE UP @ " + JoH.dateTimeText(JoH.tsl())); msg("Wake up"); if (wakeup_time > 0) { wakeup_jitter = JoH.msSince(wakeup_time); if (wakeup_jitter < 0) { UserError.Log.d(TAG, "Woke up Early.."); } else { if (wakeup_jitter > 1000) { UserError.Log.d(TAG, "Wake up, time jitter: " + JoH.niceTimeScalar(wakeup_jitter)); if ((wakeup_jitter > TOLERABLE_JITTER) && (!JoH.buggy_samsung) && JoH.isSamsung()) { UserError.Log.wtf(TAG, "Enabled Buggy Samsung workaround due to jitter of: " + JoH.niceTimeScalar(wakeup_jitter)); JoH.buggy_samsung = true; PersistentStore.incrementLong(BUGGY_SAMSUNG_ENABLED); max_wakeup_jitter = 0; } else { max_wakeup_jitter = Math.max(max_wakeup_jitter, wakeup_jitter); } } } } if (!shouldServiceRun()) { UserError.Log.d(TAG, "Stopping service due to shouldServiceRun() result"); msg("Stopping"); stopSelf(); return START_NOT_STICKY; } scheduleWakeUp(Constants.MINUTE_IN_MS * 6, "fail-over"); if ((state == BOND) || (state == PREBOND) || (state == DISCOVER) || (state == CONNECT)) state = SCAN; checkAndEnableBT(); Ob1G5StateMachine.restoreQueue(); if (JoH.quietratelimit("evaluateG6Settings", 600)) { evaluateG6Settings(); } minimize_scanning = Pref.getBooleanDefaultFalse("ob1_minimize_scanning"); automata(); // sequence logic UserError.Log.d(TAG, "Releasing service start"); return START_STICKY; } finally { JoH.releaseWakeLock(wl); } } @Override public void onDestroy() { msg("Shutting down"); if (pendingIntent != null) { JoH.cancelAlarm(this, pendingIntent); pendingIntent = null; wakeup_time = 0; } stopScan(); stopDiscover(); stopConnect(); scanSubscription = null; connectionSubscription = null; stateSubscription = null; discoverSubscription = null; listenForChangeInSettings(false); unregisterPairingReceiver(); try { unregisterReceiver(mBondStateReceiver); } catch (Exception e) { UserError.Log.e(TAG, "Got exception unregistering pairing receiver: " + e); } state = INIT; // Should be STATE.END ? last_automata_state = CLOSED; msg("Service Stopped"); super.onDestroy(); } public void unregisterPairingReceiver() { try { unregisterReceiver(mPairingRequestRecevier); } catch (Exception e) { UserError.Log.e(TAG, "Got exception unregistering pairing receiver: " + e); } } private synchronized void stopScan() { if (scanSubscription != null) { scanSubscription.unsubscribe(); } UserError.Log.d(TAG, "DEBUG: killing stop scan task"); Inevitable.kill(STOP_SCAN_TASK_ID); if (scanWakeLock != null) { JoH.releaseWakeLock(scanWakeLock); } } private synchronized void stopConnect() { if (connectionSubscription != null) { connectionSubscription.unsubscribe(); } if (stateSubscription != null) { stateSubscription.unsubscribe(); } } private synchronized void stopDiscover() { if (discoverSubscription != null) { discoverSubscription.unsubscribe(); } } private boolean isScanMatch(final String this_address, final String historical_address, final String this_name, final String search_name) { boolean result = this_address.equalsIgnoreCase(historical_address) || (this_name != null && this_name.equalsIgnoreCase(search_name)); if (result) { if (historical_address.length() == this_address.length() && !this_address.equalsIgnoreCase(historical_address)) { if (JoH.ratelimit("ob1-address-change-error", 30)) { UserError.Log.wtf(TAG, "Bluetooth device: " + search_name + " apparently changed from mac: " + historical_address + " to: " + this_address + " :: There appears to be confusion between devices - ignoring this scan result"); } result = false; } } return result; } // Successful result from our bluetooth scan private synchronized void onScanResult(ScanResult bleScanResult) { // TODO MIN RSSI final int this_rssi = bleScanResult.getRssi(); final String this_name = bleScanResult.getBleDevice().getName(); final String this_address = bleScanResult.getBleDevice().getMacAddress(); final String search_name = getTransmitterBluetoothName(); if (isScanMatch(this_address, historicalTransmitterMAC, this_name, search_name)) { stopScan(); // we got one! last_scan_started = 0; // clear scanning for time lastScanError = null; // error should be cleared UserError.Log.d(TAG, "Got scan result match: " + bleScanResult.getBleDevice().getName() + " " + this_address + " rssi: " + this_rssi); transmitterMAC = bleScanResult.getBleDevice().getMacAddress(); transmitterIDmatchingMAC = transmitterID; PersistentStore.setString(OB1G5_MACSTORE + transmitterID, transmitterMAC); //if (JoH.ratelimit("ob1-g5-scan-to-connect-transition", 3)) { if (state == SCAN) { // if (always_scan) { changeState(CONNECT_NOW, 500); // } else { // changeState(STATE.CONNECT); // } } else { UserError.Log.e(TAG, "Skipping apparent duplicate connect transition, current state = " + state); } } else { String this_mac = bleScanResult.getBleDevice().getMacAddress(); if (this_mac == null) this_mac = "NULL"; if (JoH.quietratelimit("bt-obi1-null-match" + this_mac, 15)) { UserError.Log.d(TAG, "Bluetooth scanned device doesn't match (" + search_name + ") found: " + this_name + " " + bleScanResult.getBleDevice().getMacAddress()); } } } // Failed result from our bluetooth scan private synchronized void onScanFailure(Throwable throwable) { if (throwable instanceof BleScanException) { final String info = handleBleScanException((BleScanException) throwable); lastScanError = info; UserError.Log.d(TAG, info); if (((BleScanException) throwable).getReason() == BleScanException.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED) { if (JoH.ratelimit("bluetooth-cannot-register", 120)) { if (Pref.getBooleanDefaultFalse("automatically_turn_bluetooth_on")) { UserError.Log.wtf(TAG, "Android bluetooth appears broken - attempting to turn off and on"); JoH.niceRestartBluetooth(xdrip.getAppContext()); } else { UserError.Log.e(TAG, "Cannot reset bluetooth due to preference being disabled"); } } } if (((BleScanException) throwable).getReason() == BleScanException.BLUETOOTH_DISABLED) { // Attempt to turn bluetooth on if (JoH.ratelimit("bluetooth_toggle_on", 30)) { UserError.Log.d(TAG, "Pause before Turn Bluetooth on"); try { Thread.sleep(2000); } catch (InterruptedException e) { // } if (WholeHouse.isRpi()) { UserError.Log.e(TAG,"Trying to turn off/on bluetooth"); JoH.niceRestartBluetooth(xdrip.getAppContext()); } else { UserError.Log.e(TAG, "Trying to Turn Bluetooth on"); JoH.setBluetoothEnabled(xdrip.getAppContext(), true); } } } // TODO count scan duration stopScan(); // Note that this is not reached on timeout only failure if (minimize_scanning) { UserError.Log.d(TAG, "Preparing to wake up at next expected time"); prepareToWakeup(); } else { backoff_automata(); } } } private void unbondIfAllowed() { if (Pref.getBoolean("ob1_g5_allow_resetbond", true)) { unBond(); } else { UserError.Log.e(TAG, "Would have tried to unpair but preference setting prevents it. (unbond)"); } } private boolean resetBondIfAllowed(boolean force) { if (Pref.getBoolean("ob1_g5_allow_resetbond", true)) { reset_bond(force); return true; } else { UserError.Log.e(TAG, "Would have tried to unpair but preference setting prevents it. (resetbond)"); return false; } } // Connection has been terminated or failed // - quite normal when device switches to sleep between readings private void onConnectionFailure(final Throwable throwable) { // msg("Connection failure"); // TODO under what circumstances should we change state or do something here? UserError.Log.d(TAG, "Connection Disconnected/Failed: " + throwable); if (state == DISCOVER) { // possible encryption failure if (!resetBondIfAllowed(false) && android_wear) { UserError.Log.d(TAG, "Trying alternate reconnection strategy"); changeState(CONNECT_NOW); } return; } if (state == CONNECT_NOW) { connectNowFailures++; lastConnectFailed = true; if (throwable instanceof BleGattCallbackTimeoutException) { if (throwable.getMessage().contains("BleGattOperation{description='CONNECTION_STATE'")) { UserError.Log.d(TAG, "Setting pre-scan failure marker enabled due to exception type"); preScanFailureMarker = true; } } else if (JoH.msSince(last_connect_started) < Constants.SECOND_IN_MS) { UserError.Log.d(TAG, "Setting pre-scan failure marker enabled due to exception timing"); preScanFailureMarker = true; } UserError.Log.d(TAG, "Connect Now failures incremented to: " + connectNowFailures); if (minimize_scanning && DexSyncKeeper.anticipate(transmitterID) > 0) { // TODO under what circumstances does this CLOSE state not get executed? and anticipate not scheduled? changeState(CLOSE); } else { changeState(CONNECT); } return; } if (state == CONNECT) { connectFailures++; lastConnectFailed = true; // TODO check bluetooth on or in connect section if (JoH.ratelimit("ob1-restart-scan-on-connect-failure", 10)) { UserError.Log.d(TAG, "Restarting scan due to connect failure"); tryGattRefresh(); changeState(SCAN); } } } public void tryGattRefresh() { if (JoH.ratelimit("ob1-gatt-refresh", 60)) { if (Pref.getBoolean("use_gatt_refresh", true)) { try { if (connection != null) UserError.Log.d(TAG, "Trying gatt refresh queue"); connection.queue((new GattRefreshOperation(0))).timeout(2, TimeUnit.SECONDS).subscribe( readValue -> { UserError.Log.d(TAG, "Refresh OK: " + readValue); }, throwable -> { UserError.Log.d(TAG, "Refresh exception: " + throwable); }); } catch (NullPointerException e) { UserError.Log.d(TAG, "Probably harmless gatt refresh exception: " + e); } catch (Exception e) { UserError.Log.d(TAG, "Got exception trying gatt refresh: " + e); } } else { UserError.Log.d(TAG, "Gatt refresh rate limited"); } } } // We have connected to the device! private void onConnectionReceived(RxBleConnection this_connection) { msg("Connected"); static_last_connected = JoH.tsl(); lastConnectFailed = false; preScanFailureMarker = false; DexSyncKeeper.store(transmitterID, static_last_connected); // TODO check connection already exists - close etc? if (connection_linger != null) JoH.releaseWakeLock(connection_linger); connection = this_connection; if (state == CONNECT_NOW) { connectNowFailures = -3; // mark good } if (state == CONNECT) { connectFailures = -1; // mark good } scanTimeouts = 0; // reset counter if (JoH.ratelimit("g5-to-discover", 1)) { changeState(DISCOVER); } } private synchronized void onConnectionStateChange(RxBleConnection.RxBleConnectionState newState) { String connection_state = "Unknown"; switch (newState) { case CONNECTING: connection_state = "Connecting"; connecting_time = JoH.tsl(); break; case CONNECTED: connection_state = "Connected"; JoH.releaseWakeLock(floatingWakeLock); floatingWakeLock = JoH.getWakeLock("floating-connected", 40000); final long since_connecting = JoH.msSince(connecting_time); if ((connecting_time > static_last_timestamp) && (since_connecting > Constants.SECOND_IN_MS * 310) && (since_connecting < Constants.SECOND_IN_MS * 620)) { if (!always_scan) { UserError.Log.e(TAG, "Connection time shows missed reading, switching to always scan, metric: " + JoH.niceTimeScalar(since_connecting)); always_scan = true; } else { UserError.Log.e(TAG, "Connection time shows missed reading, despite always scan, metric: " + JoH.niceTimeScalar(since_connecting)); } } break; case DISCONNECTING: connection_state = "Disconnecting"; break; case DISCONNECTED: connection_state = "Disconnected"; JoH.releaseWakeLock(floatingWakeLock); break; } static_connection_state = connection_state; UserError.Log.d(TAG, "Bluetooth connection: " + static_connection_state); if (connection_state.equals("Disconnecting")) { //tryGattRefresh(); } } public void connectionStateChange(String connection_state) { static_connection_state = connection_state; if (connection_state.equals(CLOSED_OK_TEXT)) { JoH.releaseWakeLock(floatingWakeLock); } } private void onServicesDiscovered(RxBleDeviceServices services) { for (BluetoothGattService service : services.getBluetoothGattServices()) { if (d) UserError.Log.d(TAG, "Service: " + getUUIDName(service.getUuid())); if (service.getUuid().equals(BluetoothServices.CGMService)) { if (d) UserError.Log.i(TAG, "Found CGM Service!"); if (!always_discover) { do_discovery = false; } changeState(STATE.CHECK_AUTH); return; } } UserError.Log.e(TAG, "Could not locate CGM service during discovery"); incrementErrors(); } private void onDiscoverFailed(Throwable throwable) { UserError.Log.e(TAG, "Discover failure: " + throwable.toString()); incrementErrors(); prepareToWakeup(); } private void clearSubscription() { scanSubscription = null; } private boolean g5BluetoothWatchdog() { return Pref.getBoolean("g5_bluetooth_watchdog", true); } public static void updateLast(long timestamp) { if ((static_last_timestamp == 0) && (transmitterID != null)) { final String ref = "last-ob1-data-" + transmitterID; if (PersistentStore.getLong(ref) == 0) { PersistentStore.setLong(ref, timestamp); if (!android_wear) JoH.playResourceAudio(R.raw.labbed_musical_chime); } } static_last_timestamp = timestamp; } private String handleBleScanException(BleScanException bleScanException) { final String text; switch (bleScanException.getReason()) { case BleScanException.BLUETOOTH_NOT_AVAILABLE: text = "Bluetooth is not available"; break; case BleScanException.BLUETOOTH_DISABLED: text = "Enable bluetooth and try again"; break; case BleScanException.LOCATION_PERMISSION_MISSING: text = "On Android 6.0+ location permission is required. Implement Runtime Permissions"; break; case BleScanException.LOCATION_SERVICES_DISABLED: text = "Location services needs to be enabled on Android 6.0+"; break; case BleScanException.SCAN_FAILED_ALREADY_STARTED: text = "Scan with the same filters is already started"; break; case BleScanException.SCAN_FAILED_APPLICATION_REGISTRATION_FAILED: text = "Failed to register application for bluetooth scan"; break; case BleScanException.SCAN_FAILED_FEATURE_UNSUPPORTED: text = "Scan with specified parameters is not supported"; break; case BleScanException.SCAN_FAILED_INTERNAL_ERROR: text = "Scan failed due to internal error"; break; case BleScanException.SCAN_FAILED_OUT_OF_HARDWARE_RESOURCES: text = "Scan cannot start due to limited hardware resources"; break; case BleScanException.UNDOCUMENTED_SCAN_THROTTLE: text = String.format( Locale.getDefault(), "Android 7+ does not allow more scans. Try in %d seconds", secondsTill(bleScanException.getRetryDateSuggestion()) ); break; case BleScanException.UNKNOWN_ERROR_CODE: case BleScanException.BLUETOOTH_CANNOT_START: default: text = "Unable to start scanning"; break; } UserError.Log.w(TAG, text + " " + bleScanException); return text; } private static class GattRefreshOperation implements RxBleCustomOperation<Void> { private long delay_ms = 500; GattRefreshOperation() { } GattRefreshOperation(long delay_ms) { this.delay_ms = delay_ms; } @NonNull @Override public Observable<Void> asObservable(BluetoothGatt bluetoothGatt, RxBleGattCallback rxBleGattCallback, Scheduler scheduler) throws Throwable { return Observable.fromCallable(() -> refreshDeviceCache(bluetoothGatt)) .delay(delay_ms, TimeUnit.MILLISECONDS, Schedulers.computation()) .subscribeOn(scheduler); } private Void refreshDeviceCache(final BluetoothGatt gatt) { UserError.Log.d(TAG, "Gatt Refresh " + (JoH.refreshDeviceCache(TAG, gatt) ? "succeeded" : "failed")); return null; } } private int currentBondState = 0; public volatile int waitingBondConfirmation = 0; // 0 = not waiting, 1 = waiting, 2 = received public volatile int weInitiatedBondConfirmation = 0; // 0 = not waiting, 1 = waiting, 2 = received final BroadcastReceiver mBondStateReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!keep_running) { try { UserError.Log.e(TAG, "Rogue bond state receiver still active - unregistering"); unregisterReceiver(mBondStateReceiver); } catch (Exception e) { // } return; } final String action = intent.getAction(); UserError.Log.d(TAG, "BondState: onReceive ACTION: " + action); if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { final BluetoothDevice parcel_device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); currentBondState = parcel_device.getBondState(); final int bond_state_extra = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, -1); final int previous_bond_state_extra = intent.getIntExtra(BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, -1); UserError.Log.e(TAG, "onReceive UPDATE Name " + parcel_device.getName() + " Value " + parcel_device.getAddress() + " Bond state " + parcel_device.getBondState() + bondState(parcel_device.getBondState()) + " " + "bs: " + bondState(bond_state_extra) + " was " + bondState(previous_bond_state_extra)); try { if (parcel_device.getAddress().equals(transmitterMAC)) { msg(bondState(bond_state_extra).replace(" ", "")); if (parcel_device.getBondState() == BluetoothDevice.BOND_BONDED) { if (waitingBondConfirmation == 1) { waitingBondConfirmation = 2; // received UserError.Log.e(TAG, "Bond confirmation received!"); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { UserError.Log.d(TAG, "Sleeping before create bond"); try { Thread.sleep(1000); } catch (InterruptedException e) { // } instantCreateBondIfAllowed(); } } if (weInitiatedBondConfirmation == 1) { weInitiatedBondConfirmation = 2; changeState(GET_DATA); } } } } catch (Exception e) { UserError.Log.e(TAG, "Got exception trying to process bonded confirmation: ", e); } } } }; public void instantCreateBondIfAllowed() { if (getInitiateBondingFlag()) { try { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { UserError.Log.d(TAG, "instantCreateBond() called"); bleDevice.getBluetoothDevice().createBond(); } } catch (Exception e) { UserError.Log.e(TAG, "Got exception in instantCreateBond() " + e); } } else { UserError.Log.e(TAG, "instantCreateBond blocked by lack of initiate_bonding flag"); } } private boolean getInitiateBondingFlag() { return Pref.getBoolean("ob1_initiate_bonding_flag", true); } private final BroadcastReceiver mPairingRequestRecevier = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { if (!keep_running) { try { UserError.Log.e(TAG, "Rogue pairing request receiver still active - unregistering"); unregisterReceiver(mPairingRequestRecevier); } catch (Exception e) { // } return; } if ((bleDevice != null) && (bleDevice.getBluetoothDevice().getAddress() != null)) { UserError.Log.e(TAG, "Processing mPairingRequestReceiver !!!"); JoH.releaseWakeLock(fullWakeLock); fullWakeLock = JoH.fullWakeLock("pairing-screen-wake", 30 * Constants.SECOND_IN_MS); if (!android_wear) Home.startHomeWithExtra(context, Home.HOME_FULL_WAKEUP, "1"); if (!JoH.doPairingRequest(context, this, intent, bleDevice.getBluetoothDevice().getAddress())) { if (!android_wear) { unregisterPairingReceiver(); UserError.Log.e(TAG, "Pairing failed so removing pairing automation"); // todo use flag } } } else { UserError.Log.e(TAG, "Received pairing request but device was null !!!"); } } }; private static long secondsTill(Date retryDateSuggestion) { return TimeUnit.MILLISECONDS.toSeconds(retryDateSuggestion.getTime() - System.currentTimeMillis()); } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } private static final String NEEDING_CALIBRATION = "G5_NEEDING_CALIBRATION"; private static final String IS_STARTED = "G5_IS_STARTED"; private static final String IS_FAILED = "G5_IS_FAILED"; public static void processCalibrationState(CalibrationState state) { if (state == CalibrationState.Unknown) { UserError.Log.d(TAG, "Not processing push of unknown state as this is the unset state"); return; } lastSensorStatus = state.getExtendedText(); lastSensorState = state; storeCalibrationState(state); final boolean needs_calibration = state.needsCalibration(); final boolean was_needing_calibration = PersistentStore.getBoolean(NEEDING_CALIBRATION); final boolean is_started = state.sensorStarted(); final boolean was_started = PersistentStore.getBoolean(IS_STARTED); final boolean is_failed = state.sensorFailed(); final boolean was_failed = PersistentStore.getBoolean(IS_FAILED); if (needs_calibration && !was_needing_calibration) { final Class c; switch (state) { case NeedsFirstCalibration: c = DoubleCalibrationActivity.class; break; default: c = AddCalibration.class; break; } Inevitable.task("ask initial calibration", SECOND_IN_MS * 30, () -> { final PendingIntent pi = PendingIntent.getActivity(xdrip.getAppContext(), G5_CALIBRATION_REQUEST, JoH.getStartActivityIntent(c), PendingIntent.FLAG_UPDATE_CURRENT); // pending intent not used on wear JoH.showNotification(state.getText(), "G5 Calibration Required", android_wear ? null : pi, G5_CALIBRATION_REQUEST, state == CalibrationState.NeedsFirstCalibration, true, false); }); } else if (!needs_calibration && was_needing_calibration) { JoH.cancelNotification(G5_CALIBRATION_REQUEST); } if (!is_started && was_started) { if (Pref.getBooleanDefaultFalse("ob1_g5_restart_sensor") && (Sensor.isActive())) { if (state.ended()) { UserError.Log.uel(TAG, "Requesting time-travel restart"); Ob1G5StateMachine.restartSensorWithTimeTravel(); } else { UserError.Log.uel(TAG, "Attempting to auto-start sensor"); Ob1G5StateMachine.startSensor(JoH.tsl()); } final PendingIntent pi = PendingIntent.getActivity(xdrip.getAppContext(), G5_SENSOR_RESTARTED, JoH.getStartActivityIntent(Home.class), PendingIntent.FLAG_UPDATE_CURRENT); JoH.showNotification("Auto Start", "Sensor Requesting Restart", pi, G5_SENSOR_RESTARTED, true, true, false); } final PendingIntent pi = PendingIntent.getActivity(xdrip.getAppContext(), G5_SENSOR_STARTED, JoH.getStartActivityIntent(Home.class), PendingIntent.FLAG_UPDATE_CURRENT); JoH.showNotification(state.getText(), "Sensor Stopped", pi, G5_SENSOR_STARTED, true, true, false); UserError.Log.ueh(TAG, "Native Sensor is now Stopped: " + state.getExtendedText()); } else if (is_started && !was_started) { JoH.cancelNotification(G5_SENSOR_STARTED); UserError.Log.ueh(TAG, "Native Sensor is now Started: " + state.getExtendedText()); } if (is_failed && !was_failed) { final PendingIntent pi = PendingIntent.getActivity(xdrip.getAppContext(), G5_SENSOR_FAILED, JoH.getStartActivityIntent(Home.class), PendingIntent.FLAG_UPDATE_CURRENT); JoH.showNotification(state.getText(), "G5 Sensor FAILED", pi, G5_SENSOR_FAILED, true, true, false); UserError.Log.ueh(TAG, "Native Sensor is now marked FAILED: " + state.getExtendedText()); } // we can't easily auto-cancel a failed notice as auto-restart may mean the user is not aware of it? updateG5State(needs_calibration, was_needing_calibration, NEEDING_CALIBRATION); updateG5State(is_started, was_started, IS_STARTED); updateG5State(is_failed, was_failed, IS_FAILED); } private static void updateG5State(boolean now, boolean previous, String reference) { if (now != previous) { PersistentStore.setBoolean(reference, now); } } private static void storeCalibrationState(final CalibrationState state) { PersistentStore.setByte(OB1G5_STATESTORE, state.getValue()); PersistentStore.setLong(OB1G5_STATESTORE_TIME, JoH.tsl()); } private static CalibrationState getStoredCalibrationState() { if (JoH.msSince(PersistentStore.getLong(OB1G5_STATESTORE_TIME)) < HOUR_IN_MS * 2) { return CalibrationState.parse(PersistentStore.getByte(OB1G5_STATESTORE)); } return CalibrationState.Unknown; } private static void loadCalibrationStateAsRequired() { if ((lastSensorState == null) && JoH.quietratelimit("ob1-load-sensor-state", 5)) { final CalibrationState savedState = getStoredCalibrationState(); if (savedState != Unknown) { lastSensorState = savedState; } } } public static boolean isG5ActiveButUnknownState() { loadCalibrationStateAsRequired(); return (lastSensorState == null || lastSensorState == CalibrationState.Unknown) && usingNativeMode(); } public static boolean isG5WarmingUp() { loadCalibrationStateAsRequired(); return lastSensorState != null && lastSensorState == CalibrationState.WarmingUp && usingNativeMode(); } public static boolean isG5SensorStarted() { loadCalibrationStateAsRequired(); return lastSensorState != null && lastSensorState.sensorStarted() && usingNativeMode() && !pendingStop() && !pendingStart(); } public static boolean isPendingStart() { return pendingStart() && usingNativeMode(); } public static boolean isPendingCalibration() { return pendingCalibration() && usingNativeMode(); } public static boolean isG5WantingInitialCalibration() { loadCalibrationStateAsRequired(); return lastSensorStatus != null && lastSensorState == CalibrationState.NeedsFirstCalibration && usingNativeMode(); } public static boolean isG5WantingCalibration() { loadCalibrationStateAsRequired(); return lastSensorStatus != null && lastSensorState.needsCalibration() && usingNativeMode(); } // are we using the G5 Transmitter to evaluate readings public static boolean usingNativeMode() { return usingCollector() && Pref.getBooleanDefaultFalse("ob1_g5_use_transmitter_alg") && Pref.getBooleanDefaultFalse(OB1G5_PREFS); } public static boolean onlyUsingNativeMode() { return usingNativeMode() && !fallbackToXdripAlgorithm(); } public static boolean isProvidingNativeGlucoseData() { // TODO check age of data? loadCalibrationStateAsRequired(); return usingNativeMode() && lastSensorState != null && lastSensorState.usableGlucose(); } public static boolean fallbackToXdripAlgorithm() { return Pref.getBooleanDefaultFalse("ob1_g5_fallback_to_xdrip"); } public static void msg(String msg) { lastState = msg + " " + JoH.hourMinuteString(); UserError.Log.d(TAG, "Status: " + lastState); lastStateUpdated = JoH.tsl(); if (android_wear && wear_broadcast) { BroadcastGlucose.sendLocalBroadcast(null); } } /* public static void setWatchStatus(DataMap dataMap) { lastStateWatch = dataMap.getString("lastState", ""); static_last_timestamp_watch = dataMap.getLong("timestamp", 0); } public static DataMap getWatchStatus() { DataMap dataMap = new DataMap(); dataMap.putString("lastState", lastState); dataMap.putLong("timestamp", static_last_timestamp); return dataMap; } */ // data for NanoStatus public static SpannableString nanoStatus() { if (android_wear) { final SpannableStringBuilder builder = new SpannableStringBuilder(); builder.append(lastSensorStatus != null ? lastSensorStatus + "\n" : ""); builder.append(state.getString()); return new SpannableString(builder); } else { if (lastSensorState != null && lastSensorState != CalibrationState.Ok) { return Span.colorSpan(lastSensorState.getExtendedText(), lastSensorState == CalibrationState.WarmingUp ? NOTICE.color() : lastSensorState.sensorFailed() ? CRITICAL.color() : BAD.color()); } else { return null; } } } // data for MegaStatus public static List<StatusItem> megaStatus() { init_tx_id(); // needed if we have not passed through local INIT state final List<StatusItem> l = new ArrayList<>(); l.add(new StatusItem("Phone Service State", lastState, JoH.msSince(lastStateUpdated) < 300000 ? (lastState.startsWith("Got data") ? Highlight.GOOD : NORMAL) : (isWatchRunning() ? Highlight.GOOD : CRITICAL))); if (last_scan_started > 0) { final long scanning_time = JoH.msSince(last_scan_started); l.add(new StatusItem("Time scanning", JoH.niceTimeScalar(scanning_time), scanning_time > Constants.MINUTE_IN_MS * 5 ? (scanning_time > Constants.MINUTE_IN_MS * 10 ? BAD : NOTICE) : NORMAL)); } if (lastScanError != null) { l.add(new StatusItem("Scan Error", lastScanError, BAD)); } if ((lastSensorStatus != null)) { l.add(new StatusItem("Sensor Status", lastSensorStatus, lastSensorState != Ok ? NOTICE : NORMAL)); } if (hardResetTransmitterNow) { l.add(new StatusItem("Hard Reset", "Attempting - please wait", Highlight.CRITICAL)); } if (transmitterID != null) { l.add(new StatusItem("Sensor Device", transmitterID + ((transmitterMAC != null && Home.get_engineering_mode()) ? "\n" + transmitterMAC : ""))); } if (static_connection_state != null) { l.add(new StatusItem("Bluetooth Link", static_connection_state)); } if (static_last_connected > 0) { l.add(new StatusItem("Last Connected", JoH.niceTimeScalar(JoH.msSince(static_last_connected)) + " ago")); } if ((!lastState.startsWith("Service Stopped")) && (!lastState.startsWith("Not running"))) l.add(new StatusItem("Brain State", state.getString() + (error_count > 1 ? " Errors: " + error_count : ""), error_count > 1 ? NOTICE : error_count > 4 ? BAD : NORMAL)); if (lastUsableGlucosePacketTime != 0) { if (JoH.msSince(lastUsableGlucosePacketTime) < Constants.MINUTE_IN_MS * 15) { l.add(new StatusItem("G5 Algorithm", "Data Received " + JoH.hourMinuteString(lastUsableGlucosePacketTime), Highlight.GOOD)); } } final int queueSize = Ob1G5StateMachine.queueSize(); if (queueSize > 0) { l.add(new StatusItem("Queue Items", "(" + queueSize + ") " + Ob1G5StateMachine.getFirstQueueItemName())); } if (max_wakeup_jitter > 5000) { l.add(new StatusItem("Slowest Wakeup ", JoH.niceTimeScalar(max_wakeup_jitter), max_wakeup_jitter > Constants.SECOND_IN_MS * 10 ? CRITICAL : NOTICE)); } if (JoH.buggy_samsung) { l.add(new StatusItem("Buggy Samsung", "Using workaround", max_wakeup_jitter < TOLERABLE_JITTER ? Highlight.GOOD : BAD)); } final String tx_id = getTransmitterID(); if (Pref.getBooleanDefaultFalse("wear_sync") && Pref.getBooleanDefaultFalse("enable_wearG5")) { l.add(new StatusItem("Watch Service State", lastStateWatch)); if (static_last_timestamp_watch > 0) { l.add(new StatusItem("Watch got Glucose", JoH.niceTimeSince(static_last_timestamp_watch) + " ago")); } } final String sensorCode = getCurrentSensorCode(); if (sensorCode != null) { if (usingG6()) { l.add(new StatusItem("Calibration Code", sensorCode)); } } l.add(new StatusItem("Preemptive restarts", (Pref.getBooleanDefaultFalse("ob1_g5_preemptive_restart") ? "Enabled" : "Disabled") + (Ob1G5StateMachine.useExtendedTimeTravel() ? " (extended)" : ""))); final VersionRequest1RxMessage vr1 = (VersionRequest1RxMessage) Ob1G5StateMachine.getFirmwareXDetails(tx_id, 1); final VersionRequest2RxMessage vr2 = (VersionRequest2RxMessage) Ob1G5StateMachine.getFirmwareXDetails(tx_id, 2); try { if (vr1 != null) { l.add(new StatusItem("Firmware Version", vr1.firmware_version_string, FirmwareCapability.isG6Rev2(vr1.firmware_version_string) ? NOTICE : NORMAL)); //l.add(new StatusItem("Build Version", "" + vr1.build_version)); if (vr1.version_code != 3) { l.add(new StatusItem("Compat Version", "" + vr1.version_code, Highlight.BAD)); } if (vr1.max_runtime_days != 110 && vr1.max_runtime_days != 112) { l.add(new StatusItem("Transmitter Life", "" + vr1.max_runtime_days + " " + gs(R.string.days))); } } } catch (Exception e) { // TODO add message? } try { if (vr2 != null) { if (vr2.typicalSensorDays != 10 && vr2.typicalSensorDays != 7) { l.add(new StatusItem("Sensor Period", vr2.typicalSensorDays, Highlight.NOTICE)); } //l.add(new StatusItem("Feature mask", vr2.featureBits)); } } catch (Exception e) { // } // firmware hardware details final VersionRequestRxMessage vr = (VersionRequestRxMessage) Ob1G5StateMachine.getFirmwareXDetails(tx_id,0); try { if ((vr != null) && (vr.firmware_version_string.length() > 0)) { if (Home.get_engineering_mode()) { if (vr1 != null && !vr.firmware_version_string.equals(vr1.firmware_version_string)) { l.add(new StatusItem("2nd Firmware Version", vr.firmware_version_string, FirmwareCapability.isG6Rev2(vr.firmware_version_string) ? NOTICE : NORMAL)); } if (vr1 != null && !vr.bluetooth_firmware_version_string.equals(vr1.firmware_version_string)) { l.add(new StatusItem("Bluetooth Version", vr.bluetooth_firmware_version_string)); } l.add(new StatusItem("Other Version", vr.other_firmware_version)); // l.add(new StatusItem("Hardware Version", vr.hardwarev)); if (vr.asic != 61440 && vr.asic != 16705 && vr.asic != 243 && vr.asic != 74) l.add(new StatusItem("ASIC", vr.asic, NOTICE)); } } } catch (NullPointerException e) { l.add(new StatusItem("Version", "Information corrupted", BAD)); } // battery details final BatteryInfoRxMessage bt = Ob1G5StateMachine.getBatteryDetails(tx_id); long last_battery_query = PersistentStore.getLong(G5_BATTERY_FROM_MARKER + tx_id); if (getBatteryStatusNow) { l.add(new StatusItem("Battery Status Request Queued", "Will attempt to read battery status on next sensor reading", NOTICE, "long-press", new Runnable() { @Override public void run() { getBatteryStatusNow = false; } })); } if (JoH.quietratelimit("update-g5-battery-warning", 10)) { updateBatteryWarningLevel(); } if (vr1 != null && Home.get_engineering_mode()) { l.add(new StatusItem("Shelf Life", "" + vr1.inactive_days + " / " + vr1.max_inactive_days)); } final int timekeeperDays = DexTimeKeeper.getTransmitterAgeInDays(tx_id); if ((bt != null) && (last_battery_query > 0)) { l.add(new StatusItem("Battery Last queried", JoH.niceTimeSince(last_battery_query) + " " + "ago", NORMAL, "long-press", new Runnable() { @Override public void run() { getBatteryStatusNow = true; } })); if (vr != null) { final String battery_status = TransmitterStatus.getBatteryLevel(vr.status).toString(); if (!battery_status.equals("OK")) l.add(new StatusItem("Transmitter Status", battery_status, BAD)); } // TODO use string builder instead of ternary for days l.add(new StatusItem("Transmitter Days", ((bt.runtime > -1) ? bt.runtime : "") + ((timekeeperDays > -1) ? ((FirmwareCapability.isTransmitterG6Rev2(tx_id) ? " " : " / ") + timekeeperDays) : "") + ((last_transmitter_timestamp > 0) ? " / " + JoH.qs((double) last_transmitter_timestamp / 86400, 1) : ""))); l.add(new StatusItem("Voltage A", bt.voltagea, bt.voltagea < LOW_BATTERY_WARNING_LEVEL ? BAD : NORMAL)); l.add(new StatusItem("Voltage B", bt.voltageb, bt.voltageb < (LOW_BATTERY_WARNING_LEVEL - 10) ? BAD : NORMAL)); l.add(new StatusItem("Resistance", bt.resist, bt.resist > 1400 ? BAD : (bt.resist > 1000 ? NOTICE : (bt.resist > 750 ? NORMAL : Highlight.GOOD)))); if (vr != null && !FirmwareCapability.isG6Rev2(vr.firmware_version_string)) { l.add(new StatusItem("Temperature", bt.temperature + " \u2103")); } } return l; } public static void resetSomeInternalState() { UserError.Log.d(TAG, "Resetting internal state by request"); transmitterMAC = null; // probably gets reloaded from cache state = INIT; scan_next_run = true; } public void listenForChangeInSettings(boolean listen) { try { final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); if (listen) { prefs.registerOnSharedPreferenceChangeListener(prefListener); } else { prefs.unregisterOnSharedPreferenceChangeListener(prefListener); } } catch (Exception e) { UserError.Log.e(TAG, "Error with preference listener: " + e + " " + listen); } } public final SharedPreferences.OnSharedPreferenceChangeListener prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { checkPreferenceKey(key, prefs); } }; // remember needs proguard exclusion due to access by reflection public static boolean isCollecting() { return (state == CONNECT_NOW && JoH.msSince(static_last_timestamp) < Constants.MINUTE_IN_MS * 30) || JoH.msSince(static_last_timestamp) < Constants.MINUTE_IN_MS * 6; } public static boolean usingCollector() { return Pref.getBooleanDefaultFalse(OB1G5_PREFS) && DexCollectionType.getDexCollectionType() == DexcomG5; } }