package io.trygvis.soilmoisture; import android.app.Service; import android.bluetooth.BluetoothGatt; 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.os.IBinder; import android.util.Log; import java.util.ArrayList; import java.util.Comparator; import java.util.List; 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.continueDownChain; import static io.trygvis.android.bt.BtPromise.doneWithChain; import static io.trygvis.android.bt.BtService.BtServiceListenerBroadcastReceiver; import static io.trygvis.soilmoisture.SmDevice.GetSensorCountRes; import static io.trygvis.soilmoisture.SmDevice.SmCmdCode.GET_SENSOR_COUNT; public class DefaultSoilMoistureService extends Service implements SoilMoistureService { private final static String TAG = DefaultSoilMoistureService.class.getSimpleName(); private final static int DEFAULT_WARNING_LEVEL = 750; 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::onNewDevice); 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(); if (!smDevice.isProbed()) { Log.i(TAG, "Probing " + address + ", name=" + btDevice.getName()); BtPromise executor = new BtPromise(). onConnectionStateChange((gatt, newState) -> { //noinspection SimplifiableIfStatement if (newState == BluetoothGatt.STATE_CONNECTED) { Log.i(TAG, "Connected to " + address + ", getting services"); return gatt.discoverServices() ? continueDownChain : doneWithChain; } else { Log.i(TAG, "Disconnected from " + address + ", trying again"); return new BtPromise().onConnectionStateChange((gatt2, newState2) -> { if (newState2 == BluetoothGatt.STATE_CONNECTED) { Log.i(TAG, "Connected to " + address + ", getting services"); return continueDownChain; } Log.i(TAG, "Could still not connect to " + address + ", failing."); return doneWithChain; }).toDetour(); } }). onServicesDiscovered(gatt -> { Log.i(TAG, "Services discovered"); BluetoothGattService service = gatt.getService(TrygvisIoUuids.Services.SOIL_MOISTURE_SERVICE); if (service == null) { return doneWithChain; } BluetoothGattCharacteristic soilMoisture = service.getCharacteristic(TrygvisIoUuids.Characteristics.SOIL_MOISTURE); if (soilMoisture == null) { return doneWithChain; } BluetoothGattDescriptor ccg = soilMoisture.getDescriptor(TrygvisIoUuids.CLIENT_CHARACTERISTIC_CONFIG); ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.setCharacteristicNotification(soilMoisture, true); return gatt.writeDescriptor(ccg) ? continueDownChain : doneWithChain; }). onDescriptorWrite((gatt, descriptor) -> { Log.i(TAG, "Notifications enabled, getting sensor count"); BluetoothGattCharacteristic c = descriptor.getCharacteristic(); c.setValue(SmDevice.createGetSensorCountReq()); return gatt.writeCharacteristic(c) ? continueDownChain : doneWithChain; }). onCharacteristicWrite((gatt, characteristic) -> continueDownChain). onCharacteristicChanged((gatt, characteristic) -> { GetSensorCountRes getSensorCountRes = SmDevice.parseResponse(characteristic.getValue(), GET_SENSOR_COUNT, GetSensorCountRes.class); Log.i(TAG, "The device has " + getSensorCountRes.count + " sensors."); smDevice.setIsUseful(true); smDevice.setSensorCount(getSensorCountRes.count); return doneWithChain; }). onFinally(() -> { btDevice.disconnect(); if (smDevice.getIsUseful() == null) { smDevice.setIsUseful(false); } btService.runTx(db -> { if (!btDevice.isSeenBefore() && smDevice.isUseful()) { for (SmSensor soilMonitor : smDevice.getSensors()) { ContentValues values = new ContentValues(); values.put("bt_device", btDevice.getId()); values.put("warning_level", DEFAULT_WARNING_LEVEL); db.insert("soil_monitor", null, values); } } return null; }); sendBroadcast(createNewDevice(address)); }); btDevice.connect(executor); } else { sendBroadcast(createNewDevice(address)); } } }; // ----------------------------------------------------------------------- // 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() { btService.stopScanning(); } // ----------------------------------------------------------------------- // Event creation and dispatching // ----------------------------------------------------------------------- private SmDevice onNewDevice(BtDevice btDevice) { return new SmDevice(btDevice); } 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); } public static void dispatchEvent(Intent intent, SoilMoistureListener listener) { String event = intent.getStringExtra("event"); Log.i(TAG, "Dispatching event " + intent.getAction() + "/" + event); switch (event) { 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; default: break; } } }