/* * Copyright 2019 Fitbit, Inc. All rights reserved. * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ package com.fitbit.bluetooth.fbgatt; import com.fitbit.bluetooth.fbgatt.btcopies.BluetoothGattCharacteristicCopy; import com.fitbit.bluetooth.fbgatt.btcopies.BluetoothGattDescriptorCopy; import com.fitbit.bluetooth.fbgatt.tx.GattClientDiscoverServicesTransaction; import com.fitbit.bluetooth.fbgatt.tx.RequestGattClientPhyChangeTransaction; import com.fitbit.bluetooth.fbgatt.tx.RequestMtuGattTransaction; import com.fitbit.bluetooth.fbgatt.util.GattStatus; import com.fitbit.bluetooth.fbgatt.util.GattUtils; 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.BluetoothProfile; import android.os.Handler; import android.os.Looper; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Locale; import timber.log.Timber; /** * Common callback for the gatt clients * * Created by iowens on 10/18/17. */ public class GattClientCallback extends BluetoothGattCallback { private static final long HOT_QUEUE_EMPTYING_TIME = 1000; // we want to allow just enough for the android system queue to flush private final Handler handler; private final List<GattClientListener> listeners; private final GattUtils gattUtils = new GattUtils(); GattClientCallback() { super(); this.listeners = Collections.synchronizedList(new ArrayList<>(4)); Looper looper = FitbitGatt.getInstance().getFitbitGattAsyncOperationThread().getLooper(); this.handler = new Handler(looper); } Handler getClientCallbackHandler(){ return this.handler; } void addListener(GattClientListener gattListener) { synchronized (listeners) { if(!listeners.contains(gattListener)) { listeners.listIterator().add(gattListener); } } } void removeListener(GattClientListener gattListener) { synchronized (listeners) { Iterator<GattClientListener> listenerIterator = listeners.listIterator(); while (listenerIterator.hasNext()) { GattClientListener listener = listenerIterator.next(); if (listener.equals(gattListener)) { listenerIterator.remove(); return; } } } } List<GattClientListener> getGattClientListeners(){ ArrayList<GattClientListener> gattListeners = new ArrayList<>(listeners.size()); gattListeners.addAll(listeners); return gattListeners; } private String getDeviceMacFromGatt(BluetoothGatt gatt) { return gattUtils.debugSafeGetBtDeviceName(gatt); } @Override public void onPhyUpdate(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { super.onPhyUpdate(gatt, txPhy, rxPhy, status); Timber.v("[%s] onPhyUpdate: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { if (listener.getDevice() != null && gatt != null && listener.getDevice().equals(gatt.getDevice())) { handler.post(() -> listener.onPhyUpdate(gatt, txPhy, rxPhy, status)); } } final GattConnection conn; if (gatt != null) { conn = FitbitGatt.getInstance().getConnection(gatt.getDevice()); } else { conn = null; } if(conn != null) { // since this is one of the events that could happen asynchronously, we will // need to iterate through our connection listeners handler.post(() -> { for (ConnectionEventListener asyncConnListener : conn.getConnectionEventListeners()) { TransactionResult.Builder builder = new TransactionResult.Builder(); if(status == BluetoothGatt.GATT_SUCCESS) { builder.resultStatus(TransactionResult.TransactionResultStatus.SUCCESS); } else { builder.resultStatus(TransactionResult.TransactionResultStatus.FAILURE); } asyncConnListener.onPhyChanged(builder .transactionName(RequestGattClientPhyChangeTransaction.NAME) .txPhy(txPhy) .rxPhy(rxPhy) .gattState(conn.getGattState()) .responseStatus(GattDisconnectReason.getReasonForCode(status).ordinal()).build(), conn); } }); } } @Override public void onPhyRead(BluetoothGatt gatt, int txPhy, int rxPhy, int status) { super.onPhyRead(gatt, txPhy, rxPhy, status); Timber.v("[%s] onPhyRead: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { if(listener.getDevice() != null && gatt != null && listener.getDevice().equals(gatt.getDevice())) { handler.post(() -> listener.onPhyRead(gatt, txPhy, rxPhy, status)); } } } @Override public void onConnectionStateChange(BluetoothGatt gatt, int status, int newState) { super.onConnectionStateChange(gatt, status, newState); Timber.v("[%s] onConnectionStateChange: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); Timber.d("[%s]Connection state: %s", getDeviceMacFromGatt(gatt), newState == BluetoothProfile.STATE_CONNECTED ? "Connected" : "Not-Connected"); if(status != BluetoothGatt.GATT_SUCCESS) { Timber.i("[%s] The connection state may have changed in error", getDeviceMacFromGatt(gatt)); } // gatt could be null if this is a mock GattConnection conn; if(gatt != null) { conn = FitbitGatt.getInstance().getConnection(gatt.getDevice()); if(conn == null) { Timber.i("[%s] The instance that is receiving this callback for device %s is not in the map, ignoring",getDeviceMacFromGatt(gatt), gatt.getDevice()); return; } } else { Timber.v("[%s] If GATT is null here, this must be a mock, otherwise there is a quite serious error", getDeviceMacFromGatt(gatt)); FitbitBluetoothDevice device = new FitbitBluetoothDevice("02:00:00:00:00:00", "fooDevice"); conn = new GattConnection(device, handler.getLooper()); FitbitGatt.getInstance().putConnectionIntoDevices(device, conn); } switch(newState) { case BluetoothProfile.STATE_DISCONNECTING: // never called by android case BluetoothProfile.STATE_DISCONNECTED: Timber.d("[%s] Disconnection reason: %s", getDeviceMacFromGatt(gatt), GattDisconnectReason.getReasonForCode(newState)); /* * this is tricky, once we get here, the tracker has disconnected, but we still must * wait for supervision timeout length until we can connect again, so we will force * the state into disconnecting here for the connection object that requires it and * require a wait of 2 seconds ( the average supervision timeout ask ) before moving * it to "disconnected." This will reduce the number of 133 and bad state errors * but if someone chooses the Android default 20s, or something greater than 2s * it might cause difficulty, as such I will log what is going on so someone debugging * can see what is occurring. This will be posted before the callback on the main looper * so that the state transition should appear normal in the callback. We can change the * hardcoded value when we can get information from the peripheral what the actual * connection supervision timeout is. * * The other reason to wait here is because we want to block any other operations * from trying to use this peripheral until the system has had time to work off the * backed up bluetooth operations in the queue if there were any, this number is somewhat * arbitrary, however we want for it to be less than the 60s timeout on a transaction */ if(gatt != null) { Timber.w("[%s] disconnected, waiting %dms for full disconnection", getDeviceMacFromGatt(gatt), HOT_QUEUE_EMPTYING_TIME); conn.setState(GattState.DISCONNECTING); /* * This is required so that we can cancel the connection attempt if one was pending * and not wedge the queue since we always use direct connections. If this is removed * you must ensure that there is no way to have a gattConnect call that does not ever * disconnect. * * In the event of a 133 ( cannot start new connection at conn_st: X error, or a series of them ) this will * eventually unwedge the stack along with the gatt.close() that will occur in a second. * * Please see : https://wiki.fitbit.com/pages/viewpage.action?pageId=123374229 for a lot more detail */ gatt.disconnect(); handler.postDelayed(() -> { conn.gattRelease(); Timber.i("[%s] Full disconnection", getDeviceMacFromGatt(gatt)); conn.setState(GattState.DISCONNECTED); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { // we'll want to use the fake state here so that we can wait for disconnecting and call it back // normally once we are actually disconnected after the assumed supervision timeout. handler.post(() -> listener.onConnectionStateChange(gatt, status, BluetoothProfile.STATE_DISCONNECTED)); } } // since this is one of the events that could happen asynchronously, we will // need to iterate through our connection listeners, since this is a disconnection // we will want to report failure so that upstream consumers don't get confused on a connection // attempt for (ConnectionEventListener asyncConnListener : conn.getConnectionEventListeners()) { asyncConnListener.onClientConnectionStateChanged(new TransactionResult.Builder() .resultStatus(TransactionResult.TransactionResultStatus.FAILURE) .gattState(conn.getGattState()) .responseStatus(GattDisconnectReason.getReasonForCode(status).ordinal()).build(), conn); } }, HOT_QUEUE_EMPTYING_TIME); } else { Timber.v("[%s] Gatt was null, returning disconnected state immediately", getDeviceMacFromGatt(gatt)); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { handler.post(() -> listener.onConnectionStateChange(null, status, BluetoothProfile.STATE_DISCONNECTED)); } } break; case BluetoothProfile.STATE_CONNECTED: ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { if(gatt == null) { handler.post(() -> listener.onConnectionStateChange(null, status, BluetoothProfile.STATE_CONNECTED)); } else if(listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { handler.post(() -> listener.onConnectionStateChange(gatt, status, BluetoothProfile.STATE_CONNECTED)); } else { Timber.v("[%s] We should never get here, but if we do it is not an exception", getDeviceMacFromGatt(gatt)); } } // since this is one of the events that could happen asynchronously, we will // need to iterate through our connection listeners, since this is a disconnection // we will want to report success so that upstream consumers don't get confused, // or mixed signals on a connection attempt handler.post(() -> { for(ConnectionEventListener asyncConnListener : conn.getConnectionEventListeners()) { handler.post(() -> asyncConnListener.onClientConnectionStateChanged(new TransactionResult.Builder() .resultStatus(TransactionResult.TransactionResultStatus.SUCCESS) .gattState(conn.getGattState()) .responseStatus(GattDisconnectReason.getReasonForCode(status).ordinal()).build(), conn)); } }); break; default: throw new IllegalStateException(String.format(Locale.ENGLISH, "[%s] The state returned was something unexpected", getDeviceMacFromGatt(gatt))); } } @Override public void onServicesDiscovered(BluetoothGatt gatt, int status) { super.onServicesDiscovered(gatt, status); Timber.v("[%s] onServicesDiscovered: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); for (GattClientListener listener : copy) { if(listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { handler.post(() -> listener.onServicesDiscovered(gatt, status)); } } GattConnection conn = FitbitGatt.getInstance().getConnection(gatt.getDevice()); if(conn != null) { List<BluetoothGattService> discoveredServices = gatt.getServices(); // since this is one of the events that could happen asynchronously, we will // need to iterate through our connection listeners handler.post(() -> { for (ConnectionEventListener asyncConnListener : conn.getConnectionEventListeners()) { TransactionResult.Builder builder = new TransactionResult.Builder(); builder.transactionName(GattClientDiscoverServicesTransaction.NAME); if(status == BluetoothGatt.GATT_SUCCESS) { builder.resultStatus(TransactionResult.TransactionResultStatus.SUCCESS); } else { builder.resultStatus(TransactionResult.TransactionResultStatus.FAILURE); } asyncConnListener.onServicesDiscovered(builder .transactionName(GattClientDiscoverServicesTransaction.NAME) .serverServices(discoveredServices) .gattState(conn.getGattState()) .responseStatus(GattDisconnectReason.getReasonForCode(status).ordinal()).build(), conn); } }); } } // for Characteristics and Descriptors, they are backed by c level objects and the references // are passed up to Java via JNI. This means that the values can change, so we must copy through // here @Override public void onCharacteristicRead(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicRead(gatt, characteristic, status); if (FitbitGatt.getInstance().isSlowLoggingEnabled()) { Timber.v("[%s] onCharacteristicRead: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); } ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); handler.post(() -> { for (GattClientListener listener : copy) { if(listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onCharacteristicRead(gatt, gattUtils.copyCharacteristic(characteristic), status); } } }); } @Override public void onCharacteristicWrite(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic, int status) { super.onCharacteristicWrite(gatt, characteristic, status); if (FitbitGatt.getInstance().isSlowLoggingEnabled()) { Timber.v("[%s] onCharacteristicWrite: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); } ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); final BluetoothGattCharacteristicCopy bluetoothGattCharacteristic = gattUtils.copyCharacteristic(characteristic); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onCharacteristicWrite(gatt, bluetoothGattCharacteristic, status); } } }); } @Override public void onCharacteristicChanged(BluetoothGatt gatt, BluetoothGattCharacteristic characteristic) { super.onCharacteristicChanged(gatt, characteristic); if (FitbitGatt.getInstance().isSlowLoggingEnabled()) { Timber.d("[%s] onCharacteristicChanged: [Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); } ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); BluetoothDevice device = gatt.getDevice(); final BluetoothGattCharacteristicCopy copyOfCharacteristic = gattUtils.copyCharacteristic(characteristic); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(device)) { listener.onCharacteristicChanged(gatt, copyOfCharacteristic); } } }); GattConnection conn = FitbitGatt.getInstance().getConnection(gatt.getDevice()); if (conn != null) { handler.post(() -> { // since this is async, the result status is irrelevant so it will always be // success because we received this data, as this is a snapshot of a live object // we will need to copy the values into the tx result TransactionResult result = new TransactionResult.Builder() .gattState(conn.getGattState()) .characteristicUuid(copyOfCharacteristic.getUuid()) .data(copyOfCharacteristic.getValue()) .resultStatus(TransactionResult.TransactionResultStatus.SUCCESS).build(); for (ConnectionEventListener asyncListener : conn.getConnectionEventListeners()) { asyncListener.onClientCharacteristicChanged(result, conn); } }); } else if (FitbitGatt.getInstance().isSlowLoggingEnabled()){ Timber.v("[%s] Gatt was null, we could be mocking, if so we can't notify async", getDeviceMacFromGatt(gatt)); } } @Override public void onDescriptorRead(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorRead(gatt, descriptor, status); Timber.v("[%s] onDescriptorRead: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); final BluetoothGattDescriptorCopy bluetoothGattDescriptorCopy = gattUtils.copyDescriptor(descriptor); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onDescriptorRead(gatt, bluetoothGattDescriptorCopy, status); } } }); } @Override public void onDescriptorWrite(BluetoothGatt gatt, BluetoothGattDescriptor descriptor, int status) { super.onDescriptorWrite(gatt, descriptor, status); Timber.v("[%s] onDescriptorWrite: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); final BluetoothGattDescriptorCopy bluetoothGattDescriptorCopy = gattUtils.copyDescriptor(descriptor); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onDescriptorWrite(gatt, bluetoothGattDescriptorCopy, status); } } }); } @Override public void onReliableWriteCompleted(BluetoothGatt gatt, int status) { super.onReliableWriteCompleted(gatt, status); Timber.v("[%s] onReliableWriteCompleted: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onReliableWriteCompleted(gatt, status); } } }); } @Override public void onReadRemoteRssi(BluetoothGatt gatt, int rssi, int status) { super.onReadRemoteRssi(gatt, rssi, status); Timber.v("[%s] onReadRemoteRssi: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onReadRemoteRssi(gatt, rssi, status); } } }); } @Override public void onMtuChanged(BluetoothGatt gatt, int mtu, int status) { super.onMtuChanged(gatt, mtu, status); Timber.v("[%s] onMtuChanged: Gatt Response Status %s", getDeviceMacFromGatt(gatt), GattStatus.getStatusForCode(status)); Timber.d("[%s][Threading] Originally called on thread : %s", getDeviceMacFromGatt(gatt), Thread.currentThread().getName()); ArrayList<GattClientListener> copy = new ArrayList<>(listeners.size()); copy.addAll(listeners); handler.post(() -> { for (GattClientListener listener : copy) { if (listener.getDevice() != null && listener.getDevice().equals(gatt.getDevice())) { listener.onMtuChanged(gatt, mtu, status); } } }); GattConnection conn = FitbitGatt.getInstance().getConnection(gatt.getDevice()); if(conn != null) { TransactionResult.Builder builder = new TransactionResult.Builder(); if(status == BluetoothGatt.GATT_SUCCESS) { builder.resultStatus(TransactionResult.TransactionResultStatus.SUCCESS); } else { builder.resultStatus(TransactionResult.TransactionResultStatus.FAILURE); } TransactionResult result = builder .transactionName(RequestMtuGattTransaction.NAME) .mtu(mtu) .gattState(conn.getGattState()) .responseStatus(GattDisconnectReason.getReasonForCode(status).ordinal()).build(); // since this is one of the events that could happen asynchronously, we will // need to iterate through our connection listeners handler.post(() -> { for (ConnectionEventListener asyncConnListener : conn.getConnectionEventListeners()) { asyncConnListener.onMtuChanged(result, conn); } }); } } }