<commit_before>package net.wigle.wigleandroid.listener;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
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.SharedPreferences;
import android.location.Location;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Handler;

import com.google.android.gms.maps.model.LatLng;

import net.wigle.wigleandroid.FilterMatcher;
import net.wigle.wigleandroid.ListFragment;
import net.wigle.wigleandroid.MainActivity;
import net.wigle.wigleandroid.NetworkListAdapter;
import net.wigle.wigleandroid.R;
import net.wigle.wigleandroid.db.DatabaseHelper;
import net.wigle.wigleandroid.model.ConcurrentLinkedHashMap;
import net.wigle.wigleandroid.model.Network;
import net.wigle.wigleandroid.model.NetworkType;
import net.wigle.wigleandroid.util.WiGLEToast;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;

import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice;
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord;
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecordStore;

import static net.wigle.wigleandroid.MainActivity.DEBUG_BLUETOOTH_DATA;
import static net.wigle.wigleandroid.listener.WifiReceiver.CHANNEL_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.CRYPTO_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.FIND_TIME_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.SIGNAL_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.SSID_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.channelCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.cryptoCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.findTimeCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.signalCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.ssidCompare;

/**
 * Created by bobzilla on 12/20/15
 */
public final class BluetoothReceiver extends BroadcastReceiver {

