package io.trygvis.soilmoisture; import android.app.Service; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.content.ComponentName; import android.content.ContentValues; import android.content.Intent; import android.content.ServiceConnection; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.os.IBinder; import android.util.Log; import android.widget.Toast; import java.util.ArrayList; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeSet; import io.trygvis.android.LocalBinder; import io.trygvis.android.bt.BtDevice; import io.trygvis.android.bt.BtPromise; import io.trygvis.android.bt.BtService; import io.trygvis.android.bt.DefaultBtService; import io.trygvis.bluetooth.TrygvisIoUuids; import static io.trygvis.android.bt.BtPromise.PromiseResult.stop; import static io.trygvis.android.bt.BtPromise.PromiseResult.waitForNextEvent; import static io.trygvis.android.bt.BtService.BtServiceListenerBroadcastReceiver; import static io.trygvis.bluetooth.TrygvisIoUuids.CLIENT_CHARACTERISTIC_CONFIG; import static io.trygvis.soilmoisture.SmDevice.GetSensorCountRes; import static io.trygvis.soilmoisture.SmDevice.GetValueRes; import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.GET_SENSOR_COUNT; import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.GET_VALUE; import static io.trygvis.soilmoisture.SmDevice.createGetSensorCountReq; import static io.trygvis.soilmoisture.SmDevice.createGetValueReq; import static io.trygvis.soilmoisture.SmDevice.parseResponse; import static java.lang.String.valueOf; import static java.lang.System.currentTimeMillis; public class DefaultSoilMoistureService extends Service implements SoilMoistureService { private final static String TAG = DefaultSoilMoistureService.class.getSimpleName(); private final IBinder binder = new LocalBinder<>(this); @SuppressWarnings("UnusedDeclaration") private final DefaultSoilMoistureService context = DefaultSoilMoistureService.this; private ServiceConnection serviceConnection; private BtService btService; @Override public IBinder onBind(Intent intent) { return binder; } @Override public void onCreate() { Log.i(TAG, "onCreate"); serviceConnection = new ServiceConnection() { @SuppressWarnings("unchecked") @Override public void onServiceConnected(ComponentName componentName, IBinder service) { btService = ((LocalBinder>) service).getService(); boolean ok = btService.initialize(DefaultSoilMoistureService.this::createTag); sendBroadcast(createReady(ok)); } @Override public void onServiceDisconnected(ComponentName componentName) { btService = null; } }; bindService(new Intent(this, DefaultBtService.class), serviceConnection, BIND_AUTO_CREATE); registerReceiver(btServiceListener, BtServiceListenerBroadcastReceiver.INTENT_FILTER); } @Override public void onDestroy() { unregisterReceiver(btServiceListener); Log.i(TAG, "onDestroy" + btService); if (serviceConnection != null) { unbindService(serviceConnection); } } private final BtServiceListenerBroadcastReceiver btServiceListener = new BtServiceListenerBroadcastReceiver() { @Override public void onScanStarted() { sendBroadcast(createScanStarted()); } @Override public void onScanStopped() { sendBroadcast(createScanStopped()); } @Override public void onNewDevice(String address) { BtDevice btDevice = btService.getDevice(address); SmDevice smDevice = btDevice.getTag(); sendBroadcast(createNewDevice(address)); boolean candidate = btDevice.getAddress().startsWith("FB:") || btDevice.getAddress().startsWith("FD:"); if (!candidate) { Log.w(TAG, "Skipping device: " + btDevice.getAddress()); markDeviceAsNotUseful(smDevice); return; } if (!smDevice.isProbed()) { probe(smDevice.getBtDevice().getAddress()); } } }; public void probe(String address) { BtDevice btDevice = btService.getDevice(address); SmDevice smDevice = btDevice.getTag(); Log.i(TAG, "Probing " + address + ", name=" + btDevice.getName()); BtPromise executor = new BtPromise(). onDirect(gatt -> { BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE); if (service == null) { return stop(); } BluetoothGattCharacteristic soilMoisture = service.getCharacteristic(TrygvisIoUuids.Characteristics.SOIL_MOISTURE); if (soilMoisture == null) { return stop(); } BluetoothGattDescriptor ccg = soilMoisture.getDescriptor(TrygvisIoUuids.CLIENT_CHARACTERISTIC_CONFIG); ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.setCharacteristicNotification(soilMoisture, true); return gatt.writeDescriptor(ccg) ? waitForNextEvent() : stop(); }). onDescriptorWrite((gatt, descriptor) -> { Log.i(TAG, "Notifications enabled, getting sensor count"); BluetoothGattCharacteristic c = descriptor.getCharacteristic(); c.setValue(createGetSensorCountReq()); return gatt.writeCharacteristic(c) ? waitForNextEvent() : stop(); }). onCharacteristicWrite((gatt, characteristic) -> waitForNextEvent()). onCharacteristicChanged((gatt, characteristic) -> { GetSensorCountRes getSensorCountRes = parseResponse(characteristic.getValue(), GET_SENSOR_COUNT, GetSensorCountRes.class); Log.i(TAG, "The device has " + getSensorCountRes.count + " sensors."); markDeviceAsUseful(smDevice, getSensorCountRes.count); return stop(); }). onFinally(success -> { if (smDevice.getIsUseful() == null) { smDevice.setIsUseful(false); } }); btDevice.withConnection(executor); } private void markDeviceAsNotUseful(SmDevice device) { btService.runTx(db -> { device.setIsUseful(false); ContentValues values = new ContentValues(); values.put(Tables.C_USEFUL, false); db.update(Tables.T_SM_DEVICE, values, "id=?", new String[]{valueOf(device.id)}); return null; }); } private void markDeviceAsUseful(SmDevice device, int sensorCount) { btService.runTx(db -> { device.setIsUseful(true); // Find all already registered sensors and register the rest Map indexes = new HashMap<>(); Cursor cursor = db.query(Tables.T_SM_SENSOR, new String[]{Tables.C_ID, Tables.C_INDEX}, Tables.C_SM_DEVICE + "=?", new String[]{valueOf(device.id)}, null, null, null); while (cursor.moveToNext()) { indexes.put(cursor.getInt(1), cursor.getLong(0)); } ContentValues values = new ContentValues(); values.put(Tables.C_USEFUL, true); db.update(Tables.T_SM_DEVICE, values, "id=?", new String[]{valueOf(device.id)}); for (int i = 0; i < sensorCount; i++) { Long id = indexes.get(i); if (id == null) { values = new ContentValues(); values.put(Tables.C_SM_DEVICE, device.id); values.put(Tables.C_INDEX, i); id = db.insert(Tables.T_SM_SENSOR, null, values); } device.addSensor(new SmSensor(device, id, (byte) i)); } return null; }); } // ----------------------------------------------------------------------- // SmDevicesManager Implementation // ----------------------------------------------------------------------- @Override public List getDevices(Comparator comparator) { Set devices = new TreeSet<>(comparator); for (BtDevice btDevice : btService.getDevices()) { devices.add(btDevice.getTag()); } return new ArrayList<>(devices); } @Override public SmDevice getDevice(String address) { return btService.getTag(address); } @Override public boolean isScanning() { return btService.isScanning(); } @Override public boolean startScanning(long scanPeriod) { return btService.startScanning(scanPeriod); } @Override public void stopScanning() { if (btService != null) { btService.stopScanning(); } } // ----------------------------------------------------------------------- // // ----------------------------------------------------------------------- private SmDevice createTag(SQLiteDatabase db, BtDevice btDevice) { Cursor cursor = null; try { cursor = db.query(Tables.T_SM_DEVICE, new String[]{Tables.C_ID}, Tables.C_BT_DEVICE + "=?", new String[]{valueOf(btDevice.getId())}, null, null, null); long id; if (cursor.moveToNext()) { id = cursor.getLong(0); } else { ContentValues values = new ContentValues(); values.put(Tables.C_BT_DEVICE, btDevice.getId()); id = db.insert(Tables.T_SM_DEVICE, null, values); } cursor.close(); String[] sensorColumns = { Tables.C_ID, Tables.C_INDEX, }; cursor = db.query(Tables.T_SM_SENSOR, sensorColumns, Tables.C_SM_DEVICE + "=?", new String[]{valueOf(id)}, null, null, Tables.C_INDEX); SmDevice device = new SmDevice(this, btDevice, id); if (cursor.moveToNext()) { device.setIsUseful(true); do { device.addSensor(new SmSensor(device, cursor.getLong(0), cursor.getInt(1))); } while (cursor.moveToNext()); } return device; } finally { if (cursor != null) { cursor.close(); } } } void handleNewSensorValueReady(SmSensor sensor, int value) { Log.i(TAG, "new sensor value: " + sensor + "=" + value); long timestamp = currentTimeMillis(); btService.runTx(db -> { ContentValues values = new ContentValues(); values.put(Tables.C_SM_SENSOR, sensor.getId()); values.put(Tables.C_TIMESTAMP, timestamp); values.put(Tables.C_VALUE, value); return db.insert(Tables.T_SOIL_SAMPLE, null, values); }); sensor.updateLastValue(new Date(timestamp), value); sendBroadcast(createNewSample(sensor)); } void readCurrentValue(SmSensor sensor) { BtPromise promise = new BtPromise(). onDirect(gatt -> { BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE); BluetoothGattCharacteristic soilMoisture = service.getCharacteristic(TrygvisIoUuids.Characteristics.SOIL_MOISTURE); BluetoothGattDescriptor ccg = soilMoisture.getDescriptor(CLIENT_CHARACTERISTIC_CONFIG); ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.setCharacteristicNotification(soilMoisture, true); return gatt.writeDescriptor(ccg) ? waitForNextEvent() : stop(); }). onDescriptorWrite((gatt, descriptor) -> { BluetoothGattCharacteristic c = descriptor.getCharacteristic(); c.setValue(createGetValueReq((byte) sensor.getIndex())); return gatt.writeCharacteristic(c) ? waitForNextEvent() : stop(); }). onCharacteristicWrite((gatt, characteristic) -> waitForNextEvent()). onCharacteristicChanged((gatt, characteristic) -> { GetValueRes getSensorCountRes = parseResponse(characteristic.getValue(), GET_VALUE, GetValueRes.class); handleNewSensorValueReady(sensor, getSensorCountRes.value); return stop(); }). onFinally(success -> { if (!success) { sendBroadcast(createToast(R.string.error_could_not_read_value, Toast.LENGTH_SHORT)); } }); sensor.getDevice().getBtDevice().withConnection(promise); } // ----------------------------------------------------------------------- // Event creation and dispatching // ----------------------------------------------------------------------- private Intent createToast(int id, int length) { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "toast"). putExtra("id", id). putExtra("length", length); } private Intent createReady(boolean success) { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "ready"). putExtra("success", success); } private Intent createScanStarted() { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "scanStarted"); } private Intent createScanStopped() { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "scanStopped"); } private Intent createNewDevice(String address) { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "newDevice"). putExtra("address", address); } private Intent createNewSample(SmSensor sensor) { return new Intent(SoilMoistureListener.INTENT_NAME). putExtra("event", "newSample"). putExtra("address", sensor.getDevice().getBtDevice().getAddress()). putExtra("index", sensor.getIndex()); } public static void dispatchEvent(Intent intent, SoilMoistureListener listener) { String event = intent.getStringExtra("event"); Log.i(TAG, "Dispatching event " + intent.getAction() + "/" + event); switch (event) { case "toast": listener.onToast( intent.getIntExtra("id", 0), intent.getIntExtra("length", 0)); break; case "ready": listener.onReady( intent.getBooleanExtra("success", false)); break; case "newDevice": listener.onNewDevice( intent.getStringExtra("address")); break; case "scanStarted": listener.onScanStarted(); break; case "scanStopped": listener.onScanStopped(); break; case "newSample": listener.onNewSample( intent.getStringExtra("address"), intent.getIntExtra("index", -1)); break; default: break; } } }