package com.codegy.aerlink.connection; import android.bluetooth.*; import android.content.Context; import android.os.Handler; import android.util.Log; import com.codegy.aerlink.connection.characteristic.CharacteristicIdentifier; import com.codegy.aerlink.connection.characteristic.CharacteristicSubscriber; import com.codegy.aerlink.connection.characteristic.CharacteristicSubscriberThread; import com.codegy.aerlink.connection.command.Command; import com.codegy.aerlink.connection.command.CommandHandler; import com.codegy.aerlink.connection.command.CommandQueueThread; import java.lang.reflect.Method; import java.util.*; import static android.content.ContentValues.TAG; /** * Created by Guiye on 18/5/15. */ public class ConnectionManager implements CharacteristicSubscriber, CommandHandler { private static final String LOG_TAG = ConnectionManager.class.getSimpleName(); private static final String DESCRIPTOR_CONFIG = "00002902-0000-1000-8000-00805f9b34fb"; public interface Callback { void onConnectionStateChange(ConnectionState state); void onReadyToSubscribe(BluetoothGatt bluetoothGatt); void onCharacteristicChanged(BluetoothGattCharacteristic characteristic); } private Context mContext; private Callback mCallback; private BluetoothDevice mDevice; private BluetoothAdapter mBluetoothAdapter; private int mBondsFailed = 0; private int mConnectionsFailed = 0; private CharacteristicSubscriberThread subscriberThread; private CommandQueueThread commandQueue; public ConnectionManager(Context context, Callback callback, BluetoothAdapter bluetoothAdapter) { this.mContext = context; this.mCallback = callback; this.mBluetoothAdapter = bluetoothAdapter; this.commandQueue = new CommandQueueThread(this); this.commandQueue.start(); } public void close() { // Check if already closed if (mCallback == null) { return; } Log.i(LOG_TAG, "Close"); mCallback = null; disconnectDevice(); if (subscriberThread != null) { subscriberThread.kill(); subscriberThread = null; } if (commandQueue != null) { commandQueue.kill(); commandQueue = null; } } private void reset() { // Check if already closed if (mCallback == null) { return; } if (subscriberThread != null) { subscriberThread.kill(); subscriberThread = null; } if (commandQueue != null) { commandQueue.clear(); commandQueue.setReady(false); } // mCallback.onConnectionStateChange(ConnectionState.Disconnected); } public synchronized void connectToDevice(final BluetoothDevice device) { if (mBluetoothGatt != null) { return; } mDevice = device; if (subscriberThread == null) { subscriberThread = new CharacteristicSubscriberThread(this); subscriberThread.start(); } else { subscriberThread.reset(); } Handler handler = new Handler(mContext.getMainLooper()); handler.post(new Runnable() { @Override public void run() { mBluetoothGatt = device.connectGatt(mContext, false, mBluetoothGattCallback); Log.i(LOG_TAG, "Connecting...: " + device.getName()); mCallback.onConnectionStateChange(ConnectionState.Connecting); } }); } private synchronized void disconnectDevice() { try { if (mBluetoothGatt != null) { mBluetoothGatt.close(); } } catch (Exception e) { e.printStackTrace(); } finally { mBluetoothGatt = null; } } public synchronized void tryToReconnect() { if (mCallback == null) { return; } reset(); disconnectDevice(); if (mDevice == null) { mCallback.onConnectionStateChange(ConnectionState.Disconnected); return; } Log.w(LOG_TAG, "Reconnecting"); connectToDevice(mDevice); } private boolean changeGattMtu(int mtu) { int retry = 5; boolean ok = false; while (!ok && retry > 0) { ok = mBluetoothGatt.requestMtu(mtu); retry--; } return ok; } private BluetoothGatt mBluetoothGatt; private final BluetoothGattCallback mBluetoothGattCallback = new BluetoothGattCallback() { @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); Log.d(LOG_TAG, "onConnectionStateChange: " + status + " -> " + newState); if (status == BluetoothGatt.GATT_SUCCESS) { switch (newState) { case BluetoothProfile.STATE_CONNECTED: if (mCallback != null && gatt == mBluetoothGatt) { gatt.discoverServices(); subscriberThread.setDiscovering(); } break; case BluetoothProfile.STATE_DISCONNECTED: Log.e(LOG_TAG, "Disconnected"); gatt.close(); Log.w(LOG_TAG, "Closed"); if (mCallback == null || gatt != mBluetoothGatt) { return; } mBluetoothGatt = null; tryToReconnect(); break; } } else { Log.wtf(LOG_TAG, "ON CONNECTION STATE CHANGED ERROR: " + status); gatt.close(); Log.w(LOG_TAG, "Closed"); if (mCallback == null || gatt != mBluetoothGatt) { return; } mBluetoothGatt = null; switch (status) { case 8: // GATT CONN TIMEOUT // just reconnect when possible break; case 133: // GATT ERROR BluetoothUtils.unpairDevice(gatt.getDevice()); default: mDevice = null; } /* switch (status) { case BluetoothGatt.GATT_SUCCESS: return "SUCCESS"; case 0x01: return "GATT CONN L2C FAILURE"; case 0x08: return "GATT CONN TIMEOUT"; case 0x13: return "GATT CONN TERMINATE PEER USER"; case 0x16: return "GATT CONN TERMINATE LOCAL HOST"; case 0x3E: return "GATT CONN FAIL ESTABLISH"; case 0x22: return "GATT CONN LMP TIMEOUT"; case 0x0100: return "GATT CONN CANCEL "; case 0x0085: return "GATT ERROR"; // Device not reachable default: return "UNKNOWN (" + error + ")"; } */ tryToReconnect(); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); Log.d(LOG_TAG, "onServicesDiscovered: " + status); // Check if already closed if (mCallback == null || gatt != mBluetoothGatt) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { if (!changeGattMtu(512)) { mCallback.onReadyToSubscribe(gatt); } } } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); // Check if already closed if (mCallback == null || gatt != mBluetoothGatt) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(LOG_TAG, "Descriptor write successful: " + descriptor.getCharacteristic().getUuid().toString()); subscriberThread.remove(); } else { BluetoothDevice device = gatt.getDevice(); // Don't do anything while bonding if (device == null || device.getBondState() != BluetoothDevice.BOND_BONDING) { Log.e(LOG_TAG, "Status: write not permitted"); BluetoothUtils.unpairDevice(device); mDevice = null; tryToReconnect(); } } } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); // Check if already closed if (mCallback == null || gatt != mBluetoothGatt) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { Log.d(LOG_TAG, "Characteristic write successful: " + characteristic.getUuid().toString()); commandQueue.remove(); } else { Log.w(LOG_TAG, "Characteristic write error: " + status + " :: " + characteristic.getUuid().toString()); commandQueue.moveToBack(); } Log.d(LOG_TAG, "Characteristic value: " + new String(characteristic.getValue())); } @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicRead(gatt, characteristic, status); Log.d(LOG_TAG, "onCharacteristicRead status:: " + status); // Check if already closed if (mCallback == null || gatt != mBluetoothGatt) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { mCallback.onCharacteristicChanged(characteristic); } commandQueue.remove(); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); Log.d(LOG_TAG, "onCharacteristicChanged: " + characteristic.getUuid().toString()); // Check if already closed if (mCallback == null || gatt != mBluetoothGatt) { return; } mCallback.onCharacteristicChanged(characteristic); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { super.onMtuChanged(gatt, mtu, status); Log.d(LOG_TAG, "onMtuChanged: " + mtu + " status: " + status); if (mCallback == null || gatt != mBluetoothGatt) { return; } if (status == BluetoothGatt.GATT_SUCCESS) { mCallback.onReadyToSubscribe(gatt); } } }; private BluetoothGattCharacteristic findCharacteristic(UUID serviceUUID, UUID characteristicUUID) { if (mBluetoothGatt == null) { return null; } BluetoothGattService service = mBluetoothGatt.getService(serviceUUID); if (service == null) { Log.e(LOG_TAG, "Service unavailable"); return null; } BluetoothGattCharacteristic characteristic = service.getCharacteristic(characteristicUUID); if (characteristic == null) { Log.e(LOG_TAG, "Characteristic unavailable"); return null; } return characteristic; } // region Commands public void addCommandToQueue(Command command) { commandQueue.put(command); } @Override public void handleCommand(Command command) { if (mBluetoothGatt == null) { return; } final Command finalCommand = command; Handler handler = new Handler(mContext.getMainLooper()); handler.post(new Runnable() { @Override public void run() { try { BluetoothGattCharacteristic characteristic = findCharacteristic(finalCommand.getServiceUUID(), finalCommand.getCharacteristic()); if (characteristic == null) { return; } if (finalCommand.isWriteCommand()) { // not being used // mBluetoothGattCharacteristic.setWriteType(command.getWriteType()); characteristic.setValue(finalCommand.getPacket()); characteristic.setWriteType(BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT); boolean result = mBluetoothGatt.writeCharacteristic(characteristic); Log.d(LOG_TAG, "Started writing command: " + result); } else { boolean result = mBluetoothGatt.readCharacteristic(characteristic); Log.d(LOG_TAG, "Started reading command: " + result); } } catch (Exception e) { e.printStackTrace(); } } }); } // endregion //region Subscribe requests public void addSubscribeRequests(Queue<CharacteristicIdentifier> requests) { subscriberThread.setSubscribeRequests(requests); } @Override public void subscribeCharacteristic(CharacteristicIdentifier characteristicIdentifier) { if (mBluetoothGatt == null) { return; } final CharacteristicIdentifier finalCharacteristicIdentifier = characteristicIdentifier; Handler handler = new Handler(mContext.getMainLooper()); handler.post(new Runnable() { @Override public void run() { try { BluetoothGattCharacteristic characteristic = findCharacteristic(finalCharacteristicIdentifier.getServiceUUID(), finalCharacteristicIdentifier.getCharacteristicUUID()); if (characteristic == null) { return; } mBluetoothGatt.setCharacteristicNotification(characteristic, true); BluetoothGattDescriptor descriptor = characteristic.getDescriptor(UUID.fromString(DESCRIPTOR_CONFIG)); if (descriptor == null) { Log.e(LOG_TAG, "Descriptor unavailable to write"); return; } descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE); //descriptor.setValue(BluetoothGattDescriptor.ENABLE_INDICATION_VALUE); boolean result = mBluetoothGatt.writeDescriptor(descriptor); Log.d(LOG_TAG, "Started writing descriptor: " + result); } catch (Exception e) { e.printStackTrace(); } } }); } //endregion @Override public void onConnectionFailed() { Log.w(LOG_TAG, "Connecting timed out"); if (mCallback == null) { return; } if (mBluetoothGatt != null && mBluetoothGatt.getDevice() != null) { BluetoothDevice device = mBluetoothGatt.getDevice(); int bondState = device.getBondState(); // Don't do anything while bonding if (bondState == BluetoothDevice.BOND_BONDING && mBondsFailed < 30) { Log.w(LOG_TAG, "Waiting for bond..."); mBondsFailed++; subscriberThread.setConnecting(); } else { mBondsFailed = 0; mConnectionsFailed++; if (mConnectionsFailed > 0 && mConnectionsFailed%3 == 0) { mDevice = null; BluetoothUtils.disableBluetooth(mBluetoothAdapter); } tryToReconnect(); } } else { Log.w(LOG_TAG, "Start scanning again"); mDevice = null; tryToReconnect(); } } @Override public void onSubscribingFailed() { Log.w(LOG_TAG, "Subscribing timed out"); if (mCallback == null) { return; } if (mBluetoothGatt != null && mBluetoothGatt.getDevice() != null) { BluetoothDevice device = mBluetoothGatt.getDevice(); int bondState = device.getBondState(); // Don't do anything while bonding if (bondState == BluetoothDevice.BOND_BONDING && mBondsFailed < 30) { Log.w(LOG_TAG, "Waiting for bond..."); mBondsFailed++; subscriberThread.setBonding(); } else { mBondsFailed = 0; mConnectionsFailed++; if (mConnectionsFailed >= 7) { mConnectionsFailed = 0; // Last resort to prepare for a new connection // Reset bonded devices BluetoothUtils.resetBondedDevices(mBluetoothAdapter); } if (mConnectionsFailed > 0 && mConnectionsFailed%3 == 0) { mDevice = null; BluetoothUtils.disableBluetooth(mBluetoothAdapter); } tryToReconnect(); } } else { Log.w(LOG_TAG, "Start scanning again"); mDevice = null; tryToReconnect(); } } @Override public void onConnectionReady() { Log.i(LOG_TAG, "Ready"); mCallback.onConnectionStateChange(ConnectionState.Ready); mBondsFailed = 0; mConnectionsFailed = 0; commandQueue.setReady(true); // We are done with this thread, we don't need it until disconnection subscriberThread = null; } }