    private static final Map<Integer, String> DEVICE_TYPE_LEGEND;
    //TODO: i18n
    static {
        Map<Integer, String> initMap = new HashMap<>();
        initMap.put(0, "Misc");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER, "Camcorder");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO, "Car Audio");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE, "Handsfree");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES, "Headphones");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO, "HiFi");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER, "Speaker");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE, "Mic");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO, "Portable Audio");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX, "Settop");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED, "A/V");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VCR, "VCR");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA, "Camera");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING, "Videoconf");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER, "Display/Speaker");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY, "AV Toy");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR, "Monitor");
        initMap.put(BluetoothClass.Device.COMPUTER_DESKTOP, "Desktop");
        initMap.put(BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA, "PDA");
        initMap.put(BluetoothClass.Device.COMPUTER_LAPTOP, "Laptop");
        initMap.put(BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA, "Palm");
        initMap.put(BluetoothClass.Device.COMPUTER_SERVER, "Server");
        initMap.put(BluetoothClass.Device.COMPUTER_UNCATEGORIZED, "Computer");
        initMap.put(BluetoothClass.Device.COMPUTER_WEARABLE, "Wearable Computer");
        initMap.put(BluetoothClass.Device.HEALTH_BLOOD_PRESSURE, "Blood Pressure");
        initMap.put(BluetoothClass.Device.HEALTH_DATA_DISPLAY, "Health Display");
        initMap.put(BluetoothClass.Device.HEALTH_GLUCOSE, "Glucose");
        initMap.put(BluetoothClass.Device.HEALTH_PULSE_OXIMETER, "PulseOxy");
        initMap.put(BluetoothClass.Device.HEALTH_PULSE_RATE, "Pulse");
        initMap.put(BluetoothClass.Device.HEALTH_THERMOMETER, "Thermometer");
        initMap.put(BluetoothClass.Device.HEALTH_UNCATEGORIZED, "Health");
        initMap.put(BluetoothClass.Device.HEALTH_WEIGHING, "Scale");
        initMap.put(BluetoothClass.Device.PHONE_CELLULAR, "Cellphone");
        initMap.put(BluetoothClass.Device.PHONE_CORDLESS, "Cordless Phone");
        initMap.put(BluetoothClass.Device.PHONE_ISDN, "ISDN Phone");
        initMap.put(BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY, "Modem/GW");
        initMap.put(BluetoothClass.Device.PHONE_SMART, "Smartphone");
        initMap.put(BluetoothClass.Device.PHONE_UNCATEGORIZED, "Phone");
        initMap.put(BluetoothClass.Device.TOY_CONTROLLER, "Controller");
        initMap.put(BluetoothClass.Device.TOY_DOLL_ACTION_FIGURE, "Doll");
        initMap.put(BluetoothClass.Device.TOY_GAME, "Game");
        initMap.put(BluetoothClass.Device.TOY_ROBOT, "Robot");
        initMap.put(BluetoothClass.Device.TOY_UNCATEGORIZED, "Toy");
        initMap.put(BluetoothClass.Device.TOY_VEHICLE, "Vehicle");
        initMap.put(BluetoothClass.Device.WEARABLE_GLASSES, "Glasses");
        initMap.put(BluetoothClass.Device.WEARABLE_HELMET, "Helmet");
        initMap.put(BluetoothClass.Device.WEARABLE_JACKET, "Jacket");
        initMap.put(BluetoothClass.Device.WEARABLE_PAGER, "Pager");
        initMap.put(BluetoothClass.Device.WEARABLE_UNCATEGORIZED, "Wearable");
        initMap.put(BluetoothClass.Device.WEARABLE_WRIST_WATCH, "Watch");
        initMap.put(BluetoothClass.Device.Major.UNCATEGORIZED, "Uncategorized");

        DEVICE_TYPE_LEGEND = Collections.unmodifiableMap(initMap);
    }

    private MainActivity mainActivity;
    private final DatabaseHelper dbHelper;
    private final AtomicBoolean scanning = new AtomicBoolean(false);
    private final Map<String,Network> currentBluetoothNetworks = new ConcurrentLinkedHashMap<String, Network>(64);
    private final Set<String> runNetworks = new HashSet<>();
    private NetworkListAdapter listAdapter;
    private final ScanCallback scanCallback;

    private Handler bluetoothTimer;
    private long scanRequestTime = Long.MIN_VALUE;
    private boolean scanInFlight = false;
    private long lastScanResponseTime = Long.MIN_VALUE;
    private final long constructionTime = System.currentTimeMillis();

    // refresh thresholds - probably should either make these configurable
    // arguably expiration should live per element not-seen in n scans.
    private static final int EMPTY_LE_THRESHOLD = 30;
    private static final int EMPTY_BT_THRESHOLD = 4;

    // scan state
    private int btCount = 0;
    private long lastDiscoveryAt = 0;

    private long adUuidNoScanUuid = 0;
    private long scanUuidNoAdUuid = 0;


    public BluetoothReceiver(final MainActivity mainActivity, final DatabaseHelper dbHelper ) {
        this.mainActivity = mainActivity;
        this.dbHelper = dbHelper;


        if (Build.VERSION.SDK_INT >= 21) {
            scanCallback = new ScanCallback() {
                final SharedPreferences prefs = mainActivity.getSharedPreferences( ListFragment.SHARED_PREFS, 0 );
                private int empties = 0;

                @Override
                public void onScanResult(int callbackType, ScanResult scanResult) {
                    final GPSListener gpsListener = mainActivity.getGPSListener();
                    //DEBUG:
                    MainActivity.info("LE scanResult: " + scanResult + " callbackType: " + callbackType);
                    Location location = null;
                    if (gpsListener != null) {
                        final long gpsTimeout = prefs.getLong(ListFragment.PREF_GPS_TIMEOUT, GPSListener.GPS_TIMEOUT_DEFAULT);
                        final long netLocTimeout = prefs.getLong(ListFragment.PREF_NET_LOC_TIMEOUT, GPSListener.NET_LOC_TIMEOUT_DEFAULT);
                        gpsListener.checkLocationOK(gpsTimeout, netLocTimeout);
                        location = gpsListener.getLocation();
                    } else {
                        MainActivity.warn("Null gpsListener in LE Single Scan Result");
                    }

                    handleLeScanResult(scanResult, location);
                    final long newBtCount = dbHelper.getNewBtCount();
                    ListFragment.lameStatic.newBt = newBtCount;
                    ListFragment.lameStatic.runBt = runNetworks.size();
                    sort(prefs);
                    listAdapter.notifyDataSetChanged();
                }

                @Override
                public void onBatchScanResults(List<ScanResult> results) {
                    //MainActivity.info("LE Batch results: " + results);
                    final GPSListener gpsListener = mainActivity.getGPSListener();

                    Location location = null;

                    boolean forceReset = false;
                    if (results.isEmpty()) {
                        empties++;
                        //ALIBI: if it's been too long, we'll force-clear
                        if (EMPTY_LE_THRESHOLD < empties) {
                            forceReset = true;
                            empties = 0;
                        }
                    } else {
                        forceReset = true;
                    }

                    if ((listAdapter != null) && prefs.getBoolean( ListFragment.PREF_SHOW_CURRENT, true ) && forceReset ) {
                        listAdapter.clearBluetoothLe();
                    }

                    if (gpsListener != null) {
                        final long gpsTimeout = prefs.getLong(ListFragment.PREF_GPS_TIMEOUT, GPSListener.GPS_TIMEOUT_DEFAULT);
                        final long netLocTimeout = prefs.getLong(ListFragment.PREF_NET_LOC_TIMEOUT, GPSListener.NET_LOC_TIMEOUT_DEFAULT);
                        gpsListener.checkLocationOK(gpsTimeout, netLocTimeout);
                        location = gpsListener.getLocation();
                    } else {
                        MainActivity.warn("Null gpsListener in LE Batch Scan Result");
                    }

                    for (final ScanResult scanResult : results) {
                        handleLeScanResult(scanResult, location);
                    }
                    final long newBtCount = dbHelper.getNewBtCount();
                    ListFragment.lameStatic.newBt = newBtCount;
                    ListFragment.lameStatic.runBt = runNetworks.size();
                    sort(prefs);
                    listAdapter.notifyDataSetChanged();
                }

                @Override
                public void onScanFailed(int errorCode) {
                    /*if ((listAdapter != null) && prefs.getBoolean( ListFragment.PREF_SHOW_CURRENT, true ) ) {
                        listAdapter.clearBluetoothLe();
                    }*/
                    switch (errorCode) {
                        case SCAN_FAILED_ALREADY_STARTED:
                            MainActivity.info("BluetoothLEScan already started");
                            break;
                        default:
                            MainActivity.error("Bluetooth scan error: " + errorCode);
                            scanning.set(false);
                    }
                }
            };
        } else {
            scanCallback = null;
        }
    }

    private void handleLeScanResult(final ScanResult scanResult, Location location) {
        if (Build.VERSION.SDK_INT >= 21) {
            //DEBUG: MainActivity.info("LE scanResult: " + scanResult);
            final ScanRecord scanRecord = scanResult.getScanRecord();
            if (scanRecord != null) {
                final BluetoothDevice device = scanResult.getDevice();
                //BluetoothUtil.BleAdvertisedData adData = BluetoothUtil.parseAdvertisedData(scanRecord.getBytes());
                //final String adDeviceName = (adData != null) ? adData.getName(): null;

                final String bssid = device.getAddress();

                final String ssid =
                        (null ==  scanRecord.getDeviceName() || scanRecord.getDeviceName().isEmpty())
                                ? device.getName()
                                :scanRecord.getDeviceName();

                // This is questionable - of of Major class being known when specific class seems thin
                int type = (device.getBluetoothClass().getDeviceClass() == 0 ||
                            device.getBluetoothClass().getDeviceClass() == BluetoothClass.Device.Major.UNCATEGORIZED)
                        ?  device.getBluetoothClass().getMajorDeviceClass()
                        : device.getBluetoothClass().getDeviceClass();

                if (DEBUG_BLUETOOTH_DATA) {
                    MainActivity.info("LE deviceName: " + ssid
                            + "\n\taddress: " + bssid
                            + "\n\tname: " + scanRecord.getDeviceName() + " (vs. "+device.getName()+")"
                            //+ "\n\tadName: " + adDeviceName
                            + "\n\tclass:" + DEVICE_TYPE_LEGEND.get(device.getBluetoothClass().getDeviceClass())+ "("
                            + device.getBluetoothClass() + ")"
                            + "\n\ttype:" + device.getType()
                            + "\n\tRSSI:" + scanResult.getRssi()
                            //+ "\n\tTX power:" + scanRecord.getTxPowerLevel() //THIS IS ALWAYS GARBAGE
                            //+ "\n\tbytes: " + Arrays.toString(scanRecord.getBytes())
                            );


                    /*final int scanCount = ((scanRecord != null) && (scanRecord.getServiceUuids() != null)) ? scanRecord.getServiceUuids().size() : 0;
                    final int adCount = ((adData != null) && (adData.getUuids() != null)) ? adData.getUuids().size() : 0;

                    if (adCount > 0 || scanCount > 0){
                        final List<java.util.UUID> adUuids = adData.getUuids();
                        final List<ParcelUuid> srUuids = scanRecord.getServiceUuids();
                        if (scanCount > adCount) {
                            for (ParcelUuid uuid: srUuids) {
                                if (! adUuids.contains(uuid.getUuid())) {
                                    MainActivity.error("\n\t\tSR: "+uuid.toString());
                                }
                            }
                            scanUuidNoAdUuid++;
                        } else if (adCount > scanCount) {
                            for (UUID uuid: adUuids) {
                                if (! srUuids.contains(new ParcelUuid(uuid))) {
                                    MainActivity.error("\n\t\tAD: "+uuid.toString());
                                }
                            }
                            adUuidNoScanUuid++;
                        } else if (scanCount > 0) {
                            for (ParcelUuid uuid: srUuids) {
                                MainActivity.info("\n\t\t==: "+uuid.toString());
                            }
                        }
                    }*/
                }
                try {
                    //TODO: not seeing a lot of value from these checks yet (vs. the adData name extraction above)
                    final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, scanResult.getRssi(),
                            scanRecord.getBytes(), System.currentTimeMillis());
                    final AdRecordStore adRecordStore = deviceLe.getAdRecordStore();
                    for (int i = 0; i < 200; i++) {
                        if (!adRecordStore.isRecordPresent(i)) {
                            continue;
                        }
                        final AdRecord adRecord = adRecordStore.getRecord(i);
                        if (DEBUG_BLUETOOTH_DATA) {
                            MainActivity.info("LE adRecord(" + i + "): " + adRecord);
                        }
                    }
                } catch (Exception ex) {
                    //TODO: so this happens:
                    MainActivity.warn("failed to parse LeDevice from ScanRecord", ex);
                    //parseScanRecordAsSparseArray explodes on array indices
                }

                final String capabilities = DEVICE_TYPE_LEGEND.get(device.getBluetoothClass().getDeviceClass()) /* + "("
                        + device.getBluetoothClass().getMajorDeviceClass() + ":"
                        + device.getBluetoothClass().getDeviceClass() + ") " +*/;
                final SharedPreferences prefs = mainActivity.getSharedPreferences( ListFragment.SHARED_PREFS, 0 );
                //ALIBI: shamelessly re-using frequency here for device type.
                final Network network = addOrUpdateBt(bssid, ssid, type, capabilities,
                        scanResult.getRssi(),
                        NetworkType.BLE, location, prefs);
            }
        }
    }

    /**
     * initiate a bluetooth scan, if discovery is not currently in-progress (callbacks via onReceive)
     */
    public void bluetoothScan() {
        final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            return;
        }

        // classic BT scan - basically "Always Be Discovering" times between discovery runs will be MAX(wifi delay) since this is called from wifi receiver
        if (!bluetoothAdapter.isDiscovering()) {
            bluetoothAdapter.startDiscovery();
            lastDiscoveryAt = System.currentTimeMillis();
            //ALIBI: since calls are triggered off of the WiFi Receiver delay, we're just introducing a retirement counter for BT
            btCount++;
        } else {
            if (DEBUG_BLUETOOTH_DATA) {
                MainActivity.info("skipping bluetooth scan; discover already in progress (last scan started "+(System.currentTimeMillis()-lastDiscoveryAt)+"ms ago)");
            }
        }<commit_msg>handle bluetooth npe<commit_after>package net.wigle.wigleandroid.listener;

