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 android.bluetooth.BluetoothAdapter.LeScanCallback;
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;
private Scanner scanner = new Scanner();
static {
BtSequencer.d = msg -> Log.d(BtSequencer.TAG, msg);
BtSequencer.i = msg -> Log.i(BtSequencer.TAG, msg);
BtSequencer.w = msg -> Log.w(BtSequencer.TAG, msg);
BtSequencer.we = pair -> Log.w(BtSequencer.TAG, pair.getKey(), pair.getValue());
}
// -----------------------------------------------------------------------
// 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, false);
}
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(scanner)) {
scanning = true;
scanner.found.clear();
sendBroadcast(createScanStarted());
return true;
}
return false;
}
@Override
public void stopScanning() {
Log.d(TAG, "stopScanning");
// This doesn't mind being called twice.
bluetoothAdapter.stopLeScan(scanner);
scanning = false;
for (BtDevice device : devices) {
boolean recentlySeen = scanner.found.contains(device);
Log.i(TAG, "scanner.found.contains(device)=" + recentlySeen + ", " +
"address=" + device.getAddress());
boolean old = device.isRecentlySeen();
device.setRecentlySeen(recentlySeen);
// Only if it not seen and it wasn't previously seen
if (!recentlySeen && old) {
sendBroadcast(createDevicePropertyUpdated(device.getAddress()));
}
}
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, false);
}
@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();
}
}
// -----------------------------------------------------------------------
// 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, boolean fromScan) {
String address = bluetoothDevice.getAddress();
BtDevice device = findDevice(address);
if (device != null) {
device.setRecentlySeen(true);
sendBroadcast(createDevicePropertyUpdated(device.getAddress()));
return device;
}
long now = System.currentTimeMillis();
device = 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, seenBefore,
firstSeen, lastSeen, fromScan);
});
devices.add(device);
sendBroadcast(createNewDevice(device.getAddress()));
return device;
}
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);
}
Intent createDevicePropertyUpdated(String address) {
return new Intent(BtServiceListenerBroadcastReceiver.INTENT_NAME).
putExtra("event", "devicePropertyUpdated").
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 "devicePropertyUpdated":
listener.onDevicePropertyUpdated(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;
}
private class Scanner implements LeScanCallback {
private List found = new ArrayList<>();
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
BtDevice d = register(device, rssi, true);
found.add(d);
}
}
}