package com.eveningoutpost.dexdrip.Services;

/**
 * Created by jcostik1 on 3/15/16.
 */

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
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.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanResult;
import android.bluetooth.le.ScanSettings;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.PowerManager;
import android.preference.PreferenceManager;

import com.eveningoutpost.dexdrip.G5Model.AuthChallengeRxMessage;
import com.eveningoutpost.dexdrip.G5Model.AuthChallengeTxMessage;
import com.eveningoutpost.dexdrip.G5Model.AuthRequestTxMessage;
import com.eveningoutpost.dexdrip.G5Model.AuthStatusRxMessage;
import com.eveningoutpost.dexdrip.G5Model.BatteryInfoRxMessage;
import com.eveningoutpost.dexdrip.G5Model.BatteryInfoTxMessage;
import com.eveningoutpost.dexdrip.G5Model.BluetoothServices;
import com.eveningoutpost.dexdrip.G5Model.BondRequestTxMessage;
import com.eveningoutpost.dexdrip.G5Model.DisconnectTxMessage;
import com.eveningoutpost.dexdrip.G5Model.Extensions;
import com.eveningoutpost.dexdrip.G5Model.GlucoseRxMessage;
import com.eveningoutpost.dexdrip.G5Model.GlucoseTxMessage;
import com.eveningoutpost.dexdrip.G5Model.KeepAliveTxMessage;
import com.eveningoutpost.dexdrip.G5Model.SensorRxMessage;
import com.eveningoutpost.dexdrip.G5Model.SensorTxMessage;
import com.eveningoutpost.dexdrip.G5Model.Transmitter;
import com.eveningoutpost.dexdrip.G5Model.TransmitterStatus;
import com.eveningoutpost.dexdrip.G5Model.VersionRequestRxMessage;
import com.eveningoutpost.dexdrip.G5Model.VersionRequestTxMessage;
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.TransmitterData;
import com.eveningoutpost.dexdrip.Models.UserError;
import com.eveningoutpost.dexdrip.Models.UserError.Log;
import com.eveningoutpost.dexdrip.UtilityModels.CollectionServiceStarter;
//KS import com.eveningoutpost.dexdrip.UtilityModels.ForegroundServiceStarter;
import com.eveningoutpost.dexdrip.UtilityModels.PersistentStore;
import com.eveningoutpost.dexdrip.UtilityModels.Pref;
import com.eveningoutpost.dexdrip.UtilityModels.StatusItem;
//KS import com.eveningoutpost.dexdrip.utils.BgToSpeech;
import com.eveningoutpost.dexdrip.xdrip;

import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.UUID;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;

import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.getStatusName;
import static com.eveningoutpost.dexdrip.G5Model.BluetoothServices.getUUIDName;

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
public class G5CollectionService extends G5BaseService {

    public final static String TAG = G5CollectionService.class.getSimpleName();

    private static final Object short_lock = new Object();
    private final Object mLock = new Object();
    private static boolean cycling_bt = false;
    private static boolean service_running = false;
    private static boolean scan_scheduled = false;

    private static byte lastOnReadCode = (byte)0xff;
    private static int successes = 0;
    private static int failures = 0;
    private boolean force_always_authenticate = false;
    private boolean force_always_on_screen = false;//TODO enable for certain phones, eg., Moto360 2G

    //KS private ForegroundServiceStarter foregroundServiceStarter;

    public Service service;
    //KS private BgToSpeech bgToSpeech;
    private static PendingIntent pendingIntent;

    private android.bluetooth.BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private BluetoothLeScanner mLEScanner;
    private BluetoothGatt mGatt;
    private Transmitter defaultTransmitter;
    public AuthStatusRxMessage authStatus = null;
    public AuthRequestTxMessage authRequest = null;

    private BluetoothGattService cgmService;// = gatt.getService(UUID.fromString(BluetoothServices.CGMService));
    private BluetoothGattCharacteristic authCharacteristic;// = cgmService.getCharacteristic(UUID.fromString(BluetoothServices.Authentication));
    private BluetoothGattCharacteristic controlCharacteristic;//
    //private BluetoothGattCharacteristic commCharacteristic;//

    private BluetoothDevice device;
    private Boolean isBondedOrBonding = false;
    private Boolean isBonded = false;
    private int currentBondState = 0;
    private int waitingBondConfirmation = 0; // 0 = not waiting, 1 = waiting, 2 = received
    public static boolean keep_running = true;

    private ScanSettings settings;
    private List<ScanFilter> filters;
    private SharedPreferences prefs;

    private static boolean isScanning = false;
    private boolean isConnected = false;
    private boolean encountered133 = false;
    //private Handler handler;
    private final int max133Retries = 5;
    public int max133RetryCounter = 0;
    private static int disconnected133 = 0;
    private static int disconnected59 = 0;
    public boolean isIntialScan = true;
    public static Timer scan_interval_timer = new Timer();
    public ArrayList<Long> advertiseTimeMS = new ArrayList<Long>();
    public long timeInMillisecondsOfLastSuccessfulSensorRead = new Date().getTime();
    private int maxScanIntervalInMilliseconds = 5 * 1000; //seconds *1k
    private int maxScanCycles = 24;
    private int scanCycleCount = 0;
    public Context mContext;//KS
    //private boolean delays = false;

    //private static String lastState = "Not running";
    //private static String lastStateWatch = "Not running";
    //private static long static_last_timestamp = 0;
    //private static long static_last_timestamp_watch = 0;
    private static long last_transmitter_timestamp = 0;

    //public static boolean getBatteryStatusNow = false;

    // test params
    private static final boolean ignoreLocalBondingState = false; // don't try to bond gives: GATT_ERR_UNLIKELY but no more 133s
    private static final boolean delayOnBond = false; // delay while bonding also gives ERR_UNLIKELY but no more 133s
    private static final boolean tryPreBondWithDelay = false; // prebond with delay seems to help
    private static final boolean tryOnDemandBondWithDelay = true; // bond when requested
    private static final boolean delayOn133Errors = true; // add some delays with 133 errors
    private static final boolean useKeepAlive = true; // add some delays with 133 errors
    private static final boolean simpleBondWait = true; // possible UI thread issue but apparently more reliable
    private static final boolean getVersionDetails = true; // try to load firmware version details
    private static final boolean getBatteryDetails = true; // try to load battery info details

    private static final long BATTERY_READ_PERIOD_MS = 1000 * 60 * 60 * 12; // how often to poll battery data (12 hours)
    private PowerManager.WakeLock fullWake;
    private static long nextWakeUpTime = -1;
    private static long wake_time_difference = 0;
    private static int wakeUpErrors = 0;

    StringBuilder log = new StringBuilder();

    @Override
    public void onCreate() {
        super.onCreate();
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            initScanCallback();
        }
        advertiseTimeMS.add((long)0);
        service = this;
        mContext = getApplicationContext();//KS
        //KS foregroundServiceStarter = new ForegroundServiceStarter(getApplicationContext(), service);
        //KS foregroundServiceStarter.start();

        prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        listenForChangeInSettings();

        // TODO check this
        //KS bgToSpeech = BgToSpeech.setupTTS(getApplicationContext()); //keep reference to not being garbage collected
        // handler = new Handler(getApplicationContext().getMainLooper());