import android.bluetooth.BluetoothAdapter;
import android.bluetooth.BluetoothClass;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.le.BluetoothLeScanner;
import android.bluetooth.le.ScanCallback;
import android.bluetooth.le.ScanFilter;
import android.bluetooth.le.ScanRecord;
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.SharedPreferences;
import android.location.Location;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Handler;

import com.google.android.gms.maps.model.LatLng;

import net.wigle.wigleandroid.FilterMatcher;
import net.wigle.wigleandroid.ListFragment;
import net.wigle.wigleandroid.MainActivity;
import net.wigle.wigleandroid.NetworkListAdapter;
import net.wigle.wigleandroid.R;
import net.wigle.wigleandroid.db.DatabaseHelper;
import net.wigle.wigleandroid.model.ConcurrentLinkedHashMap;
import net.wigle.wigleandroid.model.Network;
import net.wigle.wigleandroid.model.NetworkType;
import net.wigle.wigleandroid.util.WiGLEToast;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.regex.Matcher;

import uk.co.alt236.bluetoothlelib.device.BluetoothLeDevice;
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecord;
import uk.co.alt236.bluetoothlelib.device.adrecord.AdRecordStore;

import static net.wigle.wigleandroid.MainActivity.DEBUG_BLUETOOTH_DATA;
import static net.wigle.wigleandroid.listener.WifiReceiver.CHANNEL_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.CRYPTO_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.FIND_TIME_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.SIGNAL_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.SSID_COMPARE;
import static net.wigle.wigleandroid.listener.WifiReceiver.channelCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.cryptoCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.findTimeCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.signalCompare;
import static net.wigle.wigleandroid.listener.WifiReceiver.ssidCompare;

