package no.topi.fiken.display; import android.app.Service; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; import android.bluetooth.BluetoothGatt; import android.bluetooth.BluetoothGattCallback; import android.bluetooth.BluetoothGattCharacteristic; import android.bluetooth.BluetoothGattDescriptor; import android.bluetooth.BluetoothGattService; import android.bluetooth.BluetoothManager; import android.content.Context; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.util.Log; import android.widget.Toast; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.List; import java.util.Queue; public class DefaultDisplayService extends Service implements DisplayService { private final Context context = DefaultDisplayService.this; private final static String TAG = DefaultDisplayService.class.getSimpleName(); private final IBinder binder = new LocalBinder(this); private BluetoothManager mBluetoothManager; private BluetoothAdapter mBluetoothAdapter; private State state; private static class State { BluetoothGatt gatt; BluetoothGattService displayService; BluetoothGattCharacteristic gaugeCtrl; BluetoothGattCharacteristic gaugeData; } private Handler handler; private int UPDATE_RSSI_DELAY = 100 * 1000; private Runnable updateRssi = new Runnable() { @Override public void run() { if (state != null && state.gatt != null) { state.gatt.readRemoteRssi(); } handler.postDelayed(this, UPDATE_RSSI_DELAY); } }; public static enum ServiceState { BROKEN, IDLE, SCANNING, CONNECTED, } private ServiceState serviceState = ServiceState.BROKEN; @Override public IBinder onBind(Intent intent) { return binder; } @Override public void onCreate() { handler = new Handler(); handler.postDelayed(updateRssi, UPDATE_RSSI_DELAY); } public boolean initialize() { // For API level 18 and above, get a reference to BluetoothAdapter through // BluetoothManager. if (mBluetoothManager == null) { mBluetoothManager = (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE); if (mBluetoothManager == null) { Log.e(TAG, "Unable to initialize BluetoothManager."); return false; } } mBluetoothAdapter = mBluetoothManager.getAdapter(); if (mBluetoothAdapter == null) { Log.e(TAG, "Unable to obtain a BluetoothAdapter."); return false; } Log.e(TAG, "Bluetooth initialized"); serviceState = ServiceState.IDLE; return true; } @Override public boolean connect(final String address) { if (serviceState != ServiceState.IDLE) { if (!(serviceState == ServiceState.CONNECTED && state.gatt.getDevice().getAddress().equals(address))) { Log.e(TAG, "connect(): Not idle: " + serviceState); return false; } Log.i(TAG, "connect(): already connected: " + serviceState); return true; } state = new State(); BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); BtActionExecutor executor = new BtActionExecutor(); executor.onSuccess(new BtCallback() { @Override public boolean onConnectionStateChange(BluetoothGatt gatt, int newState) { boolean ok = gatt.discoverServices(); if (!ok) { return false; } Intent intent = IntentAction.DEVICE_UPDATE.intent(); intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), address); intent.putExtra(IntentExtra.CONNECTED.name(), true); sendBroadcast(intent); return true; } }).onSuccess(new BtCallback() { @Override public boolean onServicesDiscovered(BluetoothGatt gatt) { Log.i(TAG, "Services discovered"); state.displayService = gatt.getService(Constants.TRYGVIS_IO_FIKEN_STATUS_PANEL_UUID); state.gaugeCtrl = state.displayService.getCharacteristic(Constants.TRYGVIS_IO_GAUGE_CTRL_UUID); state.gaugeData = state.displayService.getCharacteristic(Constants.TRYGVIS_IO_GAUGE_DATA_UUID); Log.i(TAG, "service=" + state.displayService + ", gaugeCtrl=" + state.gaugeCtrl + ", gaugeData=" + state.gaugeData); Intent intent = IntentAction.DEVICE_UPDATE.intent(); intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), address); intent.putExtra(IntentExtra.DEVICE_IS_DISPLAY.name(), state.displayService != null); sendBroadcast(intent); BluetoothGattDescriptor ccg = state.gaugeCtrl.getDescriptor(Constants.CLIENT_CHARACTERISTIC_CONFIG); ccg.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); gatt.setCharacteristicNotification(state.gaugeCtrl, true); return gatt.writeDescriptor(ccg); } }).onSuccess(new BtCallback() { @Override public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { state.gaugeCtrl.setValue(new byte[]{0x01}); gatt.writeCharacteristic(state.gaugeCtrl); return true; } }).onSuccess(new BtCallback() { @Override public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { Log.w(TAG, "wait for it...!"); return true; } }).onSuccess(new BtCallback() { @Override public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { byte[] value = state.gaugeCtrl.getValue(); Log.w(TAG, "gauge bytes: " + toHexString(value)); if (value.length != 2) { Log.w(TAG, "Bad response, expected value.length to be 2, was " + value.length); return false; } int gaugeCount = (int) value[1]; Log.d(TAG, "Gauge count=" + gaugeCount); Intent intent = IntentAction.DEVICE_UPDATE.intent(); intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), address); intent.putExtra(IntentExtra.GAUGE_COUNT.name(), gaugeCount); sendBroadcast(intent); return true; } }); executor.onRemoteRssi(new BluetoothGattCallback() { @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { Intent intent = IntentAction.DEVICE_UPDATE.intent(); intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), gatt.getDevice().getAddress()); intent.putExtra(IntentExtra.RSSI.name(), rssi); sendBroadcast(intent); } }); state.gatt = device.connectGatt(this, false, executor.asCallback()); if (state.gatt != null) { serviceState = ServiceState.CONNECTED; return true; } else { return false; } } private static class BtCallback { public boolean onConnectionStateChange(BluetoothGatt gatt, int newState) { throw new NotOverriddenException(); } public boolean onServicesDiscovered(BluetoothGatt gatt) { throw new NotOverriddenException(); } public boolean onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { throw new NotOverriddenException(); } public boolean onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { throw new NotOverriddenException(); } public boolean onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { throw new NotOverriddenException(); } public boolean onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { throw new NotOverriddenException(); } public boolean onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor) { throw new NotOverriddenException(); } public boolean onReliableWriteCompleted(BluetoothGatt gatt) { throw new NotOverriddenException(); } public boolean onReadRemoteRssi(BluetoothGatt gatt, int rssi) { throw new NotOverriddenException(); } } private static class NotOverriddenException extends RuntimeException { } private class BtActionExecutor { Queue actions = new ArrayDeque(); List remoteRssi = new ArrayList(); public BtActionExecutor onSuccess(BtCallback btCallback) { actions.add(btCallback); return this; } public BtActionExecutor onRemoteRssi(BluetoothGattCallback callback) { remoteRssi.add(callback); return this; } public void onEvent(String key, BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, BluetoothGattDescriptor descriptor, int status, int newState) { if (status != BluetoothGatt.GATT_SUCCESS) { Log.w(TAG, "Operation failed: " + key); } Log.i(TAG, "Bt action completed successfully: callback=" + key); if (!actions.isEmpty()) { BtCallback btCallback = actions.remove(); Log.i(TAG, "Executing bt action"); boolean ok; try { if (key.equals("onConnectionStateChange")) { ok = btCallback.onConnectionStateChange(gatt, newState); } else if (key.equals("onServicesDiscovered")) { ok = btCallback.onServicesDiscovered(gatt); } else if (key.equals("onCharacteristicRead")) { ok = btCallback.onCharacteristicRead(gatt, characteristic); } else if (key.equals("onCharacteristicWrite")) { ok = btCallback.onCharacteristicWrite(gatt, characteristic); } else if (key.equals("onCharacteristicChanged")) { ok = btCallback.onCharacteristicChanged(gatt, characteristic); } else if (key.equals("onDescriptorRead")) { ok = btCallback.onDescriptorRead(gatt, descriptor); } else if (key.equals("onDescriptorWrite")) { ok = btCallback.onDescriptorWrite(gatt, descriptor); } else if (key.equals("onReliableWriteCompleted")) { ok = btCallback.onReliableWriteCompleted(gatt); } else { Log.w(TAG, "Unknown callback: " + key); return; } } catch (NotOverriddenException e) { Log.w(TAG, "Unexpected callback by listener: " + key); disconnect(); return; } if (!ok) { Log.w(TAG, "Error performing Bluetooth action."); disconnect(); } } else { Log.d(TAG, "All Bluetooth actions are done"); } } public BluetoothGattCallback asCallback() { return new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { onEvent("onConnectionStateChange", gatt, null, null, status, newState); } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { onEvent("onServicesDiscovered", gatt, null, null, status, 9); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { onEvent("onCharacteristicRead", gatt, characteristic, null, status, 0); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { onEvent("onCharacteristicWrite", gatt, characteristic, null, status, 0); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { onEvent("onCharacteristicChanged", gatt, characteristic, null, BluetoothGatt.GATT_SUCCESS, 0); } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { onEvent("onDescriptorRead", gatt, null, descriptor, status, 0); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { onEvent("onDescriptorWrite", gatt, null, descriptor, status, 0); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { onEvent("onReliableWriteCompleted", gatt, null, null, status, 0); } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { for (BluetoothGattCallback callback : remoteRssi) { callback.onReadRemoteRssi(gatt, rssi, status); } } }; } } @Override public void disconnect() { if (serviceState != ServiceState.CONNECTED) { Log.d(TAG, "disconnect(): Not connected: " + serviceState); return; } serviceState = ServiceState.IDLE; if (state.gatt != null) { try { state.gatt.disconnect(); } catch (Exception e) { Log.w(TAG, "gatt.disconnect()", e); } try { state.gatt.close(); } catch (Exception e) { Log.w(TAG, "gatt.close()", e); } state.gatt = null; } state.displayService = null; state = new State(); } @Override public void startScan() { if (serviceState != ServiceState.IDLE) { Toast.makeText(context, "startScan(): Not idle", Toast.LENGTH_SHORT).show(); return; } serviceState = ServiceState.SCANNING; mBluetoothAdapter.startLeScan(leScanCallback); } @Override public void stopScan() { Log.d(TAG, "stopScan(): stopping scanning"); if (serviceState != ServiceState.SCANNING) { Log.d(TAG, "stopScan(): not scanning"); return; } mBluetoothAdapter.stopLeScan(leScanCallback); serviceState = ServiceState.IDLE; } private BluetoothAdapter.LeScanCallback leScanCallback = new BluetoothAdapter.LeScanCallback() { @Override public void onLeScan(final BluetoothDevice device, final int rssi, final byte[] scanRecord) { Log.i(TAG, "onLeScan()"); Intent intent = IntentAction.DEVICE_UPDATE.intent(); intent.putExtra(IntentExtra.DEVICE_ADDRESS.name(), device.getAddress()); intent.putExtra(IntentExtra.DEVICE_NAME.name(), device.getName()); intent.putExtra(IntentExtra.RSSI.name(), rssi); sendBroadcast(intent); } }; public static String toHexString(byte[] bytes) { StringBuilder s = new StringBuilder(); for (byte b : bytes) { if (b < 10) { s.append('0'); } s.append(Integer.toHexString(b)); } return s.toString(); } }