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;
}
}