/* * Copyright (C) 2013 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.eveningoutpost.dexdrip.Services; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.app.PendingIntent; import android.app.Service; import android.app.UiModeManager; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.bluetooth.BluetoothProfile; 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 com.eveningoutpost.dexdrip.GcmActivity; import com.eveningoutpost.dexdrip.Home; import com.eveningoutpost.dexdrip.ImportedLibraries.usbserial.util.HexDump; import com.eveningoutpost.dexdrip.Models.ActiveBluetoothDevice; import com.eveningoutpost.dexdrip.Models.BgReading; import com.eveningoutpost.dexdrip.Models.Bubble; import com.eveningoutpost.dexdrip.Models.JoH; import com.eveningoutpost.dexdrip.Models.Sensor; import com.eveningoutpost.dexdrip.Models.Tomato; import com.eveningoutpost.dexdrip.Models.TransmitterData; import com.eveningoutpost.dexdrip.Models.UserError; import com.eveningoutpost.dexdrip.Models.UserError.Log; import com.eveningoutpost.dexdrip.Models.blueReader; import com.eveningoutpost.dexdrip.R; import com.eveningoutpost.dexdrip.UtilityModels.Blukon; import com.eveningoutpost.dexdrip.UtilityModels.BridgeResponse; import com.eveningoutpost.dexdrip.UtilityModels.CollectionServiceStarter; import com.eveningoutpost.dexdrip.UtilityModels.Constants; import com.eveningoutpost.dexdrip.UtilityModels.ForegroundServiceStarter; import com.eveningoutpost.dexdrip.UtilityModels.HM10Attributes; import com.eveningoutpost.dexdrip.UtilityModels.Inevitable; import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore; import com.eveningoutpost.dexdrip.UtilityModels.Pref; import com.eveningoutpost.dexdrip.UtilityModels.StatusItem; import com.eveningoutpost.dexdrip.UtilityModels.XbridgePlus; import com.eveningoutpost.dexdrip.utils.BtCallBack; import com.eveningoutpost.dexdrip.utils.CheckBridgeBattery; import com.eveningoutpost.dexdrip.utils.DexCollectionType; import com.eveningoutpost.dexdrip.utils.DisconnectReceiver; import com.eveningoutpost.dexdrip.utils.bt.ScanMeister; import com.eveningoutpost.dexdrip.utils.framework.WakeLockTrampoline; import com.eveningoutpost.dexdrip.xdrip; import com.google.android.gms.wearable.DataMap; import com.rits.cloning.Cloner; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.UUID; import static android.bluetooth.BluetoothDevice.TRANSPORT_LE; import static com.eveningoutpost.dexdrip.Models.JoH.convertPinToBytes; import static com.eveningoutpost.dexdrip.UtilityModels.BgGraphBuilder.DEXCOM_PERIOD; import static com.eveningoutpost.dexdrip.utils.bt.Helper.getStatusName; import static com.eveningoutpost.dexdrip.xdrip.gs; @TargetApi(Build.VERSION_CODES.KITKAT) public class DexCollectionService extends Service implements BtCallBack { public static final String LIMITTER_NAME = "LimiTTer"; private final static String TAG = DexCollectionService.class.getSimpleName(); private static final boolean d = false; //private Context mContext; private static final int STATE_DISCONNECTED = BluetoothProfile.STATE_DISCONNECTED; private static final int STATE_DISCONNECTING = BluetoothProfile.STATE_DISCONNECTING; private static final int STATE_CONNECTING = BluetoothProfile.STATE_CONNECTING; private static final int STATE_CONNECTED = BluetoothProfile.STATE_CONNECTED; private static final String PREF_DEX_COLLECTION_BONDING = "pref_dex_collection_bonding"; private static final String PREF_DEX_COLLECTION_POLLING = "pref_dex_collection_polling"; private static final long POLLING_PERIOD = (Constants.MINUTE_IN_MS * 5) - Constants.SECOND_IN_MS; // TODO different pre-connect timeout windows for different hardware //private static final long RETRY_PERIOD = DEXCOM_PERIOD - (Constants.SECOND_IN_MS * 35); private static final long RETRY_PERIOD = DEXCOM_PERIOD - (Constants.SECOND_IN_MS * 95); private static final long TOLERABLE_JITTER = 10000; private static long last_time_seen = 0; public static String lastState = "Not running"; public static String lastError = null; public static TransmitterData last_transmitter_Data; //WATCH: public static String lastStateWatch = "Not running"; private static PendingIntent serviceIntent; private static PendingIntent serviceFailoverIntent; private static volatile BluetoothGatt mBluetoothGatt; private volatile static int mStaticState = BluetoothProfile.STATE_DISCONNECTING; private static int mStaticStateWatch = 0; // default unknown private static byte[] immediateSend; private static String bondedState; private static int bondingTries = 0; private static int last_battery_level = -1; private static long retry_time = 0; private static long failover_time = 0; private static long poll_backoff = 0; private static long retry_backoff = 0; private static long last_connect_request = 0; private static volatile long descriptor_time = 0; private static volatile int descriptor_callback_failures = 0; private static int watchdog_count = 0; private static long max_wakeup_jitter = 0; private static volatile DISCOVERED servicesDiscovered = DISCOVERED.NULL; private static boolean static_use_transmiter_pl_bluetooth = false; private static boolean static_use_rfduino_bluetooth = false; private static boolean static_use_polling = false; private static boolean static_use_blukon = false; private static boolean static_use_nrf = false; private static String static_last_hexdump; private static String static_last_sent_hexdump; private static TransmitterData last_transmitter_DataWatch; private static int last_battery_level_watch = -1; private static int error133 = 0; private static long last_poll_sent = 0; private static long retry_time_watch = 0; private static long failover_time_watch = 0; private static String static_last_hexdump_watch; private static String static_last_sent_hexdump_watch; private static final UUID CCCD = UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG); public final UUID nrfDataService = UUID.fromString(HM10Attributes.NRF_UART_SERVICE); public final UUID nrfDataRXCharacteristic = UUID.fromString(HM10Attributes.NRF_UART_TX); public final UUID nrfDataTXCharacteristic = UUID.fromString(HM10Attributes.NRF_UART_RX); private final Object mLock = new Object(); // Experimental support for "Transmiter PL" from Marek Macner @FPV-UAV private final boolean use_transmiter_pl_bluetooth = Pref.getBooleanDefaultFalse("use_transmiter_pl_bluetooth"); private final boolean use_rfduino_bluetooth = Pref.getBooleanDefaultFalse("use_rfduino_bluetooth"); // private final boolean use_polling = Home.getBooleanDefaultFalse(PREF_DEX_COLLECTION_POLLING) && DexCollectionType.hasLibre(); private final boolean use_polling = Pref.getBooleanDefaultFalse(PREF_DEX_COLLECTION_POLLING); private final UUID xDripDataService = use_transmiter_pl_bluetooth ? UUID.fromString(HM10Attributes.TRANSMITER_PL_SERVICE) : UUID.fromString(HM10Attributes.HM_10_SERVICE); private final UUID xDripDataCharacteristic = use_transmiter_pl_bluetooth ? UUID.fromString(HM10Attributes.TRANSMITER_PL_RX_TX) : UUID.fromString(HM10Attributes.HM_RX_TX); // Experimental support for rfduino from Tomasz Stachowicz private final UUID xDripDataCharacteristicSend = use_rfduino_bluetooth ? UUID.fromString(HM10Attributes.HM_TX) : UUID.fromString(HM10Attributes.HM_RX_TX); private final String DEFAULT_BT_PIN = getDefaultPin(); private final UUID blukonDataService = UUID.fromString(HM10Attributes.BLUKON_SERVICE); public DexCollectionService dexCollectionService; long lastPacketTime; private SharedPreferences prefs; private static volatile ScanMeister scanMeister; private BluetoothAdapter mBluetoothAdapter; private String mDeviceAddress; private volatile long delay_offset = 0; private final Cloner cloner = new Cloner(); private final BroadcastReceiver mPairingRequestRecevier = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "Received pairing request"); if (!JoH.doPairingRequest(context, this, intent, mDeviceAddress, DEFAULT_BT_PIN)) { UserError.Log.d(TAG, "Pairing request marked as failed, reducing settings to avoid auto-pairing"); Pref.setBoolean("blukon_unbonding", false); unRegisterPairingReceiver(); } } }; private ForegroundServiceStarter foregroundServiceStarter; private volatile int mConnectionState = BluetoothProfile.STATE_DISCONNECTING; private static volatile BluetoothDevice device; private static volatile BluetoothGattCharacteristic mCharacteristic; // Experimental support for rfduino from Tomasz Stachowicz private static volatile BluetoothGattCharacteristic mCharacteristicSend; private byte[] lastdata = null; private static int mStatus = -1; // for display in system status public SharedPreferences.OnSharedPreferenceChangeListener prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() { public void onSharedPreferenceChanged(SharedPreferences prefs, String key) { if (key.compareTo("run_service_in_foreground") == 0) { Log.d("FOREGROUND", "run_service_in_foreground changed!"); if (prefs.getBoolean("run_service_in_foreground", false)) { foregroundServiceStarter = new ForegroundServiceStarter(getApplicationContext(), dexCollectionService); foregroundServiceStarter.start(); Log.d(TAG, "Moving to foreground"); } else { dexCollectionService.stopForeground(true); Log.d(TAG, "Removing from foreground"); } } if (key.equals("dex_collection_method") || key.equals("dex_txid")) { //if the input method or ID changed, accept any new package once even if they seem duplicates Log.d(TAG, "collection method or txID changed - setting lastdata to null"); lastdata = null; } } }; private synchronized void handleConnectedStateChange() { mConnectionState = STATE_CONNECTED; scanMeister.stop(); if ((servicesDiscovered == DISCOVERED.NULL) || Pref.getBoolean("always_discover_services", true)) { Log.d(TAG, "Requesting to discover services: previous: " + servicesDiscovered); servicesDiscovered = DISCOVERED.PENDING; } ActiveBluetoothDevice.connected(); if (JoH.ratelimit("attempt-connection", 30)) { checkConnection(); // refresh status info } if (servicesDiscovered != DISCOVERED.COMPLETE) { if (mBluetoothGatt != null) { if (JoH.ratelimit("dexcollect-discover-services", 1)) { Log.d(TAG, "Calling discoverServices"); mBluetoothGatt.discoverServices(); } else { Log.d(TAG, "Discover services duplicate blocked by rate limit"); } } else { UserError.Log.d(TAG, "Wanted to discover services but gatt was null!"); } } else { Log.d(TAG, "Services already discovered"); checkImmediateSend(); } if (mBluetoothGatt == null) { //gregorybel: no needs to continue if Gatt is null! UserError.Log.e(TAG, "gregorybel: force disconnect!"); handleDisconnectedStateChange(); } } @Override public void btCallback(String address, String status) { UserError.Log.d(TAG, "Processing callback: " + address + " :: " + status); if (address.equals(mDeviceAddress)) { switch (status) { case "DISCONNECTED": handleDisconnectedStateChange(); break; case "SCAN_FOUND": connectIfNotConnected(address); break; case "SCAN_TIMEOUT": status("Scan timed out"); setRetryTimer(); break; case "SCAN_FAILED": status("Scan Failed!"); break; default: UserError.Log.e(TAG, "Unknown status callback for: " + address + " with " + status); } } else { UserError.Log.d(TAG, "Ignoring: " + status + " for " + address + " as we are using: " + mDeviceAddress); } } private synchronized void handleDisconnectedStateChange() { if (JoH.ratelimit("handle-disconnected-state-change", 2)) { mConnectionState = STATE_DISCONNECTED; ActiveBluetoothDevice.disconnected(); if (!getTrustAutoConnect()) { if (mBluetoothGatt != null) { UserError.Log.d(TAG, "Sending disconnection"); try { mBluetoothGatt.disconnect(); } catch (Exception e) { // } } } // TODO should we allow close when trusting auto-connect? if (prefs.getBoolean("close_gatt_on_ble_disconnect", true)) { if (mBluetoothGatt != null) { Log.i(TAG, "onConnectionStateChange: mBluetoothGatt is not null, closing."); if (JoH.ratelimit("refresh-gatt", 60)) { Log.d(TAG, "Refresh result state close: " + JoH.refreshDeviceCache(TAG, mBluetoothGatt)); } mBluetoothGatt.close(); mBluetoothGatt = null; mCharacteristic = null; servicesDiscovered = DISCOVERED.NULL; } else { Log.d(TAG, "mBluetoothGatt is null so not closing"); } lastdata = null; } else { UserError.Log.d(TAG, "Not closing gatt on bluetooth disconnect"); } Log.i(TAG, "onConnectionStateChange: Disconnected from GATT server."); setRetryTimer(); } else { UserError.Log.d(TAG, "Ignoring duplicate disconnected state change"); } } private final BluetoothGattCallback mGattCallback = new BluetoothGattCallback() { @Override public synchronized void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { final PowerManager.WakeLock wl = JoH.getWakeLock("bluetooth-gatt", 60000); mStatus = status; // for display in system status if (status == 133) { error133++; } else { error133 = 0; } try { if (Pref.getBoolean("bluetooth_excessive_wakelocks", true)) { /* PowerManager powerManager = (PowerManager) mContext.getSystemService(POWER_SERVICE); PowerManager.WakeLock wakeLock2 = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DexCollectionService"); wakeLock2.acquire(45000);*/ final PowerManager.WakeLock wakeLock2 = JoH.getWakeLock("DexCollectionExcessive", 45000); } UserError.Log.d(TAG, "onState change status = " + status); // temporary debug // TODO are implementations really bad enough that we need to check that gatt.getDevice() matches the // requested device also? switch (newState) { case BluetoothProfile.STATE_CONNECTED: if (status == 0) { Log.i(TAG, "onConnectionStateChange: Connected to GATT server. "); handleConnectedStateChange(); } else { Log.i(TAG, "Not accepting CONNECTED change as status code is: " + status); if (status == 133) { Log.i(TAG, "Treating as disconnected due to error 133 count: " + error133); if (error133 > 3) { Blukon.unBondIfBlukonAtInit(); } handleDisconnectedStateChange(); } } break; case BluetoothProfile.STATE_DISCONNECTED: Log.i(TAG, "onConnectionStateChange: State disconnected."); handleDisconnectedStateChange(); break; } } catch (Exception e) { UserError.Log.wtf(TAG, "Caught exception in Gatt callback " + e); UserError.Log.wtf(TAG, e); } finally { mStaticState = mConnectionState; JoH.releaseWakeLock(wl); } } @Override public synchronized void onServicesDiscovered(BluetoothGatt gatt, int status) { Log.d(TAG, "Services discovered start"); if (status != BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, "onServicesDiscovered received: " + status); return; } final PowerManager.WakeLock wl = JoH.getWakeLock("bluetooth-onservices", 60000); Log.d(TAG, "onServicesDiscovered received status: " + status); if (prefs.getBoolean(PREF_DEX_COLLECTION_BONDING, false)) { if ((mDeviceAddress != null) && (device != null) && (!areWeBonded(mDeviceAddress))) { if (JoH.ratelimit("dexcollect-create-bond", 20)) { Log.d(TAG, "Attempting to create bond to: " + mDeviceAddress); bondingTries++; if (bondingTries > 5) { Home.toaststaticnext("Bonding failing so disabling bonding feature"); Pref.setBoolean(PREF_DEX_COLLECTION_BONDING, false); } else { device.setPin(convertPinToBytes(DEFAULT_BT_PIN)); device.createBond(); } } } } final BluetoothGattService gattService = mBluetoothGatt.getService(xDripDataService); if (gattService == null) { if (!(static_use_blukon || blueReader.isblueReader() || Tomato.isTomato()||Bubble.isBubble())) { Log.w(TAG, "onServicesDiscovered: xdrip service " + xDripDataService + " not found"); //TODO the selection of nrf is not active at the beginning,so this error will be trown one time unneeded, mey to be optimized. // TODO this should be reworked to be an efficient selector listAvailableServices(mBluetoothGatt); } // try next } else { final BluetoothGattCharacteristic gattCharacteristic = gattService.getCharacteristic(xDripDataCharacteristic); if (gattCharacteristic == null) { Log.w(TAG, "onServicesDiscovered: characteristic " + xDripDataCharacteristic + " not found"); JoH.releaseWakeLock(wl); Log.d(TAG, "onServicesDiscovered: returning due to null xDrip characteristic"); return; } mCharacteristic = gattCharacteristic; final int charaProp = gattCharacteristic.getProperties(); if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { mBluetoothGatt.setCharacteristicNotification(gattCharacteristic, true); if (!static_use_blukon) { try { // TODO use CCCD? final BluetoothGattDescriptor bdescriptor = gattCharacteristic.getDescriptor(UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG)); Log.i(TAG, "Bluetooth Notification Descriptor found: " + bdescriptor.getUuid()); bdescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptor_time = JoH.tsl(); mBluetoothGatt.writeDescriptor(bdescriptor); } catch (Exception e) { Log.e(TAG, "Error setting notification value descriptor: " + e); } } // Experimental support for "Transmiter PL" from Marek Macner @FPV-UAV //if (use_transmiter_pl_bluetooth) { // BluetoothGattDescriptor descriptor = gattCharacteristic.getDescriptor(UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG)); // Log.i(TAG, "Transmiter Descriptor found: " + descriptor.getUuid()); // descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); // mBluetoothGatt.writeDescriptor(descriptor); //} // Experimental support for rfduino from Tomasz Stachowicz if (use_rfduino_bluetooth) { // BluetoothGattDescriptor descriptor = gattCharacteristic.getDescriptor(UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG)); // Log.i(TAG, "Transmiter Descriptor found use_rfduino_bluetooth: " + descriptor.getUuid()); // descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); // mBluetoothGatt.writeDescriptor(descriptor); Log.w(TAG, "onServicesDiscovered: use_rfduino_bluetooth send characteristic " + xDripDataCharacteristicSend + " found"); mCharacteristicSend = gattService.getCharacteristic(xDripDataCharacteristicSend); } else { mCharacteristicSend = mCharacteristic; } } else { Log.w(TAG, "onServicesDiscovered: characteristic " + xDripDataCharacteristic + " not found"); } } final BluetoothGattService nrfGattService = mBluetoothGatt.getService(nrfDataService); // NRF code if (nrfGattService != null) { final BluetoothGattCharacteristic nrfGattCharacteristic = nrfGattService.getCharacteristic(nrfDataRXCharacteristic); if (nrfGattCharacteristic == null) { Log.w(TAG, "onServicesDiscovered: characteristic " + nrfGattCharacteristic + " not found"); JoH.releaseWakeLock(wl); Log.d(TAG, "onServicesDiscovered: returning due to null nrf characteristic"); return; } else { static_use_nrf = true; mCharacteristic = nrfGattCharacteristic; final int charaProp = nrfGattCharacteristic.getProperties(); if ((charaProp | BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { // TODO isn't this condition always true, shouldn't it be & instead of | ? mBluetoothGatt.setCharacteristicNotification(nrfGattCharacteristic, true); try { final BluetoothGattDescriptor bdescriptor = nrfGattCharacteristic.getDescriptor(UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG)); Log.i(TAG, "NRF Bluetooth Notification Descriptor found: " + bdescriptor.getUuid()); descriptor_time = JoH.tsl(); bdescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(bdescriptor); } catch (Exception e) { Log.e(TAG, "Error setting notification value descriptor: " + e); } } mCharacteristic = nrfGattCharacteristic; mCharacteristicSend = nrfGattService.getCharacteristic(nrfDataTXCharacteristic); if (mCharacteristicSend == null) { Log.w(TAG, "onServicesDiscovered: nrf characteristic " + mCharacteristicSend + " not found"); JoH.releaseWakeLock(wl); return; } if (blueReader.isblueReader()) { status("Enabled blueReader"); Log.d(TAG, "blueReader initialized and Version requested"); sendBtMessage(blueReader.initialize()); } else if (Tomato.isTomato()) { status("Enabled tomato"); Log.d(TAG, "Queueing Tomato initialization.."); Inevitable.task("initialize-tomato", 4000, new Runnable() { @Override public void run() { final List<ByteBuffer> buffers = Tomato.initialize(); for (ByteBuffer buffer : buffers) { sendBtMessage(buffer); JoH.threadSleep(150); } Log.d(TAG, "tomato initialized and data requested"); } }); servicesDiscovered = DISCOVERED.NULL; // reset this state }else if (Bubble.isBubble()) { status("Enabled bubble"); Log.d(TAG, "Queueing bubble initialization.."); Inevitable.task("initialize-bubble", 4000, new Runnable() { @Override public void run() { final List<ByteBuffer> buffers = Bubble.initialize(); for (ByteBuffer buffer : buffers) { sendBtMessage(buffer); JoH.threadSleep(150); } Log.d(TAG, "bubble initialized and data requested"); } }); servicesDiscovered = DISCOVERED.NULL; // reset this state } } } // TODO make these detection sections a generic method where only one can match // BLUKON final BluetoothGattService blukonService = mBluetoothGatt.getService(blukonDataService); if (blukonService != null) { Log.i(TAG, "Found " + getString(R.string.blukon) + " device"); mCharacteristic = blukonService.getCharacteristic(UUID.fromString(HM10Attributes.BLUKON_UART_RX)); if (mCharacteristic == null) { Log.w(TAG, "onServicesDiscovered: blukon characteristic " + mCharacteristic + " not found"); // WHAT TO DO HERE? JoH.releaseWakeLock(wl); return; } try { final int charaProp = mCharacteristic.getProperties(); if ((charaProp & BluetoothGattCharacteristic.PROPERTY_NOTIFY) > 0) { UserError.Log.d(TAG, "Setting notification on characteristic: " + mCharacteristic.getUuid() + " charaprop: " + charaProp); final boolean result = mBluetoothGatt.setCharacteristicNotification(mCharacteristic, true); if (!result) UserError.Log.d(TAG, "Failed setting notification on blukon characteristic! " + mCharacteristic.getUuid()); } else { Log.e(TAG, "Blukon characteristic doesn't seem to allow notify - this is very unusual"); } } catch (Exception e) { Log.e(TAG, " Exception during notification preparation " + e); } /* // TODO move this to a function for generic use try { final BluetoothGattDescriptor bdescriptor = mCharacteristic.getDescriptor(UUID.fromString(HM10Attributes.CLIENT_CHARACTERISTIC_CONFIG)); Log.i(TAG, "Blukon Bluetooth Notification Descriptor found: " + bdescriptor.getUuid()); descriptor_time = JoH.tsl(); bdescriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); mBluetoothGatt.writeDescriptor(bdescriptor); } catch (Exception e) { Log.e(TAG, "Error creating notification value descriptor: " + e); } */ mCharacteristicSend = blukonService.getCharacteristic(UUID.fromString(HM10Attributes.BLUKON_UART_TX)); if (mCharacteristicSend == null) { Log.w(TAG, "onServicesDiscovered: blukon send characteristic " + mCharacteristicSend + " not found"); JoH.releaseWakeLock(wl); return; } status("Enabled " + getString(R.string.blukon)); static_use_blukon = true; // doesn't ever get unset Blukon.initialize(); } // TODO is this duplicated in some situations? Inevitable.task("dex-descrpiptor-retry", 2000, () -> { try { UserError.Log.d(TAG, "Writing descriptor inside delayed discover services"); final BluetoothGattDescriptor descriptor = mCharacteristic.getDescriptor(CCCD); if (descriptor != null) { descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); descriptor_time = JoH.tsl(); if (!mBluetoothGatt.writeDescriptor(descriptor)) { Log.d(TAG, "Failed to write descriptor!"); unBondBlucon(); } else { Inevitable.task("dex_check_descriptor_write", 2000, new Runnable() { @Override public void run() { if (descriptor_time != 0) { Log.e(TAG, "Descriptor write did not callback! since: " + JoH.dateTimeText(descriptor_time)); descriptor_time = 0; if ((descriptor_callback_failures == 0) || !static_use_blukon) { try { if (!mBluetoothGatt.writeDescriptor(descriptor)) { Log.d(TAG, "Failed to write descriptor in check callback!"); unBondBlucon(); } } catch (Exception e) { UserError.Log.e(TAG, "Exception during callback check retry: " + e); } } else { if (unBondBlucon()) { descriptor_callback_failures = 0; } } } } }); } } } catch (NullPointerException e) { Log.e(TAG, "Got null pointer trying to set CCCD descriptor"); if (static_use_blukon || Blukon.expectingBlukonDevice()) { // TODO applicable for other devices? if (JoH.ratelimit("null-ccd-retry", 300)) { Log.d(TAG, "Refresh result state close: " + JoH.refreshDeviceCache(TAG, mBluetoothGatt)); setRetryTimer(); } } } }); try { final int charaProp = mCharacteristic.getProperties(); if (!static_use_blukon) { if ((charaProp & BluetoothGattCharacteristic.PROPERTY_READ) > 0) { JoH.runOnUiThreadDelayed(() -> { try { Log.d(TAG, "Reading characteristic: " + mCharacteristic.getUuid().toString()); mBluetoothGatt.readCharacteristic(mCharacteristic); } catch (NullPointerException e) { Log.e(TAG, "Got null pointer trying to readCharacteristic"); } }, 300); } } } catch (NullPointerException e) { UserError.Log.d(TAG, "mCharacteristic was null when attempting to get properties!"); } Log.d(TAG, "Services discovered end"); servicesDiscovered = DISCOVERED.COMPLETE; // waitFor(300); Log.d(TAG, "Services discovered release"); checkImmediateSend(); // TODO maybe find a better home for this JoH.releaseWakeLock(wl); } @Override public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) { try { final String msg = "OnCharacteristic WRITE: " + " status: " + getStatusName(status) + " data: " + JoH.bytesToHex(characteristic.getValue()) + " uuid: " + characteristic.getUuid(); if (status == 0) { Log.d(TAG, msg); } else { UserError.Log.e(TAG, msg); } } catch (Exception e) { UserError.Log.wtf(TAG, "Got exception trying to display data: " + e); } } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { onCharacteristicChanged(gatt, characteristic); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) { final PowerManager.WakeLock wakeLock1 = JoH.getWakeLock("DexCollectionService", 60000); try { final byte[] data = characteristic.getValue(); if (data != null && data.length > 0) { setSerialDataToTransmitterRawData(data, data.length); final String hexdump = HexDump.dumpHexString(data); //if (!hexdump.contains("0x00000000 00 ")) { if (data.length > 1 || data[0] != 0x00) { static_last_hexdump = hexdump; } if (d) Log.i(TAG, "onCharacteristicChanged entered " + hexdump); } lastdata = data; Inevitable.task("dex-set-failover", 1000, () -> { setFailoverTimer(); // restart the countdown // intentionally left hanging wakelock for 5 seconds after we receive something final PowerManager.WakeLock wakeLock2 = JoH.getWakeLock("DexCollectionLinger", 5000); }); } finally { /* if (Pref.getBoolean("bluetooth_frequent_reset", false)) { Log.e(TAG, "Resetting bluetooth due to constant reset option being set!"); JoH.restartBluetooth(getApplicationContext(), 5000); }*/ JoH.releaseWakeLock(wakeLock1); } } @Override public synchronized void onDescriptorWrite(BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, int status) { if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(TAG, "onDescriptorWrite: Wrote GATT Descriptor successfully."); descriptor_callback_failures = 0; } else { Log.d(TAG, "onDescriptorWrite: Error writing GATT Descriptor: " + status); if (descriptor_callback_failures == 0) { descriptor_callback_failures++; // TODO not sure if we want this retry here.. try { if (!mBluetoothGatt.writeDescriptor(descriptor)) { Log.d(TAG, "Failed to write descriptor in on descriptor write retry"); unBondBlucon(); } else { UserError.Log.d(TAG, "Tried to write descriptor again inside onDescriptorWrite"); } } catch (Exception e) { UserError.Log.e(TAG, "Exception during callback retry: " + e); } } else { unBondBlucon(); } } descriptor_time = 0; } }; private static String getDefaultPin() { final String bk_pin = Blukon.getPin(); return bk_pin != null ? bk_pin : HM10Attributes.HM_DEFAULT_BT_PIN; } @SuppressLint("ObsoleteSdkInt") private static boolean shouldServiceRun() { if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) return false; final boolean result = (DexCollectionType.hasXbridgeWixel() || DexCollectionType.hasBtWixel()) && ((!Home.get_forced_wear() && (((UiModeManager) xdrip.getAppContext().getSystemService(UI_MODE_SERVICE)).getCurrentModeType() != Configuration.UI_MODE_TYPE_WATCH)) || PersistentStore.getBoolean(CollectionServiceStarter.pref_run_wear_collector)); if (d) Log.d(TAG, "shouldServiceRun() returning: " + result); return result; } // remember needs proguard exclusion due to access by reflection public static boolean isCollecting() { if (static_use_blukon) { return Blukon.isCollecting(); } return false; } private static void status(String msg) { lastState = msg + " " + JoH.hourMinuteString(); } private static void error(String msg) { lastError = msg + " " + JoH.hourMinuteString(); } private static String getStateStr(int mConnectionState) { mStaticState = mConnectionState; switch (mConnectionState) { case STATE_CONNECTED: return "CONNECTED"; case STATE_CONNECTING: return "CONNECTING"; case STATE_DISCONNECTED: return "DISCONNECTED"; case STATE_DISCONNECTING: return "DISCONNECTING"; default: return "UNKNOWN STATE!"; } } private static Integer convertSrc(final String Src) { Integer res = 0; String tmpSrc = Src.toUpperCase(); res |= getSrcValue(tmpSrc.charAt(0)) << 20; res |= getSrcValue(tmpSrc.charAt(1)) << 15; res |= getSrcValue(tmpSrc.charAt(2)) << 10; res |= getSrcValue(tmpSrc.charAt(3)) << 5; res |= getSrcValue(tmpSrc.charAt(4)); return res; } private static int getSrcValue(char ch) { int i; char[] cTable = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U', 'W', 'X', 'Y'}; for (i = 0; i < cTable.length; i++) { if (cTable[i] == ch) break; } return i; } // Status for Watchface public static boolean isRunning() { return lastState.equals("Not Running") || lastState.startsWith("Stopping", 0) ? false : true; } public static DataMap getWatchStatus() { DataMap dataMap = new DataMap(); dataMap.putString("lastState", lastState); if (last_transmitter_Data != null) dataMap.putLong("timestamp", last_transmitter_Data.timestamp); dataMap.putInt("mStaticState", mStaticState); dataMap.putInt("last_battery_level", last_battery_level); dataMap.putLong("retry_time", retry_time); dataMap.putLong("failover_time", failover_time); dataMap.putString("static_last_hexdump", static_last_hexdump); dataMap.putString("static_last_sent_hexdump", static_last_sent_hexdump); return dataMap; } public static void setWatchStatus(DataMap dataMap) { lastStateWatch = dataMap.getString("lastState", ""); last_transmitter_DataWatch = new TransmitterData(); last_transmitter_DataWatch.timestamp = dataMap.getLong("timestamp", 0); mStaticStateWatch = dataMap.getInt("mStaticState", 0); last_battery_level_watch = dataMap.getInt("last_battery_level", -1); retry_time_watch = dataMap.getLong("retry_time", 0); failover_time_watch = dataMap.getLong("failover_time", 0); static_last_hexdump_watch = dataMap.getString("static_last_hexdump", ""); static_last_sent_hexdump_watch = dataMap.getString("static_last_sent_hexdump", ""); } public static String getBestLimitterHardwareName() { if (static_use_nrf && blueReader.isblueReader()) { return "BlueReader"; } else if (static_use_nrf && Tomato.isTomato()) { return xdrip.getAppContext().getString(R.string.tomato); } else if (static_use_nrf && Bubble.isBubble()) { return xdrip.getAppContext().getString(R.string.bubble); } else if (static_use_blukon) { return xdrip.getAppContext().getString(R.string.blukon); } else if (static_use_transmiter_pl_bluetooth) { return "Transmiter PL"; } else if (static_use_rfduino_bluetooth) { return "Rfduino"; } else return LIMITTER_NAME; } // data for MegaStatus public static List<StatusItem> megaStatus() { final List<StatusItem> l = new ArrayList<>(); final boolean forced_wear = Home.get_forced_wear(); l.add(new StatusItem("Phone Service State", lastState + (forced_wear ? " (Watch Forced)" : ""))); if (lastError != null) { l.add(new StatusItem("Last Error", lastError, StatusItem.Highlight.NOTICE, "long-press", () -> lastError = null)); } l.add(new StatusItem("Bluetooth Device", JoH.ucFirst(getStateStr(mStaticState)))); if (device != null) { l.add(new StatusItem("Device Mac Address", device.getAddress())); } if (Home.get_engineering_mode()) { l.add(new StatusItem("Active device connected", String.valueOf(ActiveBluetoothDevice.is_connected()))); l.add(new StatusItem("Bluetooth GATT", String.valueOf(mBluetoothGatt))); String hint = ""; if (mStatus == 133) { hint = " (restart device?)"; } l.add(new StatusItem("Last status", String.valueOf(mStatus) + hint)); BluetoothManager myBluetoothManager = (BluetoothManager) xdrip.getAppContext().getSystemService(Context.BLUETOOTH_SERVICE); if (myBluetoothManager != null) { for (BluetoothDevice bluetoothDevice : myBluetoothManager.getConnectedDevices(BluetoothProfile.GATT)) { l.add(new StatusItem("GATT device connected", bluetoothDevice.getAddress())); } } } if (mStaticState == STATE_CONNECTING) { final long connecting_ms = JoH.msSince(last_connect_request); l.add(new StatusItem("Connecting for", JoH.niceTimeScalar(connecting_ms))); } if (static_use_polling) { l.add(new StatusItem("Polling mode", ((last_poll_sent > 0) ? "Last poll: " + JoH.niceTimeSince(last_poll_sent) + " ago" : "Enabled"))); } if (static_use_transmiter_pl_bluetooth) { l.add(new StatusItem("Hardware", "Transmiter PL")); } if (static_use_rfduino_bluetooth) { l.add(new StatusItem("Hardware", "Rfduino")); } if (static_use_blukon) { l.add(new StatusItem("Hardware", xdrip.getAppContext().getString(R.string.blukon))); } if (static_use_nrf && blueReader.isblueReader()) { l.add(new StatusItem("Hardware", "BlueReader")); } if (static_use_nrf && Tomato.isTomato()) { l.add(new StatusItem("Hardware", xdrip.getAppContext().getString(R.string.tomato))); } // TODO add LimiTTer info if (last_transmitter_Data != null) { l.add(new StatusItem("Glucose data from", JoH.niceTimeSince(last_transmitter_Data.timestamp) + " ago")); } if (last_battery_level > -1) { l.add(new StatusItem("Battery level", last_battery_level)); } if (Pref.getBooleanDefaultFalse(PREF_DEX_COLLECTION_BONDING)) { if (bondedState != null) { l.add(new StatusItem("Bluetooth Pairing", (bondedState.length() > 0) ? "Bonded" : "Not bonded" + (bondingTries > 1 ? " (" + bondingTries + ")" : ""), (bondedState.length() > 0) ? StatusItem.Highlight.GOOD : StatusItem.Highlight.NOTICE, "long-press", new Runnable() { @Override public void run() { Pref.setBoolean(PREF_DEX_COLLECTION_BONDING, false); if (bondedState.length() > 0) { JoH.static_toast_long("If you want to unbond use Android bluetooth system settings to Forget device"); bondedState = null; } new Thread(new Runnable() { @Override public void run() { CollectionServiceStarter.restartCollectionService(xdrip.getAppContext()); } } ).start(); } })); } } else { l.add(new StatusItem("Bluetooth Pairing", "Disabled, tap to enable", StatusItem.Highlight.NORMAL, "long-press", new Runnable() { @Override public void run() { Pref.setBoolean(PREF_DEX_COLLECTION_BONDING, true); JoH.static_toast_long("This probably only works on HM10/HM11 and blucon devices at the moment and takes a minute"); new Thread(new Runnable() { @Override public void run() { CollectionServiceStarter.restartCollectionService(xdrip.getAppContext()); } } ).start(); } })); } if (max_wakeup_jitter > 2000) { l.add(new StatusItem("Slowest wake up", JoH.niceTimeScalar(max_wakeup_jitter) + " late", max_wakeup_jitter > 61000 ? StatusItem.Highlight.CRITICAL : StatusItem.Highlight.NORMAL)); } if (JoH.buggy_samsung) { l.add(new StatusItem("Buggy Samsung", "Using workaround", max_wakeup_jitter < TOLERABLE_JITTER ? StatusItem.Highlight.GOOD : StatusItem.Highlight.BAD)); } if (retry_time > 0) l.add(new StatusItem("Next Retry", JoH.niceTimeTill(retry_time), JoH.msTill(retry_time) < -2 ? StatusItem.Highlight.CRITICAL : StatusItem.Highlight.NORMAL)); if (failover_time > 0) l.add(new StatusItem("Next Wake up", JoH.niceTimeTill(failover_time), JoH.msTill(failover_time) < -2 ? StatusItem.Highlight.CRITICAL : StatusItem.Highlight.NORMAL)); if (Home.get_engineering_mode() && DexCollectionType.hasLibre()) { l.add(new StatusItem("Request Data", "Test for xBridgePlus protocol", immediateSend == null ? StatusItem.Highlight.NORMAL : StatusItem.Highlight.NOTICE, "long-press", new Runnable() { @Override public void run() { immediateSend = XbridgePlus.sendDataRequestPacket(); CollectionServiceStarter.restartCollectionService(xdrip.getAppContext()); // TODO quicker/cleaner restart } })); } if (Home.get_engineering_mode() && (static_last_hexdump != null)) { l.add(new StatusItem("Received Data", filterHexdump(static_last_hexdump))); } if (Home.get_engineering_mode() && (static_last_sent_hexdump != null)) { l.add(new StatusItem("Sent Data", filterHexdump(static_last_sent_hexdump))); } //WATCH if (forced_wear) { l.add(new StatusItem()); l.add(new StatusItem("Watch Service State", lastStateWatch)); l.add(new StatusItem("Bridge Device", JoH.ucFirst(getStateStr(mStaticStateWatch)))); // TODO add LimiTTer info if ((last_transmitter_DataWatch != null) && (last_transmitter_DataWatch.timestamp > 0)) { l.add(new StatusItem("Watch Glucose data", JoH.niceTimeSince(last_transmitter_DataWatch.timestamp) + " ago")); } if (last_battery_level_watch > -1) { l.add(new StatusItem("Bridge Battery level", last_battery_level_watch)); } if (retry_time_watch > 0) l.add(new StatusItem("Watch Next Retry", JoH.niceTimeTill(retry_time_watch))); if (failover_time_watch > 0) l.add(new StatusItem("Watch Wake up", JoH.niceTimeTill(failover_time_watch))); if (Home.get_engineering_mode() && (static_last_hexdump_watch != null) && (static_last_hexdump_watch.length() > 0)) { l.add(new StatusItem("Watch Received Data", filterHexdump(static_last_hexdump_watch))); } if (Home.get_engineering_mode() && (static_last_sent_hexdump_watch != null) && (static_last_sent_hexdump_watch.length() > 0)) { l.add(new StatusItem("Watch Sent Data", filterHexdump(static_last_sent_hexdump_watch))); } } // blueReader if (blueReader.isblueReader()) { l.add(new StatusItem("blueReader Battery", Pref.getInt("bridge_battery", 0) + "%")); l.add(new StatusItem("blueReader rest days", PersistentStore.getString("bridge_battery_days"))); l.add(new StatusItem("blueReader Firmware", PersistentStore.getString("blueReaderFirmware"))); } if (Tomato.isTomato()) { l.add(new StatusItem("Tomato Battery", PersistentStore.getString("Tomatobattery"))); l.add(new StatusItem("Tomato Hardware", PersistentStore.getString("TomatoHArdware"))); l.add(new StatusItem("Tomato Firmware", PersistentStore.getString("TomatoFirmware"))); l.add(new StatusItem("Libre SN", PersistentStore.getString("LibreSN"))); } if (Bubble.isBubble()) { l.add(new StatusItem("Bubble Battery", PersistentStore.getString("Bubblebattery"))); l.add(new StatusItem("Bubble Hardware", PersistentStore.getString("BubbleHArdware"))); l.add(new StatusItem("Bubble Firmware", PersistentStore.getString("BubbleFirmware"))); l.add(new StatusItem("Libre SN", PersistentStore.getString("LibreSN"))); } if (static_use_blukon) { l.add(new StatusItem("Battery", Pref.getInt("bridge_battery", 0) + "%")); l.add(new StatusItem("Sensor age", JoH.qs(((double) Pref.getInt("nfc_sensor_age", 0)) / 1440, 1) + "d")); l.add(new StatusItem("Libre SN", PersistentStore.getString("LibreSN"))); } return l; } private static String filterHexdump(String hex) { return hex.replaceAll("[ ]+", " ").replaceAll("\n0x0000[0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f][0-9A-Fa-f] ", "\n").replaceFirst("^\n", ""); } @Override public IBinder onBind(Intent intent) { throw new UnsupportedOperationException("Not yet implemented"); } @Override public void onCreate() { if (scanMeister == null) { scanMeister = new ScanMeister() .applyKnownWorkarounds() .addCallBack(this, TAG); } foregroundServiceStarter = new ForegroundServiceStarter(getApplicationContext(), this); foregroundServiceStarter.start(); //mContext = getApplicationContext(); dexCollectionService = this; prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()); listenForChangeInSettings(); //bgToSpeech = BgToSpeech.setupTTS(mContext); //keep reference to not being garbage collected if (CollectionServiceStarter.isDexBridgeOrWifiandDexBridge()) { Log.i(TAG, "onCreate: resetting bridge_battery preference to 0"); prefs.edit().putInt("bridge_battery", 0).apply(); //if (Home.get_master()) GcmActivity.sendBridgeBattery(prefs.getInt("bridge_battery",-1)); } cloner.dontClone( android.bluetooth.BluetoothDevice.class, android.bluetooth.BluetoothGattService.class ); final IntentFilter pairingRequestFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST); pairingRequestFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1); registerReceiver(mPairingRequestRecevier, pairingRequestFilter); Log.i(TAG, "onCreate: STARTING SERVICE: pin code: " + DEFAULT_BT_PIN); Blukon.unBondIfBlukonAtInit(); } @Override public int onStartCommand(Intent intent, int flags, int startId) { final PowerManager.WakeLock wl = JoH.getWakeLock("dexcollect-service", 120000); if (retry_time > 0 && failover_time > 0) { final long requested_wake_time = Math.min(retry_time, failover_time); final long wakeup_jitter = JoH.msSince(requested_wake_time); final String jitter_string = JoH.niceTimeScalar(wakeup_jitter); if (!jitter_string.startsWith("0 ")) { Log.d(TAG, "Wake up jitter: " + jitter_string); } JoH.persistentBuggySamsungCheck(); 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.setBuggySamsungEnabled(); max_wakeup_jitter = 0; } else { max_wakeup_jitter = Math.max(max_wakeup_jitter, wakeup_jitter); } } retry_time = 0; failover_time = 0; static_use_rfduino_bluetooth = use_rfduino_bluetooth; static_use_transmiter_pl_bluetooth = use_transmiter_pl_bluetooth; static_use_polling = use_polling; status("Started"); if (shouldServiceRun()) { setFailoverTimer(); } else { status("Stopping"); stopSelf(); JoH.releaseWakeLock(wl); return START_NOT_STICKY; } lastdata = null; DisconnectReceiver.addCallBack(this, TAG); checkConnection(); watchdog(); JoH.releaseWakeLock(wl); return START_STICKY; } private void unRegisterPairingReceiver() { try { unregisterReceiver(mPairingRequestRecevier); } catch (Exception e) { android.util.Log.e(TAG, "Error unregistering pairing receiver: " + e); } } @Override public void onDestroy() { status("Shutdown"); super.onDestroy(); Log.d(TAG, "onDestroy entered"); close(); foregroundServiceStarter.stop(); unRegisterPairingReceiver(); DisconnectReceiver.removeCallBack(TAG); if (scanMeister != null) { scanMeister.removeCallBack(TAG); scanMeister.stop(); } if (shouldServiceRun()) {//Android killed service setRetryTimer(); status("Stopped, attempting restart"); } else {//onDestroy triggered by CollectionServiceStart.stopBtService Log.d(TAG, "onDestroy stop Alarm serviceIntent"); JoH.cancelAlarm(this, serviceIntent); Log.d(TAG, "onDestroy stop Alarm serviceFailoverIntent"); JoH.cancelAlarm(this, serviceFailoverIntent); status("Service full stop"); retry_time = 0; failover_time = 0; } //BgToSpeech.tearDownTTS(); retry_backoff = 0; poll_backoff = 0; servicesDiscovered = DISCOVERED.NULL; bondingTries = 0; Log.i(TAG, "SERVICE STOPPED"); } public void listenForChangeInSettings() { prefs.registerOnSharedPreferenceChangeListener(prefListener); } public void setRetryTimer() { mStaticState = mConnectionState; if (shouldServiceRun()) { //final long retry_in = (Constants.SECOND_IN_MS * 25); final long retry_in = whenToRetryNext(); Log.d(TAG, "setRetryTimer: Restarting in: " + (retry_in / Constants.SECOND_IN_MS) + " seconds"); //serviceIntent = PendingIntent.getService(this, Constants.DEX_COLLECTION_SERVICE_RETRY_ID, new Intent(this, this.getClass()), 0); serviceIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.DEX_COLLECTION_SERVICE_RETRY_ID); retry_time = JoH.wakeUpIntent(this, retry_in, serviceIntent); } else { Log.d(TAG, "Not setting retry timer as service should not be running"); } } public synchronized void setFailoverTimer() { if (shouldServiceRun()) { final long retry_in = use_polling ? whenToPollNext() : (Constants.MINUTE_IN_MS * 6); Log.d(TAG, "setFailoverTimer: Fallover Restarting in: " + (retry_in / (Constants.MINUTE_IN_MS)) + " minutes"); //serviceFailoverIntent = PendingIntent.getService(this, Constants.DEX_COLLECTION_SERVICE_FAILOVER_ID, new Intent(this, this.getClass()), 0); serviceFailoverIntent = WakeLockTrampoline.getPendingIntent(this.getClass(), Constants.DEX_COLLECTION_SERVICE_FAILOVER_ID); failover_time = JoH.wakeUpIntent(this, retry_in, serviceFailoverIntent); retry_time = 0; // only one alarm will run } else { stopSelf(); } } private long whenToRetryNext() { final long poll_time = Math.max((Constants.SECOND_IN_MS * 10) + retry_backoff, RETRY_PERIOD - JoH.msSince(lastPacketTime)); if (retry_backoff < (Constants.MINUTE_IN_MS)) { retry_backoff += Constants.SECOND_IN_MS; } Log.d(TAG, "Scheduling next retry in: " + JoH.niceTimeScalar(poll_time) + " @ " + JoH.dateTimeText(poll_time + JoH.tsl()) + " period diff: " + (RETRY_PERIOD - JoH.msSince(lastPacketTime))); return poll_time; } private long whenToPollNext() { final long poll_time = Math.max((Constants.SECOND_IN_MS * 5) + poll_backoff, POLLING_PERIOD - JoH.msSince(lastPacketTime)); if (poll_backoff < (Constants.MINUTE_IN_MS * 6)) { poll_backoff += Constants.SECOND_IN_MS; } Log.d(TAG, "Scheduling next poll in: " + JoH.niceTimeScalar(poll_time) + " @ " + JoH.dateTimeText(poll_time + JoH.tsl()) + " period diff: " + (POLLING_PERIOD - JoH.msSince(lastPacketTime))); return poll_time; } synchronized void checkConnection() { status("Attempting connection"); final BluetoothManager bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { status("No bluetooth manager"); setRetryTimer(); return; } mBluetoothAdapter = bluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { status("No bluetooth adapter"); setRetryTimer(); return; } if (!mBluetoothAdapter.isEnabled()) { mConnectionState = STATE_DISCONNECTED; // can't be connected if BT is disabled if (Pref.getBoolean("automatically_turn_bluetooth_on", true)) { Log.i(TAG, "Turning bluetooth on as appears disabled"); status("Turning bluetooth on"); JoH.setBluetoothEnabled(getApplicationContext(), true); } else { Log.d(TAG, "Not automatically turning on bluetooth due to preferences"); } } if (device != null) { boolean found = false; for (BluetoothDevice bluetoothDevice : bluetoothManager.getConnectedDevices(BluetoothProfile.GATT)) { if (bluetoothDevice.getAddress().equals(device.getAddress())) { found = true; if (mConnectionState != STATE_CONNECTED) { UserError.Log.d(TAG, "Detected state change by checking connected devices"); handleConnectedStateChange(); } break; } } if (!found) { if (mConnectionState == STATE_CONNECTED) { UserError.Log.d(TAG, "Marking disconnected as not in list of connected devices"); mConnectionState = STATE_DISCONNECTED; // not in connected list so should be disconnected we think } } } else { UserError.Log.d(TAG, "Device is null in checkConnection"); mConnectionState = STATE_DISCONNECTED; // can't be connected if we don't know the device } Log.i(TAG, "checkConnection: Connection state: " + getStateStr(mConnectionState)); if (mConnectionState == STATE_DISCONNECTED || mConnectionState == STATE_DISCONNECTING) { final ActiveBluetoothDevice btDevice = ActiveBluetoothDevice.first(); if (btDevice != null) { final String deviceAddress = btDevice.address; mDeviceAddress = deviceAddress; try { if (mBluetoothAdapter.isEnabled() && mBluetoothAdapter.getRemoteDevice(deviceAddress) != null) { if (useScanning()) { status(gs(R.string.scanning) + (Home.get_engineering_mode() ? ": " + deviceAddress : "")); scanMeister.setAddress(deviceAddress).addCallBack(this, TAG).scan(); } else { status("Connecting" + (Home.get_engineering_mode() ? ": " + deviceAddress : "")); connect(deviceAddress); } mStaticState = mConnectionState; return; } } catch (IllegalArgumentException e) { Log.e(TAG, "IllegalArgumentException: " + e); } } } else if (mConnectionState == STATE_CONNECTING) { mStaticState = mConnectionState; if (JoH.msSince(last_connect_request) > (getTrustAutoConnect() ? Constants.SECOND_IN_MS * 3600 : Constants.SECOND_IN_MS * 30)) { Log.i(TAG, "Connecting for too long, shutting down"); retry_backoff = 0; close(); } } else if (mConnectionState == STATE_CONNECTED) { //WOOO, we are good to go, nothing to do here! status("Last Connected"); Log.i(TAG, "checkConnection: Looks like we are already connected, ready to receive"); retry_backoff = 0; mStaticState = mConnectionState; if (use_polling && (JoH.msSince(lastPacketTime) >= POLLING_PERIOD)) { pollForData(); } return; } setRetryTimer(); } private synchronized void checkImmediateSend() { if (immediateSend != null) { Log.d(TAG, "Sending immediate data: " + JoH.bytesToHex(immediateSend)); sendBtMessage(immediateSend); immediateSend = null; } } private synchronized void pollForData() { if (JoH.ratelimit("poll-for-data", 5)) { new Thread() { @Override public void run() { Log.d(TAG, "Polling for data"); int wait_counter = 0; while (servicesDiscovered != DISCOVERED.COMPLETE && wait_counter < 10) { Log.d(TAG, "Waiting for service discovery: " + servicesDiscovered + " count: " + wait_counter); try { Thread.sleep(200); // delay for wakeup readiness } catch (InterruptedException e) { // } wait_counter++; } if (servicesDiscovered == DISCOVERED.NULL) { Log.e(TAG, "Failed to discover services!"); try { if (JoH.ratelimit("rediscover-services", 30)) { Log.d(TAG, "Refresh result: " + JoH.refreshDeviceCache(TAG, mBluetoothGatt)); mBluetoothGatt.discoverServices(); } } catch (Exception e) { Log.d(TAG, "Exception discovering services: " + e); } } last_poll_sent = JoH.tsl(); if ((JoH.msSince(lastPacketTime) > Home.stale_data_millis()) && (JoH.ratelimit("poll-request-part-b", 15))) { Log.e(TAG, "Stale data so requesting backfill"); sendBtMessage(XbridgePlus.sendLast15BRequestPacket()); } else { sendBtMessage(XbridgePlus.sendDataRequestPacket()); } } }.start(); } } /** * Displays all services and characteristics for debugging purposes. * * @param bluetoothGatt BLE gatt profile. */ private void listAvailableServices(BluetoothGatt bluetoothGatt) { Log.d(TAG, "Listing available services:"); for (BluetoothGattService service : bluetoothGatt.getServices()) { Log.d(TAG, "Service: " + service.getUuid().toString()); for (BluetoothGattCharacteristic characteristic : service.getCharacteristics()) { Log.d(TAG, "|-- Characteristic: " + characteristic.getUuid().toString()); } } } private boolean sendBtMessage(byte[] buffer) { return sendBtMessage(JoH.bArrayAsBuffer(buffer)); } private synchronized boolean sendBtMessage(final ByteBuffer message) { // TODO affirm send happened //check mBluetoothGatt is available Log.i(TAG, "sendBtMessage: entered"); if (mBluetoothGatt == null) { Log.w(TAG, "sendBtMessage: lost connection"); if (JoH.ratelimit("sendbtmessagelost", 60)) { mConnectionState = STATE_DISCONNECTED; setRetryTimer(); } return false; } final byte[] value = message.array(); static_last_sent_hexdump = HexDump.dumpHexString(value); Log.i(TAG, "sendBtMessage: sending message: " + static_last_sent_hexdump); // Experimental support for rfduino from Tomasz Stachowicz if (use_rfduino_bluetooth) { Log.w(TAG, "sendBtMessage: use_rfduino_bluetooth"); if (mCharacteristicSend == null) { status("Error: mCharacteristicSend was null in sendBtMessage"); Log.e(TAG, lastState); servicesDiscovered = DISCOVERED.NULL; return false; } return writeChar(mCharacteristicSend, value); } // BLUCON NULL HERE? HOW TO RESOLVE? if (mCharacteristic == null) { status("Error: mCharacteristic was null in sendBtMessage"); Log.e(TAG, lastState); servicesDiscovered = DISCOVERED.NULL; return false; } if (mCharacteristicSend != null && mCharacteristicSend != mCharacteristic) { return writeChar(mCharacteristicSend, value); } return writeChar(mCharacteristic, value); } private synchronized boolean writeChar(final BluetoothGattCharacteristic localmCharacteristic, final byte[] value) { if (value == null) { UserError.Log.e(TAG, "Value null in write char"); return false; } if (localmCharacteristic == null) { UserError.Log.e(TAG, "localmCharacteristic null in write char"); return false; } localmCharacteristic.setValue(value); final boolean result = mBluetoothGatt != null && mBluetoothGatt.writeCharacteristic(localmCharacteristic); if (!result) { UserError.Log.d(TAG, "Error writing characteristic: " + localmCharacteristic.getUuid() + " " + JoH.bytesToHex(value)); final BluetoothGattCharacteristic resendCharacteristic = cloner.shallowClone(localmCharacteristic); if (JoH.quietratelimit("dexcol-resend-offset", 2)) { delay_offset = 0; } else { delay_offset += 100; if (d) UserError.Log.e(TAG, "Delay offset now: " + delay_offset); } JoH.getWakeLock("dexcol-resend-linger", 1000); // dangling wakelock to ensure awake for resend JoH.runOnUiThreadDelayed(new Runnable() { @Override public void run() { try { boolean result = mBluetoothGatt != null && mBluetoothGatt.writeCharacteristic(resendCharacteristic); if (!result) { UserError.Log.e(TAG, "Error writing characteristic: (2nd try) " + resendCharacteristic.getUuid() + " " + JoH.bytesToHex(value)); } else { UserError.Log.d(TAG, "Succeeded writing characteristic: (2nd try) " + resendCharacteristic.getUuid() + " " + JoH.bytesToHex(value)); } } catch (Exception e) { UserError.Log.wtf(TAG, "Exception during 2nd try write: " + e + " " + resendCharacteristic.getUuid() + " " + JoH.bytesToHex(value)); } } }, 500 + delay_offset); } return result; } public synchronized boolean connectIfNotConnected(final String address) { UserError.Log.d(TAG, "connectIfNotConnected!!! " + address); // check connected! if (mConnectionState != STATE_CONNECTED) { return connect(address); } else { UserError.Log.d(TAG, "Already connected"); return false; } } public synchronized boolean connect(final String address) { Log.i(TAG, "connect: going to connect to device at address: " + address); if (mBluetoothAdapter == null || address == null) { Log.i(TAG, "connect: BluetoothAdapter not initialized or unspecified address."); setRetryTimer(); return false; } // close and re-open the connection if preference set or device has changed final boolean should_close = Pref.getBooleanDefaultFalse("close_gatt_on_ble_disconnect") || (device == null || !address.equalsIgnoreCase(device.getAddress()) || ((JoH.msSince(last_connect_request) > (Constants.MINUTE_IN_MS * 15) && (JoH.pratelimit("dex-collect-full-close", 600))))); closeCycle(should_close); /* if (device != null) { if (!device.getAddress().equals(address)) { UserError.Log.e(TAG, "Device address changed from: " + device.getAddress() + " to " + address); device = null; } }*/ //if (device == null) { device = mBluetoothAdapter.getRemoteDevice(address); // } if (device == null) { Log.w(TAG, "Device not found. Unable to connect."); setRetryTimer(); return false; } if (static_use_blukon || Blukon.expectingBlukonDevice()) { UserError.Log.d(TAG, "Setting blukon pairing pin to: " + DEFAULT_BT_PIN); device.setPin(convertPinToBytes(DEFAULT_BT_PIN)); } setRetryTimer(); if (mBluetoothGatt == null) { Log.i(TAG, "connect: Trying to create a new connection."); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { mBluetoothGatt = device.connectGatt(getApplicationContext(), getTrustAutoConnect(), mGattCallback, TRANSPORT_LE); } else { mBluetoothGatt = device.connectGatt(getApplicationContext(), getTrustAutoConnect(), mGattCallback); } } else { Log.i(TAG, "connect: Trying to re-use connection."); mBluetoothGatt.connect(); } mConnectionState = STATE_CONNECTING; last_connect_request = JoH.tsl(); return true; } private static boolean getTrustAutoConnect() { return Pref.getBoolean("bluetooth_trust_autoconnect", true); } private void closeCycle(boolean should_close) { if (mBluetoothGatt != null) { try { if (JoH.ratelimit("refresh-gatt", 60)) { Log.d(TAG, "Refresh result close: " + JoH.refreshDeviceCache(TAG, mBluetoothGatt)); } if (should_close) { Log.i(TAG, "connect: mBluetoothGatt isn't null, Closing."); mBluetoothGatt.close(); } else { Log.i(TAG, "preserving existing connection"); } } catch (NullPointerException e) { Log.wtf(TAG, "Concurrency related null pointer in connect"); } finally { if (should_close) mBluetoothGatt = null; } } } public synchronized void close() { Log.i(TAG, "close: Closing Connection - setting state DISCONNECTED"); if (mBluetoothGatt == null) { Log.i(TAG, "not closing as bluetooth gatt is null"); // return; } else { if (JoH.ratelimit("refresh-gatt", 180)) { Log.d(TAG, "Refresh result state close: " + JoH.refreshDeviceCache(TAG, mBluetoothGatt)); } try { mBluetoothGatt.close(); } catch (NullPointerException e) { Log.wtf(TAG, "Concurrency related null pointer in close"); } } setRetryTimer(); mBluetoothGatt = null; mCharacteristic = null; mConnectionState = STATE_DISCONNECTED; servicesDiscovered = DISCOVERED.NULL; last_connect_request = 0; } private void sendReply(BridgeResponse reply) { for (ByteBuffer byteBuffer : reply.getSend()) { Log.d(TAG, "Sending reply message"); sendBtMessage(byteBuffer); } } public synchronized void setSerialDataToTransmitterRawData(byte[] buffer, int len) { last_time_seen = JoH.tsl(); watchdog_count = 0; if (static_use_blukon && Blukon.checkBlukonPacket(buffer)) { final byte[] reply = Blukon.decodeBlukonPacket(buffer); if (reply != null) { Log.d(TAG, "Sending reply message from Blukon decoder"); sendBtMessage(reply); gotValidPacket(); } } else if (blueReader.isblueReader()) { final byte[] reply = blueReader.decodeblueReaderPacket(buffer, len); if (reply != null) { Log.d(TAG, "Sending reply message from blueReader decoder"); sendBtMessage(reply); gotValidPacket(); } } else if (Tomato.isTomato()) { final BridgeResponse reply = Tomato.decodeTomatoPacket(buffer, len); if (reply.shouldDelay()) { Inevitable.task("send-tomato-reply", reply.getDelay(), () -> sendReply(reply)); } else { sendReply(reply); } if (reply.hasError()) { JoH.static_toast_long(reply.getError_message()); error(reply.getError_message()); } gotValidPacket(); }else if (Bubble.isBubble()) { final BridgeResponse reply = Bubble.decodeBubblePacket(buffer, len); if (reply.shouldDelay()) { Inevitable.task("send-bubble-reply", reply.getDelay(), () -> sendReply(reply)); } else { sendReply(reply); } if (reply.hasError()) { JoH.static_toast_long(reply.getError_message()); error(reply.getError_message()); } gotValidPacket(); } else if (XbridgePlus.isXbridgeExtensionPacket(buffer)) { // handle xBridge+ protocol packets final byte[] reply = XbridgePlus.decodeXbridgeExtensionPacket(buffer); if (reply != null) { Log.d(TAG, "Sending reply message from xBridge decoder"); sendBtMessage(reply); gotValidPacket(); } } else { long timestamp = new Date().getTime(); if (((buffer.length > 0) && (buffer[0] == 0x07 || buffer[0] == 0x11 || buffer[0] == 0x15)) || CollectionServiceStarter.isDexBridgeOrWifiandDexBridge()) { if ((buffer.length == 1) && (buffer[0] == 0x00)) { return; // null packet } Log.i(TAG, "setSerialDataToTransmitterRawData: Dealing with Dexbridge packet!"); int DexSrc; int TransmitterID; String TxId; ByteBuffer tmpBuffer = ByteBuffer.allocate(len); tmpBuffer.order(ByteOrder.LITTLE_ENDIAN); tmpBuffer.put(buffer, 0, len); ByteBuffer txidMessage = ByteBuffer.allocate(6); txidMessage.order(ByteOrder.LITTLE_ENDIAN); if (buffer[0] == 0x07 && buffer[1] == -15) { //We have a Beacon packet. Get the TXID value and compare with dex_txid Log.i(TAG, "setSerialDataToTransmitterRawData: Received Beacon packet."); //DexSrc starts at Byte 2 of a Beacon packet. DexSrc = tmpBuffer.getInt(2); TxId = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("dex_txid", "00000"); TransmitterID = convertSrc(TxId); if (TxId.compareTo("00000") != 0 && Integer.compare(DexSrc, TransmitterID) != 0) { Log.w(TAG, "setSerialDataToTransmitterRawData: TXID wrong. Expected " + TransmitterID + " but got " + DexSrc); txidMessage.put(0, (byte) 0x06); txidMessage.put(1, (byte) 0x01); txidMessage.putInt(2, TransmitterID); sendBtMessage(txidMessage); } return; } if ((buffer[0] == 0x11 || buffer[0] == 0x15) && buffer[1] == 0x00) { //we have a data packet. Check to see if the TXID is what we are expecting. Log.i(TAG, "setSerialDataToTransmitterRawData: Received Data packet"); if (len >= 0x11) { //DexSrc starts at Byte 12 of a data packet. DexSrc = tmpBuffer.getInt(12); TxId = PreferenceManager.getDefaultSharedPreferences(getApplicationContext()).getString("dex_txid", "00000"); TransmitterID = convertSrc(TxId); if (Integer.compare(DexSrc, TransmitterID) != 0) { Log.w(TAG, "TXID wrong. Expected " + TransmitterID + " but got " + DexSrc); txidMessage.put(0, (byte) 0x06); txidMessage.put(1, (byte) 0x01); txidMessage.putInt(2, TransmitterID); sendBtMessage(txidMessage); } Pref.setInt("bridge_battery", ByteBuffer.wrap(buffer).get(11)); // PreferenceManager.getDefaultSharedPreferences(mContext).edit().putInt("bridge_battery", ByteBuffer.wrap(buffer).get(11)).apply(); last_battery_level = Pref.getInt("bridge_battery", -1); //All is OK, so process it. //first, tell the wixel it is OK to sleep. Log.d(TAG, "setSerialDataToTransmitterRawData: Sending Data packet Ack, to put wixel to sleep"); ByteBuffer ackMessage = ByteBuffer.allocate(2); ackMessage.put(0, (byte) 0x02); ackMessage.put(1, (byte) 0xF0); sendBtMessage(ackMessage); //duplicates are already filtered in TransmitterData.create - so no need to filter here poll_backoff = 0; gotValidPacket(); Log.v(TAG, "setSerialDataToTransmitterRawData: Creating TransmitterData at " + timestamp); processNewTransmitterData(TransmitterData.create(buffer, len, timestamp), timestamp); if (Home.get_master()) GcmActivity.sendBridgeBattery(Pref.getInt("bridge_battery", -1)); CheckBridgeBattery.checkBridgeBattery(); } } } else { processNewTransmitterData(TransmitterData.create(buffer, len, timestamp), timestamp); } } } private void gotValidPacket() { retry_backoff = 0; lastPacketTime = JoH.tsl(); } private boolean unBondBlucon() { if (static_use_blukon && Pref.getBooleanDefaultFalse("blukon_unbonding")) { Log.d(TAG, "Attempting to unbond blukon"); JoH.unBond(mDeviceAddress); return true; } else { return false; } } private synchronized void processNewTransmitterData(TransmitterData transmitterData, long timestamp) { if (transmitterData == null) { return; } final Sensor sensor = Sensor.currentSensor(); if (sensor == null) { Log.i(TAG, "setSerialDataToTransmitterRawData: No Active Sensor, Data only stored in Transmitter Data"); return; } if (use_transmiter_pl_bluetooth && (transmitterData.raw_data == 100000)) { Log.wtf(TAG, "Ignoring probably erroneous Transmiter_PL data: " + transmitterData.raw_data); return; } //sensor.latest_battery_level = (sensor.latest_battery_level != 0) ? Math.min(sensor.latest_battery_level, transmitterData.sensor_battery_level) : transmitterData.sensor_battery_level; sensor.latest_battery_level = transmitterData.sensor_battery_level; // allow level to go up and down sensor.save(); last_transmitter_Data = transmitterData; Log.d(TAG, "BgReading.create: new BG reading at " + timestamp + " with a timestamp of " + transmitterData.timestamp); BgReading.create(transmitterData.raw_data, transmitterData.filtered_data, this, transmitterData.timestamp); } private synchronized boolean areWeBonded(String hunt_address) { if (mBluetoothAdapter == null) { Log.e(TAG, "mBluetoothAdapter is null"); return true; // failsafe } final Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices(); if ((pairedDevices != null) && (pairedDevices.size() > 0)) { for (BluetoothDevice device : pairedDevices) { final String address = device.getAddress(); if (address != null) { if (hunt_address.equals(address)) { Log.d(TAG, hunt_address + " is bonded"); bondedState = hunt_address; return true; } } } } Log.d(TAG, hunt_address + " is not bonded"); bondedState = ""; return false; } private void watchdog() { if (last_time_seen == 0) return; if (prefs.getBoolean("bluetooth_watchdog", false)) { int MAX_BT_WDG = 20; int bt_wdg_timer = JoH.parseIntWithDefault(Pref.getString("bluetooth_watchdog_timer", Integer.toString(MAX_BT_WDG)), 10, MAX_BT_WDG); if ((bt_wdg_timer <= 5) || (bt_wdg_timer > MAX_BT_WDG)) { bt_wdg_timer = MAX_BT_WDG; } if ((JoH.msSince(last_time_seen)) > bt_wdg_timer * Constants.MINUTE_IN_MS) { Log.d(TAG, "Use BT Watchdog timer=" + bt_wdg_timer); if (!JoH.isOngoingCall()) { Log.e(TAG, "Watchdog triggered, attempting to reset bluetooth"); status("Watchdog triggered"); JoH.restartBluetooth(getApplicationContext()); last_time_seen = JoH.tsl(); watchdog_count++; if (watchdog_count > 5) last_time_seen = 0; } else { Log.e(TAG, "Delaying watchdog reset as phone call is ongoing."); } } } } private static boolean useScanning() { // TODO check location services return Pref.getBooleanDefaultFalse("bluetooth_use_scan"); } public void waitFor(final int millis) { synchronized (mLock) { try { UserError.Log.d(TAG, "waiting " + millis + "ms"); mLock.wait(millis); } catch (final InterruptedException e) { UserError.Log.e(TAG, "Sleeping interrupted", e); } } } private enum DISCOVERED { NULL, PENDING, COMPLETE } }