/**
 * Created by bobzilla on 12/20/15
 */
public final class BluetoothReceiver extends BroadcastReceiver {

    private static final Map<Integer, String> DEVICE_TYPE_LEGEND;
    //TODO: i18n
    static {
        Map<Integer, String> initMap = new HashMap<>();
        initMap.put(0, "Misc");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_CAMCORDER, "Camcorder");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_CAR_AUDIO, "Car Audio");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE, "Handsfree");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HEADPHONES, "Headphones");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_HIFI_AUDIO, "HiFi");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_LOUDSPEAKER, "Speaker");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_MICROPHONE, "Mic");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_PORTABLE_AUDIO, "Portable Audio");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_SET_TOP_BOX, "Settop");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_UNCATEGORIZED, "A/V");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VCR, "VCR");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CAMERA, "Camera");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_CONFERENCING, "Videoconf");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_DISPLAY_AND_LOUDSPEAKER, "Display/Speaker");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_GAMING_TOY, "AV Toy");
        initMap.put(BluetoothClass.Device.AUDIO_VIDEO_VIDEO_MONITOR, "Monitor");
        initMap.put(BluetoothClass.Device.COMPUTER_DESKTOP, "Desktop");
        initMap.put(BluetoothClass.Device.COMPUTER_HANDHELD_PC_PDA, "PDA");
        initMap.put(BluetoothClass.Device.COMPUTER_LAPTOP, "Laptop");
        initMap.put(BluetoothClass.Device.COMPUTER_PALM_SIZE_PC_PDA, "Palm");
        initMap.put(BluetoothClass.Device.COMPUTER_SERVER, "Server");
        initMap.put(BluetoothClass.Device.COMPUTER_UNCATEGORIZED, "Computer");
        initMap.put(BluetoothClass.Device.COMPUTER_WEARABLE, "Wearable Computer");
        initMap.put(BluetoothClass.Device.HEALTH_BLOOD_PRESSURE, "Blood Pressure");
        initMap.put(BluetoothClass.Device.HEALTH_DATA_DISPLAY, "Health Display");
        initMap.put(BluetoothClass.Device.HEALTH_GLUCOSE, "Glucose");
        initMap.put(BluetoothClass.Device.HEALTH_PULSE_OXIMETER, "PulseOxy");
        initMap.put(BluetoothClass.Device.HEALTH_PULSE_RATE, "Pulse");
        initMap.put(BluetoothClass.Device.HEALTH_THERMOMETER, "Thermometer");
        initMap.put(BluetoothClass.Device.HEALTH_UNCATEGORIZED, "Health");
        initMap.put(BluetoothClass.Device.HEALTH_WEIGHING, "Scale");
        initMap.put(BluetoothClass.Device.PHONE_CELLULAR, "Cellphone");
        initMap.put(BluetoothClass.Device.PHONE_CORDLESS, "Cordless Phone");
        initMap.put(BluetoothClass.Device.PHONE_ISDN, "ISDN Phone");
        initMap.put(BluetoothClass.Device.PHONE_MODEM_OR_GATEWAY, "Modem/GW");
        initMap.put(BluetoothClass.Device.PHONE_SMART, "Smartphone");
        initMap.put(BluetoothClass.Device.PHONE_UNCATEGORIZED, "Phone");
        initMap.put(BluetoothClass.Device.TOY_CONTROLLER, "Controller");
        initMap.put(BluetoothClass.Device.TOY_DOLL_ACTION_FIGURE, "Doll");
        initMap.put(BluetoothClass.Device.TOY_GAME, "Game");
        initMap.put(BluetoothClass.Device.TOY_ROBOT, "Robot");
        initMap.put(BluetoothClass.Device.TOY_UNCATEGORIZED, "Toy");
        initMap.put(BluetoothClass.Device.TOY_VEHICLE, "Vehicle");
        initMap.put(BluetoothClass.Device.WEARABLE_GLASSES, "Glasses");
        initMap.put(BluetoothClass.Device.WEARABLE_HELMET, "Helmet");
        initMap.put(BluetoothClass.Device.WEARABLE_JACKET, "Jacket");
        initMap.put(BluetoothClass.Device.WEARABLE_PAGER, "Pager");
        initMap.put(BluetoothClass.Device.WEARABLE_UNCATEGORIZED, "Wearable");
        initMap.put(BluetoothClass.Device.WEARABLE_WRIST_WATCH, "Watch");
        initMap.put(BluetoothClass.Device.Major.UNCATEGORIZED, "Uncategorized");

        DEVICE_TYPE_LEGEND = Collections.unmodifiableMap(initMap);
    }

    private MainActivity mainActivity;
    private final DatabaseHelper dbHelper;
    private final AtomicBoolean scanning = new AtomicBoolean(false);
    private final Map<String,Network> currentBluetoothNetworks = new ConcurrentLinkedHashMap<String, Network>(64);
    private final Set<String> runNetworks = new HashSet<>();
    private NetworkListAdapter listAdapter;
    private final ScanCallback scanCallback;

    private Handler bluetoothTimer;
    private long scanRequestTime = Long.MIN_VALUE;
    private boolean scanInFlight = false;
    private long lastScanResponseTime = Long.MIN_VALUE;
    private final long constructionTime = System.currentTimeMillis();

    // refresh thresholds - probably should either make these configurable
    // arguably expiration should live per element not-seen in n scans.
    private static final int EMPTY_LE_THRESHOLD = 30;
    private static final int EMPTY_BT_THRESHOLD = 4;

    // scan state
    private int btCount = 0;
    private long lastDiscoveryAt = 0;

    private long adUuidNoScanUuid = 0;
    private long scanUuidNoAdUuid = 0;


    public BluetoothReceiver(final MainActivity mainActivity, final DatabaseHelper dbHelper ) {
        this.mainActivity = mainActivity;
        this.dbHelper = dbHelper;


        if (Build.VERSION.SDK_INT >= 21) {
            scanCallback = new ScanCallback() {
                final SharedPreferences prefs = mainActivity.getSharedPreferences( ListFragment.SHARED_PREFS, 0 );
                private int empties = 0;

                @Override
                public void onScanResult(int callbackType, ScanResult scanResult) {
                    final GPSListener gpsListener = mainActivity.getGPSListener();
                    //DEBUG:
                    MainActivity.info("LE scanResult: " + scanResult + " callbackType: " + callbackType);
                    Location location = null;
                    if (gpsListener != null) {
                        final long gpsTimeout = prefs.getLong(ListFragment.PREF_GPS_TIMEOUT, GPSListener.GPS_TIMEOUT_DEFAULT);
                        final long netLocTimeout = prefs.getLong(ListFragment.PREF_NET_LOC_TIMEOUT, GPSListener.NET_LOC_TIMEOUT_DEFAULT);
                        gpsListener.checkLocationOK(gpsTimeout, netLocTimeout);
                        location = gpsListener.getLocation();
                    } else {
                        MainActivity.warn("Null gpsListener in LE Single Scan Result");
                    }

                    handleLeScanResult(scanResult, location);
                    final long newBtCount = dbHelper.getNewBtCount();
                    ListFragment.lameStatic.newBt = newBtCount;
                    ListFragment.lameStatic.runBt = runNetworks.size();
                    sort(prefs);
                    listAdapter.notifyDataSetChanged();
                }

                @Override
                public void onBatchScanResults(List<ScanResult> results) {
                    //MainActivity.info("LE Batch results: " + results);
                    final GPSListener gpsListener = mainActivity.getGPSListener();

                    Location location = null;

                    boolean forceReset = false;
                    if (results.isEmpty()) {
                        empties++;
                        //ALIBI: if it's been too long, we'll force-clear
                        if (EMPTY_LE_THRESHOLD < empties) {
                            forceReset = true;
                            empties = 0;
                        }
                    } else {
                        forceReset = true;
                    }

                    if ((listAdapter != null) && prefs.getBoolean( ListFragment.PREF_SHOW_CURRENT, true ) && forceReset ) {
                        listAdapter.clearBluetoothLe();
                    }

                    if (gpsListener != null) {
                        final long gpsTimeout = prefs.getLong(ListFragment.PREF_GPS_TIMEOUT, GPSListener.GPS_TIMEOUT_DEFAULT);
                        final long netLocTimeout = prefs.getLong(ListFragment.PREF_NET_LOC_TIMEOUT, GPSListener.NET_LOC_TIMEOUT_DEFAULT);
                        gpsListener.checkLocationOK(gpsTimeout, netLocTimeout);
                        location = gpsListener.getLocation();
                    } else {
                        MainActivity.warn("Null gpsListener in LE Batch Scan Result");
                    }

                    for (final ScanResult scanResult : results) {
                        handleLeScanResult(scanResult, location);
                    }
                    final long newBtCount = dbHelper.getNewBtCount();
                    ListFragment.lameStatic.newBt = newBtCount;
                    ListFragment.lameStatic.runBt = runNetworks.size();
                    sort(prefs);
                    listAdapter.notifyDataSetChanged();
                }

                @Override
                public void onScanFailed(int errorCode) {
                    /*if ((listAdapter != null) && prefs.getBoolean( ListFragment.PREF_SHOW_CURRENT, true ) ) {
                        listAdapter.clearBluetoothLe();
                    }*/
                    switch (errorCode) {
                        case SCAN_FAILED_ALREADY_STARTED:
                            MainActivity.info("BluetoothLEScan already started");
                            break;
                        default:
                            MainActivity.error("Bluetooth scan error: " + errorCode);
                            scanning.set(false);
                    }
                }
            };
        } else {
            scanCallback = null;
        }
    }

    private void handleLeScanResult(final ScanResult scanResult, Location location) {
        if (Build.VERSION.SDK_INT >= 21) {
            //DEBUG: MainActivity.info("LE scanResult: " + scanResult);
            final ScanRecord scanRecord = scanResult.getScanRecord();
            if (scanRecord != null) {
                final BluetoothDevice device = scanResult.getDevice();
                //BluetoothUtil.BleAdvertisedData adData = BluetoothUtil.parseAdvertisedData(scanRecord.getBytes());
                //final String adDeviceName = (adData != null) ? adData.getName(): null;

                final String bssid = device.getAddress();

                final String ssid =
                        (null ==  scanRecord.getDeviceName() || scanRecord.getDeviceName().isEmpty())
                                ? device.getName()
                                :scanRecord.getDeviceName();

                // This is questionable - of Major class being known when specific class seems thin
                final BluetoothClass bluetoothClass = device.getBluetoothClass();
                int type = BluetoothClass.Device.Major.UNCATEGORIZED;
                if (bluetoothClass != null) {
                    final int deviceClass = bluetoothClass.getDeviceClass();
                    type = (deviceClass == 0 || deviceClass == BluetoothClass.Device.Major.UNCATEGORIZED)
                            ? bluetoothClass.getMajorDeviceClass()
                            : deviceClass;
                }

                if (DEBUG_BLUETOOTH_DATA) {
                    MainActivity.info("LE deviceName: " + ssid
                            + "\n\taddress: " + bssid
                            + "\n\tname: " + scanRecord.getDeviceName() + " (vs. "+device.getName()+")"
                            //+ "\n\tadName: " + adDeviceName
                            + "\n\tclass:"
                            + (bluetoothClass == null ? null : DEVICE_TYPE_LEGEND.get(bluetoothClass.getDeviceClass()))
                            + "(" + bluetoothClass + ")"
                            + "\n\ttype:" + device.getType()
                            + "\n\tRSSI:" + scanResult.getRssi()
                            //+ "\n\tTX power:" + scanRecord.getTxPowerLevel() //THIS IS ALWAYS GARBAGE
                            //+ "\n\tbytes: " + Arrays.toString(scanRecord.getBytes())
                            );


                    /*final int scanCount = ((scanRecord != null) && (scanRecord.getServiceUuids() != null)) ? scanRecord.getServiceUuids().size() : 0;
                    final int adCount = ((adData != null) && (adData.getUuids() != null)) ? adData.getUuids().size() : 0;

                    if (adCount > 0 || scanCount > 0){
                        final List<java.util.UUID> adUuids = adData.getUuids();
                        final List<ParcelUuid> srUuids = scanRecord.getServiceUuids();
                        if (scanCount > adCount) {
                            for (ParcelUuid uuid: srUuids) {
                                if (! adUuids.contains(uuid.getUuid())) {
                                    MainActivity.error("\n\t\tSR: "+uuid.toString());
                                }
                            }
                            scanUuidNoAdUuid++;
                        } else if (adCount > scanCount) {
                            for (UUID uuid: adUuids) {
                                if (! srUuids.contains(new ParcelUuid(uuid))) {
                                    MainActivity.error("\n\t\tAD: "+uuid.toString());
                                }
                            }
                            adUuidNoScanUuid++;
                        } else if (scanCount > 0) {
                            for (ParcelUuid uuid: srUuids) {
                                MainActivity.info("\n\t\t==: "+uuid.toString());
                            }
                        }
                    }*/
                }
                try {
                    //TODO: not seeing a lot of value from these checks yet (vs. the adData name extraction above)
                    final BluetoothLeDevice deviceLe = new BluetoothLeDevice(device, scanResult.getRssi(),
                            scanRecord.getBytes(), System.currentTimeMillis());
                    final AdRecordStore adRecordStore = deviceLe.getAdRecordStore();
                    for (int i = 0; i < 200; i++) {
                        if (!adRecordStore.isRecordPresent(i)) {
                            continue;
                        }
                        final AdRecord adRecord = adRecordStore.getRecord(i);
                        if (DEBUG_BLUETOOTH_DATA) {
                            MainActivity.info("LE adRecord(" + i + "): " + adRecord);
                        }
                    }
                } catch (Exception ex) {
                    //TODO: so this happens:
                    MainActivity.warn("failed to parse LeDevice from ScanRecord", ex);
                    //parseScanRecordAsSparseArray explodes on array indices
                }

                final String capabilities = DEVICE_TYPE_LEGEND.get(
                        bluetoothClass == null ? null : bluetoothClass.getDeviceClass());
                final SharedPreferences prefs = mainActivity.getSharedPreferences( ListFragment.SHARED_PREFS, 0 );
                //ALIBI: shamelessly re-using frequency here for device type.
                final Network network = addOrUpdateBt(bssid, ssid, type, capabilities,
                        scanResult.getRssi(),
                        NetworkType.BLE, location, prefs);
            }
        }
    }

    /**
     * initiate a bluetooth scan, if discovery is not currently in-progress (callbacks via onReceive)
     */
    public void bluetoothScan() {
        final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
        if (bluetoothAdapter == null) {
            return;
        }

        // classic BT scan - basically "Always Be Discovering" times between discovery runs will be MAX(wifi delay) since this is called from wifi receiver
        if (!bluetoothAdapter.isDiscovering()) {
            bluetoothAdapter.startDiscovery();
            lastDiscoveryAt = System.currentTimeMillis();
            //ALIBI: since calls are triggered off of the WiFi Receiver delay, we're just introducing a retirement counter for BT
            btCount++;
        } else {
            if (DEBUG_BLUETOOTH_DATA) {
                MainActivity.info("skipping bluetooth scan; discover already in progress (last scan started "+(System.currentTimeMillis()-lastDiscoveryAt)+"ms ago)");
            }
        }
{'fraction_non_alphanumeric': 0.09412260153186114, 'fraction_numerical': 0.0023688574211038877, 'mean_word_length': 2.1291385274254653, 'pattern_counts': {'":': 1, '<': 27, '<?xml version=': 0, '>': 37, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 11, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2281623', 'n_tokens_mistral': 10743, 'n_tokens_neox': 10280, 'n_words': 1833}