package io.trygvis.android.bt; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothManager; import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.util.Log; import android.widget.Toast; import org.flywaydb.core.Flyway; import org.flywaydb.core.api.android.ContextHolder; import java.util.ArrayList; import java.util.Collection; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import io.trygvis.android.Function; import io.trygvis.android.LocalBinder; import io.trygvis.soilmoisture.R; import static java.util.Collections.unmodifiableCollection; public class DefaultBtService extends Service implements BtService { private final static String TAG = DefaultBtService.class.getSimpleName(); private final IBinder binder = new LocalBinder<>(this); private Handler handler = new Handler(); // ----------------------------------------------------------------------- // State // ----------------------------------------------------------------------- private BtDbIntegration btDbIntegration; BluetoothManager bluetoothManager; private BluetoothAdapter bluetoothAdapter; private final Set> devices = new HashSet<>(); private boolean scanning = false; // ----------------------------------------------------------------------- // BtService Implementation // ----------------------------------------------------------------------- @Override public boolean initialize(BtDbIntegration btDbIntegration) { if (bluetoothManager != null) { Log.i(TAG, "Already initialized"); return false; } this.btDbIntegration = btDbIntegration; // Use this check to determine whether BLE is supported on the device. Then you can // selectively disable BLE-related features. if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) { Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show(); return false; } // Initializes a Bluetooth adapter. For API level 18 and above, get a reference to // BluetoothAdapter through BluetoothManager. bluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (bluetoothManager == null) { Log.e(TAG, "Unable to initialize BluetoothManager."); return false; } bluetoothAdapter = bluetoothManager.getAdapter(); if (bluetoothAdapter == null) { Toast.makeText(this, R.string.error_bluetooth_not_supported, Toast.LENGTH_SHORT).show(); bluetoothManager = null; return false; } Log.i(TAG, "Bluetooth initialized"); List addresses = runTx(db -> { String[] columns = {Tables.C_ADDRESS}; List as = new ArrayList(); Cursor cursor = db.query(Tables.T_BT_DEVICE, columns, null, new String[0], null, null, null); try { while (cursor.moveToNext()) { String address = cursor.getString(0); as.add(address); } } finally { cursor.close(); } return as; }); for (String address : addresses) { BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(address); register(bluetoothDevice, null, null); } return true; } @Override public void clearCache() { } @Override public boolean isScanning() { return scanning; } @Override public boolean startScanning(long timeoutMs) { Log.d(TAG, "startScanning, timeoutMs=" + timeoutMs); if (timeoutMs > 0) { handler.postDelayed(this::stopScanning, timeoutMs); } if (bluetoothAdapter.startLeScan(leScanCallback)) { scanning = true; sendBroadcast(createScanStarted()); return true; } return false; } @Override public void stopScanning() { Log.d(TAG, "stopScanning"); // This doesn't mind being called twice. bluetoothAdapter.stopLeScan(leScanCallback); scanning = false; sendBroadcast(createScanStopped()); } public BtDevice getDevice(String mac) { BtDevice device = findDevice(mac); if (device != null) { return device; } BluetoothDevice bluetoothDevice = bluetoothAdapter.getRemoteDevice(mac); return register(bluetoothDevice, null, null); } @Override public A getTag(String address) { return getDevice(address).getTag(); } @Override public Collection> getDevices() { return unmodifiableCollection(devices); } @Override public Collection getTags() { ArrayList tags = new ArrayList<>(); for (BtDevice device : devices) { tags.add(device.getTag()); } return tags; } @Override public T runTx(Function action) { SQLiteDatabase db = openBtDevices(); try { db.beginTransaction(); T value = action.apply(db); db.setTransactionSuccessful(); return value; } finally { if (db.inTransaction()) { db.endTransaction(); } db.close(); } } // ----------------------------------------------------------------------- // Scanning // ----------------------------------------------------------------------- private BluetoothAdapter.LeScanCallback leScanCallback = (device, rssi, scanRecord) -> { BtScanResult scanResult = new BtScanResult(scanRecord); register(device, rssi, scanResult); }; // ----------------------------------------------------------------------- // Service Implementation // ----------------------------------------------------------------------- @Override public IBinder onBind(Intent intent) { return binder; } /** * TODO: move this to initialize or somewhere it can be called so it doesn't block the UI * thread. */ @Override public void onCreate() { Bundle data; try { ComponentName myService = new ComponentName(this, this.getClass()); data = getPackageManager().getServiceInfo(myService, PackageManager.GET_META_DATA).metaData; } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException(e); } SQLiteDatabase db = openBtDevices(); String path; try { path = db.getPath(); } finally { db.close(); } { ContextHolder.setContext(this); Flyway flyway = new Flyway(); flyway.setDataSource("jdbc:sqlite:" + path, "", ""); flyway.setTable("schema_version_bt"); flyway.setLocations("db/migration/bt"); flyway.migrate(); } String customMigrations = data.getString(getClass().getName() + ".migration", null); if (customMigrations != null) { ContextHolder.setContext(this); Flyway flyway = new Flyway(); flyway.setDataSource("jdbc:sqlite:" + path, "", ""); flyway.setTable("schema_version_custom"); flyway.setLocations(customMigrations); flyway.setBaselineOnMigrate(true); flyway.migrate(); } } // ----------------------------------------------------------------------- // Stuff // ----------------------------------------------------------------------- private SQLiteDatabase openBtDevices() { return openOrCreateDatabase("bt-devices", MODE_ENABLE_WRITE_AHEAD_LOGGING, null); } private BtDevice register(BluetoothDevice bluetoothDevice, Integer rssi, BtScanResult scanResult) { String address = bluetoothDevice.getAddress(); BtDevice btDevice = findDevice(address); if (btDevice != null) { return btDevice; } long now = System.currentTimeMillis(); btDevice = runTx(db -> { Cursor cursor = db.query(Tables.T_BT_DEVICE, new String[]{Tables.C_ID, Tables.C_FIRST_SEEN}, Tables.C_ADDRESS + "=?", new String[]{address}, null, null, null); long id; Date firstSeen, lastSeen; boolean seenBefore = cursor.moveToNext(); if (seenBefore) { id = cursor.getLong(0); firstSeen = new Date(cursor.getLong(1)); lastSeen = new Date(now); ContentValues values = new ContentValues(); values.put(Tables.C_LAST_SEEN, now); db.update(Tables.T_BT_DEVICE, values, "address=?", new String[]{address}); } else { ContentValues values = new ContentValues(); values.put(Tables.C_ADDRESS, address); values.put(Tables.C_FIRST_SEEN, now); values.put(Tables.C_LAST_SEEN, now); id = db.insert(Tables.T_BT_DEVICE, null, values); firstSeen = lastSeen = new Date(now); } Log.i(TAG, "New device: " + address + ", seenBefore=" + seenBefore); cursor.close(); return new BtDevice<>(this, bluetoothDevice, db, btDbIntegration, id, rssi, scanResult, seenBefore, firstSeen, lastSeen); }); devices.add(btDevice); sendBroadcast(createNewDevice(btDevice.getAddress())); return btDevice; } Intent createScanStarted() { return new Intent(BtServiceListenerBroadcastReceiver.INTENT_NAME). putExtra("event", "scanStarted"); } Intent createScanStopped() { return new Intent(BtServiceListenerBroadcastReceiver.INTENT_NAME). putExtra("event", "scanStopped"); } Intent createNewDevice(String address) { return new Intent(BtServiceListenerBroadcastReceiver.INTENT_NAME). putExtra("event", "newDevice"). putExtra("address", address); } Intent createDeviceConnection(String address) { return new Intent(BtServiceListenerBroadcastReceiver.INTENT_NAME). putExtra("event", "deviceConnection"). putExtra("address", address); } public static void dispatchEvent(Intent intent, BtServiceListenerBroadcastReceiver listener) { String event = intent.getStringExtra("event"); Log.i(TAG, "Dispatching event " + intent.getAction() + "/" + event); switch (event) { case "scanStarted": listener.onScanStarted(); break; case "scanStopped": listener.onScanStopped(); break; case "newDevice": listener.onNewDevice(intent.getStringExtra("address")); break; case "deviceConnection": listener.onDeviceConnection(intent.getStringExtra("address")); break; default: break; } } private BtDevice findDevice(String mac) { for (BtDevice d : devices) { if (d.getAddress().equals(mac)) { return d; } } return null; } }