        final IntentFilter bondintent = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);//KS turn on
        bondintent.addAction(BluetoothDevice.ACTION_FOUND);//KS add
        registerReceiver(mPairReceiver, bondintent);//KS turn on

        final IntentFilter pairingRequestFilter = new IntentFilter(BluetoothDevice.ACTION_PAIRING_REQUEST);
        pairingRequestFilter.setPriority(IntentFilter.SYSTEM_HIGH_PRIORITY - 1);
        registerReceiver(mPairingRequestRecevier, pairingRequestFilter);
    }

    final BroadcastReceiver mPairReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (!keep_running) {
                try {
                    UserError.Log.e(TAG, "Rogue pair receiver still active - unregistering");
                    unregisterReceiver(mPairReceiver);
                } catch (Exception e) {
                    //
                }
                return;
            }
            final String action = intent.getAction();
            Log.d(TAG, "onReceive ACTION: " + action);
            // When discovery finds a device
            if (BluetoothDevice.ACTION_FOUND.equals(action)) {
                final BluetoothDevice parcel_device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // TODO do we need to filter on the last 2 characters of the device name here?
                currentBondState = parcel_device.getBondState();
                Log.d(TAG, "onReceive FOUND: " + parcel_device.getName() + " STATE: " + parcel_device.getBondState());
            } else if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) {
                final BluetoothDevice parcel_device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
                // TODO do we need to filter on the last 2 characters of the device name here?
                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);

                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 {
                    // TODO check getBondState() or bond_state_extra ?
                    if (parcel_device.getBondState() == BluetoothDevice.BOND_BONDED) {
                        if (parcel_device.getAddress().equals(device.getAddress())) {
                            if (waitingBondConfirmation == 1) {
                                waitingBondConfirmation = 2; // received
                                Log.e(TAG, "Bond confirmation received!");
                            }
                        }
                    }
                } catch (Exception e) {
                    Log.wtf(TAG, "Got exception trying to process bonded confirmation: ", e);
                }
            }
        }
    };



    public SharedPreferences.OnSharedPreferenceChangeListener prefListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
        public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
            //KS not needed
            /*
            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(), service);
                    foregroundServiceStarter.start();
                    Log.i(TAG, "Moving to foreground");
                } else {
                    service.stopForeground(true);
                    Log.i(TAG, "Removing from foreground");
                }
            }
            */

            if(key.compareTo("run_ble_scan_constantly") == 0 || key.compareTo("always_unbond_G5") == 0
                    || key.compareTo("always_get_new_keys") == 0 || key.compareTo("run_G5_ble_tasks_on_uithread") == 0) {
                Log.i(TAG, "G5 Setting Change");
                cycleScan(0);
            }

        }
    };

    public void listenForChangeInSettings() {
        prefs.registerOnSharedPreferenceChangeListener(prefListener);
        // TODO do we need an unregister!?
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Context context = getApplicationContext();
        xdrip.checkAppContext(context);
        Sensor.InitDb(context);//ensure db is initialized
        final PowerManager.WakeLock wl = JoH.getWakeLock("g5-start-service", 120000);
        try {
            if ((!service_running) && (keep_running)) {
                service_running = true;

                checkWakeupTimeLatency();
                logWakeTimeLatency();

                Log.d(TAG, "onG5StartCommand wakeup: "+JoH.dateTimeText(JoH.tsl()));
                Log.e(TAG, "settingsToString: " + settingsToString());

                lastState = "Started: "+JoH.hourMinuteString();

                //Log.d(TAG, "SDK: " + Build.VERSION.SDK_INT);
                //stopScan();
                if (!shouldServiceRun()) {
                    Log.e(TAG,"Shutting down as no longer using G5 data source");
                    service_running = false;
                    keep_running = false;
                    stopSelf();
                    return START_NOT_STICKY;
                } else {

                    scanCycleCount = 0;
                    mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
                    mBluetoothAdapter = mBluetoothManager.getAdapter();

                    if (mGatt != null) {
                        try {
                            Log.d(TAG, "onStartCommand mGatt != null; mGatt.close() and set to null.");
                            mGatt.close();
                            mGatt = null;
                        } catch (NullPointerException e) { //
                        }
                    }

                    if (Sensor.isActive()) {
                        setupBluetooth();
                        Log.d(TAG, "Active Sensor");

                    } else {
                        stopScan();
                        Log.d(TAG, "No Active Sensor");
                    }

                    service_running=false;

                    return START_STICKY;
                }
            } else {
                Log.e(TAG,"G5 service already active!");
                keepAlive();
                return START_NOT_STICKY;
            }

        } finally {
            JoH.releaseWakeLock(wl);
        }
    }

    private synchronized void getTransmitterDetails() {
        prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        Log.d(TAG, "Transmitter: " + prefs.getString("dex_txid", "ABCDEF"));
        defaultTransmitter = new Transmitter(prefs.getString("dex_txid", "ABCDEF"));
        final boolean previousBondedState = isBonded;
        isBondedOrBonding = false;
        isBonded = false;
        if (mBluetoothAdapter == null) {
            Log.wtf(TAG, "No bluetooth adapter");
            return;
        }
        final Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
        if ((pairedDevices != null) && (pairedDevices.size() > 0)) {
            for (BluetoothDevice device : pairedDevices) {
                if (device.getName() != null) {

                    final String transmitterIdLastTwo = Extensions.lastTwoCharactersOfString(defaultTransmitter.transmitterId);
                    final String deviceNameLastTwo = Extensions.lastTwoCharactersOfString(device.getName());

                    if (transmitterIdLastTwo.equals(deviceNameLastTwo)) {
                        isBondedOrBonding = true;
                        isBonded=true;
                        if (!previousBondedState) Log.e(TAG,"Device is now detected as bonded!");
                    // TODO should we break here for performance?
                    } else {
                        isIntialScan = true;
                    }
                }
            }
        }
        if (previousBondedState && !isBonded) Log.e(TAG,"Device is no longer detected as bonded!");
        Log.d(TAG, "getTransmitterDetails() result: Bonded? " + isBondedOrBonding.toString()+(isBonded ? " localed bonded" : " not locally bonded"));
    }

    private static boolean shouldServiceRun() {
        final boolean result = CollectionServiceStarter.isBTG5(xdrip.getAppContext()) && PersistentStore.getBoolean(CollectionServiceStarter.pref_run_wear_collector);
        Log.d(TAG, "shouldServiceRun() returning: " + result);
        return result;
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        isScanning = true;//enable to ensure scanning is stopped to prevent service from starting back up onScanResult()
        stopScan();
        isScanning = false;

        Log.d(TAG, "onDestroy");
        //SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        scan_interval_timer.cancel();
        if (pendingIntent != null && !shouldServiceRun()) {
            Log.d(TAG, "onDestroy stop Alarm pendingIntent");
            AlarmManager alarm = (AlarmManager) getSystemService(ALARM_SERVICE);
            alarm.cancel(pendingIntent);
        }
        // close gatt
        if (mGatt != null) {//KS
            try {
                mGatt.close();
            } catch (NullPointerException e) {
                Log.d(TAG, "concurrency related null pointer exception in close");
            }
        }

        // TODO do we need to gatt disconnect or close??
//        close();
//        setRetryTimer();
//        foregroundServiceStarter.stop();
//        unregisterReceiver(mPairReceiver);
        try {
            unregisterReceiver(mPairReceiver);
        } catch (Exception e) {
            Log.e(TAG, "Got exception unregistering bonding receiver: ", e);
        }
        try {
            unregisterReceiver(mPairingRequestRecevier);
        } catch (Exception e) {
            Log.e(TAG, "Got exception unregistering pairing receiver: ", e);
        }
//        BgToSpeech.tearDownTTS();
        Log.i(TAG, "SERVICE STOPPED");
        lastState="Stopped";
    }

    public synchronized void keepAlive() {
        keepAlive(0);
    }

    public synchronized void keepAlive(int wake_in_ms) {
        Log.d(TAG,"keepAlive keep_running=" + keep_running);
        if (!keep_running) return;
        if (JoH.ratelimit("G5-keepalive", 5)) {
            long wakeTime;
            if (wake_in_ms==0) {
                wakeTime = getNextAdvertiseTime() - 60 * 1000;
            } else {
                wakeTime = Calendar.getInstance().getTimeInMillis() + wake_in_ms;
            }
            nextWakeUpTime = wakeTime;//Benchmark test

            //Log.e(TAG, "Delay Time: " + minuteDelay);
            Log.e(TAG, "Scheduling Wake Time: in " +  JoH.qs((wakeTime-JoH.tsl())/1000,0)+ " secs "+ JoH.dateTimeText(wakeTime));
            AlarmManager alarm = (AlarmManager) getSystemService(ALARM_SERVICE);
            if (pendingIntent != null)
                alarm.cancel(pendingIntent);
            pendingIntent = PendingIntent.getService(this, 0, new Intent(this, this.getClass()), 0);
            // TODO use wakeIntent feature
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                alarm.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent);
            } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                alarm.setExact(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent);
            } else
                alarm.set(AlarmManager.RTC_WAKEUP, wakeTime, pendingIntent);
        } else {
            Log.e(TAG, "Ignoring keepalive call due to ratelimit");
        }
    }


    @Override
    public IBinder onBind(Intent intent) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    public void setupBluetooth() {

        getTransmitterDetails();
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            //First time using the app or bluetooth was turned off?
            Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
            Timer single_timer = new Timer();
            single_timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    if (mBluetoothAdapter != null) mBluetoothAdapter.enable();
                }
            }, 1000);
            single_timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    setupBluetooth();
                }
            }, 10000);
        } else {
            if (Build.VERSION.SDK_INT >= 21) {
                mLEScanner = mBluetoothAdapter.getBluetoothLeScanner();
                settings = new ScanSettings.Builder()
                        .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
                        .build();
                filters = new ArrayList<>();
                //Only look for CGM.
                //filters.add(new ScanFilter.Builder().setServiceUuid(new ParcelUuid(BluetoothServices.Advertisement)).build());
                String transmitterIdLastTwo = Extensions.lastTwoCharactersOfString(defaultTransmitter.transmitterId);
                filters.add(new ScanFilter.Builder().setDeviceName("Dexcom" + transmitterIdLastTwo).build());
            }

            // unbond here to avoid clashes when we are mid-connection
            if (alwaysUnbond()) {
                forgetDevice();
            }
            JoH.ratelimit("G5-timeout",0);//re-init to ensure onStartCommand always executes cycleScan
            cycleScan(0);
        }
    }

    public synchronized void stopScan(){
        if (!isScanning) {
            Log.d(TAG, "alreadyStoppedScanning");
            return;
        }
        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                mBluetoothAdapter.stopLeScan(mLeScanCallback);
            } else {

                try {

                    if (enforceMainThread()){
                        Handler iHandler = new Handler(Looper.getMainLooper());
                        iHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                stopLogic();
                            }
                        });
                    } else {
                        stopLogic();;
                    }


                } catch (NullPointerException e) {
                    //Known bug in Samsung API 21 stack
                    Log.e(TAG,"stopscan() Caught the NullPointerException");
                }
            }
        }
    }

    private synchronized void stopLogic() {
        try {
            Log.e(TAG, "stopScan");
            try {
                mLEScanner.stopScan(mScanCallback);
            } catch (NullPointerException | IllegalStateException e) {
                Log.e(TAG, "Exception in stopLogic: " + e);
            }
            isScanning = false;
        } catch (IllegalStateException is) {

        }
    }

    public synchronized void cycleScan(int delay) {

        Log.d(TAG,"cycleScan keep_running=" + keep_running);
        if (!keep_running) {
            Log.e(TAG," OnDestroy failed to stop service. Shutting down now to prevent service from being initiated onScanResult().");
            stopSelf();
            return;
        }
        if (JoH.ratelimit("G5-timeout",60) || !scan_scheduled) {
            if (JoH.ratelimit("g5-scan-log",60)) {
                Log.d(TAG, "cycleScan running");
            }
            scan_scheduled=true;
            //Log.e(TAG, "Scheduling cycle scan, delay: " + delay);
            final Timer single_timer = new Timer();
            single_timer.schedule(new TimerTask() {
                @Override
                public void run() {
                    if (scanConstantly()) {
                        startScan();
                    } else {
                        if (mBluetoothAdapter != null && mBluetoothAdapter.isEnabled()) {
                            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                                mBluetoothAdapter.stopLeScan(mLeScanCallback);
                            } else {

                                try {
                                    if (enforceMainThread()) {
                                        Handler iHandler = new Handler(Looper.getMainLooper());
                                        iHandler.post(new Runnable() {
                                            @Override
                                            public void run() {
                                                scanLogic();
                                            }
                                        });

                                    } else {
                                        scanLogic();
                                    }
                                } catch
                                        (NullPointerException e) {
                                    //Known bug in Samsung API 21 stack
                                    Log.e(TAG,"Caught the NullPointerException in cyclescan");
                                } finally {
                                    scan_scheduled=false;
                                }
                            }
                        }
                    }
                    scan_scheduled=false;
                }
            }, delay);
        } else {
            Log.e(TAG,"jamorham blocked excessive scan schedule");
        }
    }

    private synchronized void scanLogic() {
        Log.d(TAG,"scanLogic keep_running=" + keep_running);
        if (!keep_running) return;

        if (alwaysOnScreem()) {
            Log.d(TAG, "scanLogic call forceScreenOn");
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        forceScreenOn();
                    }
                });
            } else {
                forceScreenOn();
            }
        }
        if (JoH.ratelimit("G5-scanlogic", 1)) {//KS test change 2 -> 1 to support restart collecter after 6 min missed readings
            try {
                mLEScanner.stopScan(mScanCallback);
                isScanning = false;
                if (!isConnected) {
                    mLEScanner.startScan(filters, settings, mScanCallback);
                    lastState="Scanning";
                    if (JoH.ratelimit("g5-scan-log",60)) {
                        Log.w(TAG, "scan cycle start");
                    }
                }
                isScanning = true;
            } catch (IllegalStateException | NullPointerException is) {
                setupBluetooth();
            }


            scanCycleCount++;

            //Log.e(TAG, "MSSinceSensorRx: " + getMillisecondsSinceLastSuccesfulSensorRead());
            //if it isn't the initial scan, rescan for maxScanCycles
            if (!isIntialScan && scanCycleCount > maxScanCycles) {
                scan_interval_timer.cancel();
                scan_interval_timer = new Timer();
                scan_interval_timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        //Log.e(TAG, "cycling scan to stop until expected advertisement");
                        if (isScanning) {
                            keepAlive();
                        }
                        stopScan();
                    }
                }, maxScanIntervalInMilliseconds);
            }
            //last ditch
            else if (!isIntialScan && getMillisecondsSinceLastSuccesfulSensorRead() > 11 * 60 * 1000) {
                Log.e(TAG, "MSSinceSensorRx: " + getMillisecondsSinceLastSuccesfulSensorRead());
                isIntialScan = true;
                cycleBT();
            }
            //startup or re-auth, sit around and wait for tx to advertise
            else {
                scan_interval_timer.cancel();
                scan_interval_timer = new Timer();
                scan_interval_timer.schedule(new TimerTask() {
                    @Override
                    public void run() {
                        //Log.e(TAG, "cycling scan");
                        cycleScan(0);
                    }
                }, maxScanIntervalInMilliseconds);
            }
        }
    }

    private synchronized void checkWakeupTimeLatency() {
        if (nextWakeUpTime > 0) {
            wake_time_difference = Calendar.getInstance().getTimeInMillis() - nextWakeUpTime;
            if (wake_time_difference > 10000) {
                UserError.Log.e(TAG, "Slow Wake up! time difference in ms: " + wake_time_difference);
                wakeUpErrors = wakeUpErrors + 3;
            } else {
                if (wakeUpErrors > 0) wakeUpErrors--;
            }
        }
    }

    private void logWakeTimeLatency() {
        if (wakeUpErrors > 0) {
            Log.e(TAG, "Slow Wake up: " + JoH.niceTimeScalar(wake_time_difference));
            Log.e(TAG, "Wake Up Errors: " + wakeUpErrors);
        }

        if (nextWakeUpTime != -1) {
            Log.e(TAG, "Next Wake up: " + JoH.dateTimeText(nextWakeUpTime));

        }
    }

    private synchronized void forceScreenOn() {
        //Home.startHomeWithExtra(getApplicationContext(), Home.HOME_FULL_WAKEUP, "1");
        final int timeout = (3 * 60 * 1000);
        Log.d(TAG, "forceScreenOn set wakelock for FULL_WAKE_LOCK");
        if (fullWake == null || !fullWake.isHeld()) {
            UserError.Log.d(TAG, "Current time: " + JoH.dateTimeText(JoH.tsl()));
            fullWake = JoH.getWakeLock(PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP, "FORCE_FULL_WAKE_LOCK", timeout);
        }
        else {
            Log.e(TAG, "forceScreenOn fullWake is already held!");
        }
    }

    public synchronized void startScan() {
        UserError.Log.e(TAG, "Initial scan?" + isIntialScan);
        if (isScanning) {
            Log.d(TAG, "alreadyScanning");
            scan_interval_timer.cancel();
            Log.d(TAG,"startScan keep_running=" + keep_running);
            if (!keep_running) return;
            return;
        }

        Log.d(TAG,"startScan keep_running=" + keep_running);
        if (!keep_running) return;

        if (alwaysOnScreem()) {
            Log.e(TAG, "startScan call forceScreenOn");
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        forceScreenOn();
                    }
                });
            } else {
                forceScreenOn();
            }
        }

        getTransmitterDetails();
        if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
            setupBluetooth();
        } else {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                setupLeScanCallback();
                mBluetoothAdapter.startLeScan(new UUID[]{BluetoothServices.Advertisement}, mLeScanCallback);
            } else {
                if (enforceMainThread()){
                    Handler iHandler = new Handler(Looper.getMainLooper());
                    iHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            startLogic();
                        }
                    });
                } else {
                    startLogic();
                }
                Log.e(TAG, "startScan normal");
            }
        }
    }

    private synchronized void startLogic() {
        try {
            isScanning = true;
            mLEScanner.startScan(filters, settings, mScanCallback);
        } catch (Exception e) {
            isScanning = false;
            setupBluetooth();
        }
    }

    private synchronized void cycleBT(boolean t) {
        Log.e(TAG, "cycleBT special: count:" + disconnected133 + " / "+ disconnected59);
        if ((disconnected133 < 2) && (disconnected59 < 2)) {
            cycleBT();
        } else {
            Log.e(TAG, "jamorham special restart");
            keepAlive(10000); // retry in 10 seconds

            // close gatt
            if (mGatt != null) {
                try {
                    mGatt.close();
                } catch (NullPointerException e) {
                    Log.d(TAG, "concurrency related null pointer exception in close");
                }
            }
            disconnected133 = 0;
            disconnected59 = 0;
            stopSelf();
        }
    }

    private synchronized void cycleBT() {
        synchronized (short_lock) {
            if (JoH.ratelimit("cyclebt", 20)) {

                // TODO cycling_bt not used as never set to true - rate limit any sync used instead
                if (cycling_bt) {
                    Log.e(TAG, "jamorham Already concurrent BT cycle in progress!");
                    return;
                }
                encountered133 = false;
                stopScan();
                if (g5BluetoothWatchdog()) {
                    Log.e(TAG, "Cycling BT-gatt - disabling BT");
                    mBluetoothAdapter.disable();
                    Timer single_timer = new Timer();
                    single_timer.schedule(new TimerTask() {
                        @Override
                        public void run() {
                            mBluetoothAdapter.enable();
                            Log.e(TAG, "Cycling BT-gatt - enabling BT");
                            cycling_bt = false;
                        }
                    }, 3000);
                } else {
                    Log.e(TAG, "Wanted to cycle g5 bluetooth but is disabled in advanced bluetooth preferences!");
                    waitFor(3000);
                }
            }
            keepAlive();
        }
    }

    private synchronized void forgetDevice() {
        Log.d(TAG,"forgetDevice() start");
        final Transmitter defaultTransmitter = new Transmitter(prefs.getString("dex_txid", "ABCDEF")); // should be cached?
        mBluetoothAdapter = mBluetoothManager.getAdapter();
        final Set<BluetoothDevice> pairedDevices = mBluetoothAdapter.getBondedDevices();
        if (pairedDevices.size() > 0) {
            for (BluetoothDevice device : pairedDevices) {
                if (device.getName() != null) {

                    final String transmitterIdLastTwo = Extensions.lastTwoCharactersOfString(defaultTransmitter.transmitterId);
                    final String deviceNameLastTwo = Extensions.lastTwoCharactersOfString(device.getName());
                    //Log.e(TAG, "removeBond: "+transmitterIdLastTwo+" vs "+deviceNameLastTwo);
                    if (transmitterIdLastTwo.equals(deviceNameLastTwo)) {
                        try {
                            Log.e(TAG, "removingBond: "+transmitterIdLastTwo+" vs "+deviceNameLastTwo);
                            Method m = device.getClass().getMethod("removeBond", (Class[]) null);
                            m.invoke(device, (Object[]) null);
                            getTransmitterDetails();
                        } catch (Exception e) { Log.e(TAG, e.getMessage(), e); }
                    }

                }
            }
        }
        Log.d(TAG,"forgetDevice() finished");
    }

    // API 18 - 20
    //@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
    private void setupLeScanCallback() {
        if (mLeScanCallback == null) {
            mLeScanCallback = new BluetoothAdapter.LeScanCallback() {
                @Override
                public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
                    // Check if the device has a name, the Dexcom transmitter always should. Match it with the transmitter id that was entered.
                    // We get the last 2 characters to connect to the correct transmitter if there is more than 1 active or in the room.
                    // If they match, connect to the device.
                    if (device.getName() != null) {
                        String transmitterIdLastTwo = Extensions.lastTwoCharactersOfString(defaultTransmitter.transmitterId);
                        String deviceNameLastTwo = Extensions.lastTwoCharactersOfString(device.getName());

                        if (transmitterIdLastTwo.toUpperCase().equals(deviceNameLastTwo.toUpperCase())) {
                            connectToDevice(device);
                        }
                    }
                }
            };
        }
    }

    private ScanCallback mScanCallback;

    //@TargetApi(21)
    private void initScanCallback(){
        mScanCallback = new ScanCallback() {
            @Override
            public void onScanResult(int callbackType, ScanResult result) {
                UserError.Log.i(TAG, "result: " + result.toString());
                BluetoothDevice btDevice = result.getDevice();
//                // Check if the device has a name, the Dexcom transmitter always should. Match it with the transmitter id that was entered.
//                // We get the last 2 characters to connect to the correct transmitter if there is more than 1 active or in the room.
//                // If they match, connect to the device.
                if (btDevice.getName() != null) {
                    String transmitterIdLastTwo = Extensions.lastTwoCharactersOfString(defaultTransmitter.transmitterId);
                    String deviceNameLastTwo = Extensions.lastTwoCharactersOfString(btDevice.getName());

                    if (transmitterIdLastTwo.equals(deviceNameLastTwo)) {
                        if (advertiseTimeMS.size() > 0)
                            if ((new Date().getTime() - advertiseTimeMS.get(advertiseTimeMS.size()-1)) > 2.5*60*1000)
                                advertiseTimeMS.clear();
                        advertiseTimeMS.add(new Date().getTime());
                        isIntialScan = false;
                        //device = btDevice;
                        device = mBluetoothAdapter.getRemoteDevice(btDevice.getAddress());
                        if (fullWake != null) JoH.releaseWakeLock(fullWake);
                        stopScan();
                        connectToDevice(btDevice);
                    } else {
                        //stopScan(10000);
                    }
                }
            }

            @Override
            public void onScanFailed(int errorCode) {
                Log.e(TAG, "Scan Failed Error Code: " + errorCode);
                if (fullWake != null) JoH.releaseWakeLock(fullWake);
                if (errorCode == 1) {
                    UserError.Log.e(TAG, "Already Scanning: " + isScanning);
                    //isScanning = true;
                } else if (errorCode == 2){
                    cycleBT();
                }
            }
        };
    }

    public synchronized void fullAuthenticate() {
        Log.e(TAG, "fullAuthenticate() start");
        if (alwaysUnbond()) {
            forgetDevice();
        }
        try {
            Log.i(TAG, "Start Auth Process(fullAuthenticate)");
            if (authCharacteristic != null) {
                sendAuthRequestTxMessage(mGatt, authCharacteristic);
            } else {
                Log.e(TAG, "fullAuthenticate: authCharacteristic is NULL!");
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Got null pointer in fullAuthenticate: " + e);
        }
    }

    public synchronized void authenticate() {
        Log.e(TAG,"authenticate() start");
        try {
            mGatt.setCharacteristicNotification(authCharacteristic, true);
            if (!mGatt.readCharacteristic(authCharacteristic)) {
                Log.e(TAG, "onCharacteristicRead : ReadCharacteristicError");
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Got Nullpointer exception in authenticate(): " + e);
        }
    }

    public synchronized void getSensorData() {
        Log.i(TAG, "Request Sensor Data");
        try {
            if (mGatt != null) {
                mGatt.setCharacteristicNotification(controlCharacteristic, true);
                final BluetoothGattDescriptor descriptor = controlCharacteristic.getDescriptor(BluetoothServices.CharacteristicUpdateNotification);
                descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE);
                if (useG5NewMethod()) {
                    // new style
                    GlucoseTxMessage glucoseTxMessage = new GlucoseTxMessage();
                    controlCharacteristic.setValue(glucoseTxMessage.byteSequence);
                } else {
                    // old style
                    SensorTxMessage sensorTx = new SensorTxMessage();
                    controlCharacteristic.setValue(sensorTx.byteSequence);
                }
                Log.d(TAG,"getSensorData(): writing desccrptor");
                mGatt.writeDescriptor(descriptor);
            } else {
                Log.e(TAG,"getSensorData() mGatt was null");
            }
        } catch (NullPointerException e) {
            Log.e(TAG, "Got null pointer in getSensorData() " + e);
        }
    }

    private BluetoothAdapter.LeScanCallback mLeScanCallback = null;

    private synchronized void connectToDevice(BluetoothDevice device) {
        if (JoH.ratelimit("G5connect-rate", 2)) {

            Log.d(TAG, "connectToDevice() start");
            if (mGatt != null) {
                Log.i(TAG, "BGatt isnt null, Closing.");
                try {
                    mGatt.close();
                } catch (NullPointerException e) {
                    // concurrency related null pointer
                }
                mGatt = null;
            }
            Log.i(TAG, "Request Connect");
            final BluetoothDevice mDevice = device;
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        connectGatt(mDevice);
                    }
                });
            } else {
                connectGatt(mDevice);
            }

        } else {
            Log.e(TAG, "connectToDevice baulking due to rate-limit");
        }
    }

    private synchronized void connectGatt(BluetoothDevice mDevice) {
        Log.i(TAG, "mGatt Null, connecting...");
        Log.i(TAG, "connectToDevice On Main Thread? " + isOnMainThread());
        lastState="Found, Connecting";
        if (delayOn133Errors && max133RetryCounter > 1) {
            // should we only be looking at disconnected 133 here?
            Log.e(TAG, "Adding a delay before connecting to 133 count of: " + max133RetryCounter);
            waitFor(600);
            Log.e(TAG, "connectGatt() delay completed");
        }
        mGatt = mDevice.connectGatt(getApplicationContext(), false, gattCallback);//TEST false -> true
    }


    // Sends the disconnect tx message to our bt device.
    private synchronized void doDisconnectMessage(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
           Log.d(TAG, "doDisconnectMessage() start");
           gatt.setCharacteristicNotification(controlCharacteristic, false);
           final DisconnectTxMessage disconnectTx = new DisconnectTxMessage();
           characteristic.setValue(disconnectTx.byteSequence);
           gatt.writeCharacteristic(characteristic);
           gatt.disconnect();
           Log.d(TAG, "doDisconnectMessage() finished");
    }


    private synchronized void doVersionRequestMessage(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        Log.d(TAG, "doVersionRequestMessage() start");
        final VersionRequestTxMessage versionTx = new VersionRequestTxMessage();
        characteristic.setValue(versionTx.byteSequence);
        gatt.writeCharacteristic(characteristic);
        Log.d(TAG, "doVersionRequestMessage() finished");
    }

    private synchronized void doBatteryInfoRequestMessage(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        Log.d(TAG, "doBatteryInfoMessage() start");
        characteristic.setValue(new BatteryInfoTxMessage().byteSequence);
        gatt.writeCharacteristic(characteristic);
        Log.d(TAG, "doBatteryInfoMessage() finished");
    }

    private synchronized void discoverServices() {
        if (JoH.ratelimit("G5-discservices", 2)) {

            Log.i(TAG, "discoverServices() started " + (isOnMainThread() ? "on main thread" : "not on main thread"));
            if (mGatt != null) {
                if (delayOn133Errors && max133RetryCounter > 1) {
                    // should we only be looking at disconnected 133 here?
                    Log.e(TAG, "Adding a delay before discovering services due to 133 count of: " + max133RetryCounter);
                    waitFor(1600);
                }
                mGatt.discoverServices();
            } else {
                Log.e(TAG, "discoverServices: mGatt is null");
            }
        } else {
            Log.e(TAG, "discoverServices rate limited!");
        }
    }

    // big bluetooth gatt callback
    private final BluetoothGattCallback gattCallback = new BluetoothGattCallback() {

        @Override
        public void onConnectionStateChange(final BluetoothGatt gatt, final int status, final int newState) {
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                                  @Override
                                  public void run() {
                                      processOnStateChange(gatt, status, newState);
                                  }
                              }
                );
            } else {
                processOnStateChange(gatt, status, newState);
            }
        }


        private synchronized void processOnStateChange(final BluetoothGatt gatt, final int status, final int newState) {
            switch (newState) {


                case BluetoothProfile.STATE_CONNECTED:
                    Log.e(TAG, "STATE_CONNECTED");
                    isConnected = true;

                    // TODO we should already be on the correct thread
                    if (enforceMainThread()) {
                        if (!isOnMainThread()) {
                            Log.d(TAG, "We are not on the main thread so this section is still needed!!");
                        }
                        Handler iHandler = new Handler(Looper.getMainLooper());
                        iHandler.post(new Runnable() {
                            @Override
                            public void run() {
                                discoverServices();
                            }
                        });
                    } else {
                        discoverServices();
                    }


                    stopScan();
                    scan_interval_timer.cancel();
                    keepAlive();
                    break;


                case BluetoothProfile.STATE_DISCONNECTED:
                    isConnected = false;
                    if (isScanning) {
                        stopScan();
                    }
                    Log.e(TAG, "STATE_DISCONNECTED: " + getStatusName(status));

                    // do we keep failing right after attempting bonding? make sure alwaysAuthenticate is enabled if so..
                    if (status == BluetoothServices.GATT_CONN_TERMINATE_PEER_USER) {
                        failures++;
                        if (!alwaysAuthenticate() && (successes == 0) && (failures > 1) && (lastOnReadCode == 7)) {
                            Log.wtf(TAG, "Force enabling AlwaysAuthenticate mode!");
                            force_always_authenticate = true;
                        }
                    }

                    if (mGatt != null) {
                        try {
                            mGatt.close();
                        } catch (NullPointerException e) { //
                        }
                    }

                    mGatt = null;
                    if (status == 0 && !encountered133) {// || status == 59) {
                        Log.i(TAG, "clean disconnect");
                        max133RetryCounter = 0;
                        if (scanConstantly())
                            cycleScan(15000);
                    } else if (status == 133 || max133RetryCounter >= max133Retries) {
                        Log.e(TAG, "max133RetryCounter? " + max133RetryCounter);
                        Log.e(TAG, "Encountered 133: " + encountered133);
                        max133RetryCounter = 0;
                        disconnected133++;
                        cycleBT(true);
                    } else if (encountered133) {
                        Log.e(TAG, "max133RetryCounter? " + max133RetryCounter);
                        Log.e(TAG, "Encountered 133: " + encountered133);
                        if (scanConstantly())
                            startScan();
                        else
                            cycleScan(0);
                        max133RetryCounter++;
                    } else if (status == 129) {
                        Log.d(TAG, "Forgetting device due to status: " + status);
                        forgetDevice();
                    } else {
                        if (status == 59) {
                            disconnected59++;
                        }
                        if (disconnected59 > 2) {
                            cycleBT(true);
                        } else {
                            if (scanConstantly())
                                startScan();
                            else
                                cycleScan(0);
                            max133RetryCounter = 0;
                        }
                    }
                    break;


                default:
                    Log.e(TAG, "STATE_OTHER: " + newState);
            }
        }


        @Override
        public synchronized void onServicesDiscovered(final BluetoothGatt gatt, final int status) {
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processOnServicesDiscovered(gatt, status);
                    }
                });
            } else {
                processOnServicesDiscovered(gatt, status);
            }
        }

        private synchronized void processOnServicesDiscovered(final BluetoothGatt gatt, final int status) {
            Log.i(TAG, "onServicesDiscovered On Main Thread? " + isOnMainThread());
            Log.e(TAG, "onServicesDiscovered: " + getStatusName(status));
            if (status == BluetoothGatt.GATT_SUCCESS) {
                if (mGatt != null) {
                    try {
                        cgmService = mGatt.getService(BluetoothServices.CGMService);
                        if (cgmService != null) {
                            authCharacteristic = cgmService.getCharacteristic(BluetoothServices.Authentication);
                            controlCharacteristic = cgmService.getCharacteristic(BluetoothServices.Control);
                            // TODO can we remove the below comm line
                            //commCharacteristic = cgmService.getCharacteristic(BluetoothServices.Communication);
                        }
                    } catch (NullPointerException e) {
                        Log.e(TAG, "Got Null pointer in OnServices discovered 2");
                    }
                    mBluetoothAdapter.cancelDiscovery();
                }

                //TODO : ADD option in settings!
                if (alwaysAuthenticate() || alwaysUnbond()) {
                    fullAuthenticate();
                } else {
                    authenticate();
                }

            } else {
                Log.w(TAG, "onServicesDiscovered received error status: " + getStatusName(status));
            }

            if (status == 129) {//KS
                Log.w(TAG,"forgetDevice and stop service");
                forgetDevice();
                stopSelf();
            }
            if (status == 133) {
                encountered133 = true;
            }
        }

        @Override
        public void onDescriptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
            Log.e(TAG, "OnDescriptor WRITE started: status: " + getStatusName(status));
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processonDescrptorWrite(gatt, descriptor, status);
                    }
                });
            } else {
                processonDescrptorWrite(gatt, descriptor, status);
            }
        }

        private void processonDescrptorWrite(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) {
            Log.i(TAG, "onDescriptorWrite On Main Thread? " + isOnMainThread());
            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "Writing to characteristic: " + getUUIDName(descriptor.getCharacteristic().getUuid()));
                if (mGatt != null) {
                    mGatt.writeCharacteristic(descriptor.getCharacteristic());
                } else {
                    Log.e(TAG, "mGatt was null when trying to write UUID descriptor");
                }
            } else {
                Log.e(TAG, "not writing characteristic due to Unknown error writing descriptor");
            }

            if (status == 133) {
                encountered133 = true;
            }
            Log.e(TAG, "OnDescriptor WRITE finished: status: " + getStatusName(status));
        }

        @Override
        public void onCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
            Log.e(TAG, "OnCharacteristic WRITE started: "
                    + getUUIDName(characteristic.getUuid())
                    + " status: " + getStatusName(status));
            //Log.e(TAG, "Write Status " + String.valueOf(status));
            //Log.e(TAG, "Characteristic " + String.valueOf(characteristic.getUuid()));

            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processOnCharacteristicWrite(gatt, characteristic, status);
                    }
                });
            } else {
                processOnCharacteristicWrite(gatt, characteristic, status);
            }


        }

        private synchronized void processOnCharacteristicWrite(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
            Log.i(TAG, "processOnCharacteristicWrite On Main Thread? " + isOnMainThread());
            if (status == BluetoothGatt.GATT_SUCCESS) {
                // is this being written to the auth characterstic?
                if (String.valueOf(characteristic.getUuid()).equalsIgnoreCase(String.valueOf(authCharacteristic.getUuid()))) {
                    Log.i(TAG, "Auth ow Char Value: " + Arrays.toString(characteristic.getValue()));
                    Log.i(TAG, "Auth ow auth? name: " + getUUIDName(characteristic.getUuid()));
                    if (characteristic.getValue() != null) {
                        Log.e(TAG, "Auth ow: got opcode: " + characteristic.getValue()[0]);
                        if (characteristic.getValue()[0] != KeepAliveTxMessage.opcode) { /* opcode keepalive? */
                            if (delayOn133Errors && max133RetryCounter > 1) {
                                // should we only be looking at disconnected 133 here?
                                Log.e(TAG, "Adding a delay before reading characteristic with 133 count of: " + max133RetryCounter);
                                waitFor(300);
                            }
                            if (mGatt != null) {
                                mGatt.readCharacteristic(characteristic);
                            } else {
                                Log.e(TAG, "mGatt was null when trying to read KeepAliveTxMessage");
                            }
                        } else {
                            Log.e(TAG, "Auth ow: got keepalive");
                            if (useKeepAlive) {
                                Log.e(TAG, "Keepalive written, now trying bond");
                                performBondWrite(characteristic);
                            }
                        }
                    } else {
                        Log.e(TAG, "Auth ow: got NULL opcode!");
                    }
                } else {
                    Log.i(TAG, "ow unexpected? characteristic: "+ getUUIDName(characteristic.getUuid()));
                  //  Log.i(TAG, "ow status? " + status);
                }
            }

            if (status == 133) {
                encountered133 = true;
            }
            Log.e(TAG, "OnCharacteristic WRITE finished: status: " + getStatusName(status));
        }

        @Override
        public void onCharacteristicRead(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic, final int status) {
            Log.e(TAG, "OnCharacteristic READ started: " + getUUIDName(characteristic.getUuid()) + " status: " + status);
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processOnCharacteristicRead(gatt, characteristic, status);
                    }
                });
            } else {
                processOnCharacteristicRead(gatt, characteristic, status);
            }
        }

        private synchronized void performBondWrite(BluetoothGattCharacteristic characteristic)
        {
            Log.d(TAG,"performBondWrite() started");
            final BondRequestTxMessage bondRequest = new BondRequestTxMessage();
            characteristic.setValue(bondRequest.byteSequence);
            if (mGatt != null) {
                mGatt.writeCharacteristic(characteristic);
            } else {
                Log.e(TAG, "mGatt was null when trying to write bondRequest");
            }
            if (delayOnBond) {
                Log.e(TAG, "Delaying before bond");
                waitFor(1000);
                Log.e(TAG, "Delay finished");
            }
            isBondedOrBonding = true;
            device.createBond();
            Log.d(TAG,"performBondWrite() finished");
        }

        private synchronized void processOnCharacteristicRead (BluetoothGatt gatt,
                                                  final BluetoothGattCharacteristic characteristic, final int status)
        {
            Log.e(TAG, "processOnCRead: Status value: " + getStatusName(status) + (isOnMainThread() ? " on main thread" : " not on main thread"));

            if (status == BluetoothGatt.GATT_SUCCESS) {
                Log.e(TAG, "CharBytes-or " + Arrays.toString(characteristic.getValue()));
                Log.i(TAG, "CharHex-or " + Extensions.bytesToHex(characteristic.getValue()));

                final byte[] buffer = characteristic.getValue();

                if (buffer.length == 0) {
                    Log.e(TAG, "OnCharacteristic READ Got ZERO sized buffer: status: " + getStatusName(status));
                    return;
                }

                byte code = buffer[0];
                //Transmitter defaultTransmitter = new Transmitter(prefs.getString("dex_txid", "ABCDEF"));
                Log.e(TAG,"processOncRead: code:"+code);
                mBluetoothAdapter = mBluetoothManager.getAdapter();
                lastOnReadCode = code;
                switch (code) {
                    case 5:
                        authStatus = new AuthStatusRxMessage(buffer);

                        // TODO KS check here
                        if (authStatus.authenticated == 1 && authStatus.bonded == 1 && !isBondedOrBonding) {
                            Log.e(TAG, "Special bonding test case!");

                            if (tryPreBondWithDelay) {
                                Log.e(TAG,"Trying prebonding with delay!");
                                isBondedOrBonding = true;
                                device.createBond();
                                waitFor(1600);
                                Log.e(TAG,"Prebond delay finished");
                            }

                            getTransmitterDetails(); // try to refresh on the off-chance
                        }

                        if (ignoreLocalBondingState) Log.e(TAG,"Ignoring local bonding state!!");


                        if (authStatus.authenticated == 1 && authStatus.bonded == 1 && (isBondedOrBonding || ignoreLocalBondingState)) {
                            // TODO check bonding logic here and above
                            isBondedOrBonding = true; // statement has no effect?
                            getSensorData();
                        } else if ((authStatus.authenticated == 1 && authStatus.bonded == 2)
                                || (authStatus.authenticated == 1 && authStatus.bonded == 1 && !isBondedOrBonding)) {
                            Log.i(TAG, "Let's Bond! " + (isBondedOrBonding ? "locally bonded" : "not locally bonded"));

                            if (useKeepAlive) {
                                Log.e(TAG,"Trying keepalive..");
                                final KeepAliveTxMessage keepAliveRequest = new KeepAliveTxMessage(25);
                                characteristic.setValue(keepAliveRequest.byteSequence);
                                if (mGatt != null) {
                                    mGatt.writeCharacteristic(characteristic);
                                } else {
                                    Log.e(TAG, "mGatt was null when trying to write keepAliveRequest");
                                }
                            } else {
                             performBondWrite(characteristic);
                            }
                        } else {
                            Log.i(TAG, "Transmitter NOT already authenticated");
                            sendAuthRequestTxMessage(gatt, characteristic);
                        }
                        break;

                    case 3:
                        AuthChallengeRxMessage authChallenge = new AuthChallengeRxMessage(characteristic.getValue());
                        if (authRequest == null) {
                            authRequest = new AuthRequestTxMessage(getTokenSize());
                        }
                        Log.i(TAG, "tokenHash " + Arrays.toString(authChallenge.tokenHash));
                        Log.i(TAG, "singleUSe " + Arrays.toString(calculateHash(authRequest.singleUseToken)));

                        byte[] challengeHash = calculateHash(authChallenge.challenge);
                        Log.d(TAG, "challenge hash" + Arrays.toString(challengeHash));
                        if (challengeHash != null) {
                            Log.d(TAG, "Transmitter try auth challenge");
                            AuthChallengeTxMessage authChallengeTx = new AuthChallengeTxMessage(challengeHash);
                            Log.i(TAG, "Auth Challenge: " + Arrays.toString(authChallengeTx.byteSequence));
                            characteristic.setValue(authChallengeTx.byteSequence);
                            if (mGatt != null) {
                                mGatt.writeCharacteristic(characteristic);
                            } else {
                                Log.e(TAG, "mGatt was null when trying to write in opcode 3 reply");
                            }
                        }
                        break;

                    //case 7:
                    //    Log.d(TAG,"Received Bond request - trying bond");
                    //    isBondedOrBonding = true;
                    //   Log.e(TAG,"Bond state pre: "+device.getBondState());
                    //    device.createBond();
                    //    Log.e(TAG,"Bond state post: "+device.getBondState());
                    //    break;

                    default:
                        if ((code == 7) && (delayOnBond)) {
                            Log.e(TAG, "Delaying response to onRead for code: " + code);
                            waitFor(1500);
                            Log.e(TAG, "Delayed response to onRead finished");
                        }

                        if ((code == 7) && (tryOnDemandBondWithDelay)) {
                            Log.e(TAG,"Trying ondemand bond with delay!");
                            isBondedOrBonding = true;
                            waitingBondConfirmation = 1; // waiting
                            device.createBond();

                            if (simpleBondWait) {
                                Log.e(TAG, "Using simple wait for 15 secs");
                                waitFor(15000); // are we ok to do this on this thread?
                            } else {
                                for (int counter = 0; counter < 12; counter++) {
                                    if (waitingBondConfirmation != 1) {
                                        Log.e(TAG, "Received bond confirmation after: " + counter + " seconds. status: " + waitingBondConfirmation);
                                        waitFor(5000); // extra delay
                                        break;
                                    } else {
                                        waitFor(1000);
                                    }
                                }
                            }

                            Log.e(TAG,"ondemandbond delay finished");
                        }

                        Log.i(TAG, "Read code: " + code + " - Transmitter NOT already authenticated?");
                        sendAuthRequestTxMessage(gatt, characteristic);
                        break;
                }

            }

            if (status == 133) {
                encountered133 = true;
            }
            Log.e(TAG, "OnCharacteristic READ finished: status: " + getStatusName(status));
        }



        @Override
        // Characteristic notification
        public void onCharacteristicChanged(final BluetoothGatt gatt, final BluetoothGattCharacteristic characteristic) {
            Log.e(TAG, "OnCharacteristic CHANGED started: " + getUUIDName(characteristic.getUuid()));
            if (enforceMainThread()) {
                Handler iHandler = new Handler(Looper.getMainLooper());
                iHandler.post(new Runnable() {
                    @Override
                    public void run() {
                        processRxCharacteristic(gatt, characteristic);
                    }
                });
            } else {
                processRxCharacteristic(gatt, characteristic);
            }
        }


        private synchronized void processRxCharacteristic(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {

            Log.i(TAG, "onCharacteristicChanged On Main Thread? " + isOnMainThread());
            Log.e(TAG, "CharBytes-nfy" + Arrays.toString(characteristic.getValue()));
            Log.i(TAG, "CharHex-nfy" + Extensions.bytesToHex(characteristic.getValue()));


            byte[] buffer = characteristic.getValue();
            byte firstByte = buffer[0];
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP && gatt != null) {
                gatt.requestConnectionPriority(BluetoothGatt.CONNECTION_PRIORITY_HIGH);
            }
            Log.d(TAG, "Received opcode reply: " + JoH.bytesToHex(new byte[] { firstByte }));
            if (firstByte == 0x2f) {
                SensorRxMessage sensorRx = new SensorRxMessage(characteristic.getValue());

                ByteBuffer sensorData = ByteBuffer.allocate(buffer.length);
                sensorData.order(ByteOrder.LITTLE_ENDIAN);
                sensorData.put(buffer, 0, buffer.length);

                int sensor_battery_level = 0;
                if (sensorRx.status == TransmitterStatus.BRICKED) {
                    //TODO Handle this in UI/Notification
                    sensor_battery_level = 206; //will give message "EMPTY"
                } else if (sensorRx.status == TransmitterStatus.LOW) {
                    sensor_battery_level = 209; //will give message "LOW"
                } else {
                    sensor_battery_level = 216; //no message, just system status "OK"
                }

                //Log.e(TAG, "filtered: " + sensorRx.filtered);
                disconnected133 = 0; // reset as we got a reading
                disconnected59 = 0;
                lastState = "Got data OK: " + JoH.hourMinuteString();
                successes++;
                failures=0;
                Log.e(TAG, "SUCCESS!! unfiltered: " + sensorRx.unfiltered + " timestamp: " + sensorRx.timestamp + " " + JoH.qs((double)sensorRx.timestamp / 86400, 1) + " days");
                if (sensorRx.unfiltered == 0) {
                    lastState = "Transmitter sent raw sensor value of 0 !! This isn't good. " + JoH.hourMinuteString();
                }
                last_transmitter_timestamp = sensorRx.timestamp;
                if ((getVersionDetails) && (!haveFirmwareDetails())) {
                    doVersionRequestMessage(gatt, characteristic);
                } else if ((getBatteryDetails) && (getBatteryStatusNow || !haveCurrentBatteryStatus())) {
                    doBatteryInfoRequestMessage(gatt, characteristic);
                } else {
                    doDisconnectMessage(gatt, characteristic);
                }

                // TODO beware that wear G5CollectionService is now getting rather out of sync with app version
                final boolean g6 = usingG6();
                processNewTransmitterData(g6 ? sensorRx.unfiltered * G6_SCALING : sensorRx.unfiltered, g6 ? sensorRx.filtered * G6_SCALING : sensorRx.filtered, sensor_battery_level, new Date().getTime());

                // was this the first success after we force enabled always_authenticate?
                if (force_always_authenticate && (successes == 1)) {
                    Log.wtf(TAG, "We apparently only got a reading after forcing the Always Authenticate option");
                    Home.toaststaticnext("Please Enable G5 Always Authenticate debug option!");
                    // TODO should we actually change the settings here?
                }
            } else if (firstByte == GlucoseRxMessage.opcode) {
                disconnected133 = 0; // reset as we got a reading
                disconnected59 = 0;
                GlucoseRxMessage glucoseRx = new GlucoseRxMessage(characteristic.getValue());
                Log.e(TAG, "SUCCESS!! glucose unfiltered: " + glucoseRx.unfiltered);
                successes++;
                failures=0;
                doDisconnectMessage(gatt, characteristic);
                processNewTransmitterData(glucoseRx.unfiltered, glucoseRx.filtered, 216, new Date().getTime());
            } else if (firstByte == VersionRequestRxMessage.opcode) {
                if (!setStoredFirmwareBytes(defaultTransmitter.transmitterId, characteristic.getValue(), true)) {
                    Log.wtf(TAG, "Could not save out firmware version!");
                }
                doDisconnectMessage(gatt, characteristic);
            } else if (firstByte == BatteryInfoRxMessage.opcode) {
                if (!setStoredBatteryBytes(defaultTransmitter.transmitterId, characteristic.getValue())) {
                    Log.wtf(TAG, "Could not save out battery data!");
                }
                getBatteryStatusNow = false;
                doDisconnectMessage(gatt, characteristic);
            } else {
                Log.e(TAG, "onCharacteristic CHANGED unexpected opcode: " + firstByte + " (have not disconnected!)");
            }
            Log.e(TAG, "OnCharacteristic CHANGED finished: ");
        }
    };
    // end BluetoothGattCallback

    private boolean haveFirmwareDetails() {
        return defaultTransmitter.transmitterId.length() == 6 && getStoredFirmwareBytes(defaultTransmitter.transmitterId).length >= 10;
    }

    //public final static String G5_FIRMWARE_MARKER = "g5-firmware-";
    //public final static String G5_BATTERY_FROM_MARKER = "g5-battery-from";

    private boolean haveCurrentBatteryStatus() {
        return defaultTransmitter.transmitterId.length() == 6 && (JoH.msSince(PersistentStore.getLong(G5_BATTERY_FROM_MARKER + defaultTransmitter.transmitterId)) < BATTERY_READ_PERIOD_MS);
    }

    private static byte[] getStoredFirmwareBytes(String transmitterId) {
        if (transmitterId.length() != 6) return new byte[0];
        return PersistentStore.getBytes("g5-firmware-" + transmitterId);
    }

    // from wear sync
    public static boolean setStoredFirmwareBytes(String transmitterId, byte[] data) {
        return setStoredFirmwareBytes(transmitterId, data, false);
    }

    public static boolean setStoredFirmwareBytes(String transmitterId, byte[] data, boolean from_bluetooth) {
        if (from_bluetooth) UserError.Log.e(TAG, "Store: VersionRX dbg: " + JoH.bytesToHex(data));
        if (transmitterId.length() != 6) return false;
        if (data.length < 10) return false;
        if (JoH.ratelimit("store-firmware-bytes", 60)) {
            PersistentStore.setBytes("g5-firmware-" + transmitterId, data);
        }
        return true;
    }

    //public static final String G5_BATTERY_MARKER = "g5-battery-";
    //public static final String G5_BATTERY_WEARABLE_SEND = "g5-battery-wearable-send";

    public synchronized static boolean setStoredBatteryBytes(String transmitterId, byte[] data) {
        UserError.Log.e(TAG, "Store: BatteryRX dbg: " + JoH.bytesToHex(data));
        if (transmitterId.length() != 6) return false;
        if (data.length < 10) return false;
        Log.wtf(TAG, "Saving battery data: " + new BatteryInfoRxMessage(data).toString());
        PersistentStore.setBytes(G5_BATTERY_MARKER + transmitterId, data);
        PersistentStore.setLong(G5_BATTERY_FROM_MARKER + transmitterId, JoH.tsl());
        PersistentStore.setBoolean(G5_BATTERY_WEARABLE_SEND, true);
        return true;
    }

    public static BatteryInfoRxMessage getBatteryDetails(String tx_id) {
        try {
            return new BatteryInfoRxMessage(PersistentStore.getBytes(G5_BATTERY_MARKER + tx_id));
        } catch (Exception e) {
            Log.wtf(TAG, "Exception in getFirmwareDetails: " + e);
            return null;
        }
    }

    public static VersionRequestRxMessage getFirmwareDetails(String tx_id) {
        try {
            byte[] stored = getStoredFirmwareBytes(tx_id);
            if ((stored != null) && (stored.length > 9)) {
                return new VersionRequestRxMessage(stored);
            }
        } catch (Exception e) {
            Log.wtf(TAG, "Exception in getFirmwareDetails: " + e);
            return null;
        }
        return null;
    }

    public static String getFirmwareVersionString(String tx_id) {
        VersionRequestRxMessage vr = getFirmwareDetails(tx_id);
        if (vr != null) {
            return "FW: " + vr.firmware_version_string;
        } else {
            return "";
        }
    }


    private synchronized void sendAuthRequestTxMessage(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) {
        Log.e(TAG, "Sending new AuthRequestTxMessage to " + getUUIDName(characteristic.getUuid()) + " ...");
        authRequest = new AuthRequestTxMessage(getTokenSize());
        Log.i(TAG, "AuthRequestTX: " + JoH.bytesToHex(authRequest.byteSequence));
        characteristic.setValue(authRequest.byteSequence);
        if (gatt != null) {
            gatt.writeCharacteristic(characteristic);
        } else {
            Log.e(TAG, "Cannot send AuthRequestTx as supplied gatt is null!");
        }
    }

    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 ((device != null) && (device.getAddress() != null)) {
                Log.e(TAG,"Processing mPairingRequestReceiver");
                JoH.doPairingRequest(context, this, intent, device.getAddress());
            } else {
                Log.e(TAG,"Received pairing request but device was null");
            }
        }
    };

    private synchronized void processNewTransmitterData(int raw_data , int filtered_data,int sensor_battery_level, long captureTime) {

        final TransmitterData transmitterData = TransmitterData.create(raw_data, filtered_data, sensor_battery_level, captureTime);
        if (transmitterData == null) {
            Log.e(TAG, "TransmitterData.create failed: Duplicate packet");
            return;
        } else {
            timeInMillisecondsOfLastSuccessfulSensorRead = captureTime;
        }
        Sensor sensor = Sensor.currentSensor();
        if (sensor == null) {
            Log.e(TAG, "setSerialDataToTransmitterRawData: No Active Sensor, Data only stored in Transmitter Data");
            return;
        }

        //TODO : LOG if unfiltered or filtered values are zero

        Sensor.updateBatteryLevel(sensor, transmitterData.sensor_battery_level);
        Log.i(TAG,"timestamp create: "+ Long.toString(transmitterData.timestamp));

        BgReading.create(transmitterData.raw_data, transmitterData.filtered_data, this, transmitterData.timestamp);

        Log.d(TAG,"Dex raw_data "+ Double.toString(transmitterData.raw_data));//KS
        Log.d(TAG,"Dex filtered_data "+ Double.toString(transmitterData.filtered_data));//KS
        Log.d(TAG,"Dex sensor_battery_level "+ Double.toString(transmitterData.sensor_battery_level));//KS
        Log.d(TAG,"Dex timestamp "+ JoH.dateTimeText(transmitterData.timestamp));//KS

        static_last_timestamp =  transmitterData.timestamp;

    }

    @SuppressLint("GetInstance")
    private synchronized byte[] calculateHash(byte[] data) {
        if (data.length != 8) {
            Log.e(TAG, "Decrypt Data length should be exactly 8.");
            return null;
        }

        byte[] key = cryptKey();
        if (key == null)
            return null;

        byte[] doubleData;
        ByteBuffer bb = ByteBuffer.allocate(16);
        bb.put(data);
        bb.put(data);

        doubleData = bb.array();

        Cipher aesCipher;
        try {
            aesCipher = Cipher.getInstance("AES/ECB/PKCS7Padding");
            SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
            aesCipher.init(Cipher.ENCRYPT_MODE, skeySpec);
            byte[] aesBytes = aesCipher.doFinal(doubleData, 0, doubleData.length);

            bb = ByteBuffer.allocate(8);
            bb.put(aesBytes, 0, 8);

            return bb.array();
        } catch (NoSuchAlgorithmException | NoSuchPaddingException | IllegalBlockSizeException | BadPaddingException | InvalidKeyException e) {
            e.printStackTrace();
        }

        return null;
    }

    private byte[] cryptKey() {
        if (defaultTransmitter.transmitterId.length() != 6) Log.e(TAG,"cryptKey: Wrong transmitter id length!: "+defaultTransmitter.transmitterId.length());
        try {
            return ("00" + defaultTransmitter.transmitterId + "00" + defaultTransmitter.transmitterId).getBytes("UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return null;
    }

    private static boolean isOnMainThread() {
        return Looper.getMainLooper().getThread() == Thread.currentThread();
    }

    private long getNextAdvertiseTime() {
        long millisecondsSinceTx = getMillisecondsSinceTxLastSeen();
        long timeToExpected  = (300*1000 - (millisecondsSinceTx%(300*1000)));
        long expectedTxTime = new Date().getTime() + timeToExpected - 3*1000;
        Log.e(TAG, "millisecondsSinceTxAd: " + millisecondsSinceTx );
        Log.e(TAG, "advertiseTimeMS.get(0): " + advertiseTimeMS.get(0) + " " + JoH.dateTimeText(advertiseTimeMS.get(0)));
        Log.e(TAG, "timeInMillisecondsOfLastSuccessfulSensorRead: " + " " + timeInMillisecondsOfLastSuccessfulSensorRead + JoH.dateTimeText(timeInMillisecondsOfLastSuccessfulSensorRead) );
        Log.e(TAG, "getNextAdvertiseTime expectedTxTime: " + expectedTxTime + " " + JoH.dateTimeText(expectedTxTime));
        //Log.e(TAG, "timeToExpected: " + timeToExpected );
        //Log.e(TAG, "expectedTxTime: " + expectedTxTime );

        return expectedTxTime;
    }

    protected void waitFor(final int millis) {
        synchronized (mLock) {
            try {
                Log.e(TAG, "waiting " + millis + "ms");
                mLock.wait(millis);
            } catch (final InterruptedException e) {
                Log.e(TAG, "Sleeping interrupted", e);
            }
        }
    }

    private long getMillisecondsSinceTxLastSeen() {
        return new Date().getTime() - advertiseTimeMS.get(0);
    }

    private long getMillisecondsSinceLastSuccesfulSensorRead() {
        return new Date().getTime() - timeInMillisecondsOfLastSuccessfulSensorRead;
    }

    private boolean scanConstantly() {
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        return sharedPreferences.getBoolean("run_ble_scan_constantly", false);
    }

    private boolean alwaysUnbond() {
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        return sharedPreferences.getBoolean("always_unbond_G5", false);
    }

    private boolean alwaysAuthenticate() {
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        return force_always_authenticate || sharedPreferences.getBoolean("always_get_new_keys", false);
    }

    private boolean alwaysOnScreem() {
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        return force_always_on_screen || sharedPreferences.getBoolean("always-on-screen", false);
    }

    private boolean enforceMainThread() {
        SharedPreferences sharedPreferences =
                PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
        return sharedPreferences.getBoolean("run_G5_ble_tasks_on_uithread", false);
    }

    // TODO this could be cached for performance
    private boolean useG5NewMethod() {
        return Pref.getBooleanDefaultFalse("g5_non_raw_method") && (Pref.getBooleanDefaultFalse("engineering_mode"));
    }

    private boolean engineeringMode() {
        return Pref.getBooleanDefaultFalse("engineering_mode");
    }

    private boolean g5BluetoothWatchdog() {
        return Pref.getBoolean("g5_bluetooth_watchdog", true);
    }


    private int getTokenSize() {
            return 8; // d
    }

    private String settingsToString() {
        return ((scanConstantly() ? "scanConstantly " : "")
                + (alwaysUnbond() ? "alwaysUnbond " : "")
                + (alwaysAuthenticate() ? "alwaysAuthenticate " : "")
                + (enforceMainThread() ? "enforceMainThread " : "")
                + (useG5NewMethod() ? "useG5NewMethod " : "")
                + (ignoreLocalBondingState ? "ignoreLocalBondingState " : "")
                + (delayOnBond ? "delayOnBond " : "")
                + (delayOn133Errors ? "delayOn133Errors " : "")
                + (tryOnDemandBondWithDelay ? "tryOnDemandBondWithDelay " : "")
                + (engineeringMode() ? "engineeringMode " : "")
                + (alwaysOnScreem() ? "alwaysOnScreem " : "")
                + (tryPreBondWithDelay ? "tryPreBondWithDelay " : ""));
    }

    // Status for Watchface
    public static boolean isRunning() {
        return lastState.equals("Not running") || lastState.equals("Stopped") ? false : true;
    }

   /* 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 MegaStatus
    public static List<StatusItem> megaStatus() {
        final List<StatusItem> l = new ArrayList<>();

        l.add(new StatusItem("Phone Service State", lastState));
        if (static_last_timestamp > 0) {
            l.add(new StatusItem("Phone got Glucose", JoH.niceTimeSince(static_last_timestamp) + " ago"));
        }

        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 tx_id = Pref.getStringDefaultBlank("dex_txid");

        l.add(new StatusItem("Transmitter ID", tx_id));
        // get firmware details
        VersionRequestRxMessage vr = getFirmwareDetails(tx_id);
        if ((vr != null) && (vr.firmware_version_string.length() > 0)) {

            l.add(new StatusItem("Firmware Version", vr.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) l.add(new StatusItem("ASIC", vr.asic, StatusItem.Highlight.NOTICE)); // TODO color code
        }

        BatteryInfoRxMessage bt = 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", StatusItem.Highlight.NOTICE, "long-press",
                    new Runnable() {
                        @Override
                        public void run() {
                            getBatteryStatusNow = false;
                        }
                    }));
        }
        if ((bt != null) && (last_battery_query > 0)) {
            l.add(new StatusItem("Battery Last queried", JoH.niceTimeSince(last_battery_query) + " " + "ago", StatusItem.Highlight.NORMAL, "long-press",
                    new Runnable() {
                        @Override
                        public void run() {
                            getBatteryStatusNow = true;
                        }
                    }));
            l.add(new StatusItem("Transmitter Status", TransmitterStatus.getBatteryLevel(vr.status).toString()));
            l.add(new StatusItem("Transmitter Days", bt.runtime + ((last_transmitter_timestamp > 0) ? " / " + JoH.qs((double) last_transmitter_timestamp / 86400, 1) : "")));
            l.add(new StatusItem("Voltage A", bt.voltagea, bt.voltagea < 300 ? StatusItem.Highlight.BAD : StatusItem.Highlight.NORMAL));
            l.add(new StatusItem("Voltage B", bt.voltageb, bt.voltageb < 290 ? StatusItem.Highlight.BAD : StatusItem.Highlight.NORMAL));
            l.add(new StatusItem("Resistance", bt.resist, bt.resist > 1400 ? StatusItem.Highlight.BAD : (bt.resist > 1000 ? StatusItem.Highlight.NOTICE : (bt.resist > 750 ? StatusItem.Highlight.NORMAL : StatusItem.Highlight.GOOD))));
            l.add(new StatusItem("Temperature", bt.temperature + " \u2103"));
        }


        return l;
    }

    // Status for Watchface
    public static String getLastState() {
        return lastState;
    }
    public static long getLastStateTimestamp() {
        return static_last_timestamp;
    }
}