diff --git a/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java b/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java index 362bbbb7..c3383927 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java @@ -180,6 +180,11 @@ abstract class BleManagerHandler extends RequestHandler { * Current connection parameters. Those values are only available starting from Android Oreo. */ private int interval, latency, timeout; + /** + * Samsung S8 with Android 9 fails to reconnect to devices requesting PHY LE 2M just after + * connection. Workaround would be to disable PHY LE 2M on the device side. + */ + private boolean earlyPhyLe2MRequest; /** * Last received battery value or -1 if value wasn't received. * @@ -753,6 +758,7 @@ private boolean internalConnect(@NonNull final BluetoothDevice device, postConnectionStateChange(o -> o.onDeviceConnecting(device)); } connectionTime = SystemClock.elapsedRealtime(); + earlyPhyLe2MRequest = false; if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) { // connectRequest will never be null here. final int preferredPhy = connectRequest.getPreferredPhy(); @@ -2301,7 +2307,10 @@ public void onConnectionStateChange(@NonNull final BluetoothGatt gatt, // ...because the next method sets them to false. // notifyDeviceDisconnected(...) may call close() - if (timeout) { + + if (status == GattError.GATT_CONN_TIMEOUT && earlyPhyLe2MRequest) { + notifyDeviceDisconnected(gatt.getDevice(), ConnectionObserver.REASON_UNSUPPORTED_CONFIGURATION); + } else if (timeout) { notifyDeviceDisconnected(gatt.getDevice(), ConnectionObserver.REASON_TIMEOUT); } else if (notSupported) { notifyDeviceDisconnected(gatt.getDevice(), ConnectionObserver.REASON_NOT_SUPPORTED); @@ -2332,7 +2341,9 @@ public void onConnectionStateChange(@NonNull final BluetoothGatt gatt, } if (cr != null) { int reason; - if (notSupported) + if (status == GattError.GATT_CONN_TIMEOUT && earlyPhyLe2MRequest) + reason = FailCallback.REASON_UNSUPPORTED_CONFIGURATION; + else if (notSupported) reason = FailCallback.REASON_DEVICE_NOT_SUPPORTED; else if (status == BluetoothGatt.GATT_SUCCESS) reason = FailCallback.REASON_DEVICE_DISCONNECTED; @@ -2524,13 +2535,9 @@ public void onCharacteristicRead(@NonNull final BluetoothGatt gatt, rr.notifySuccess(gatt.getDevice()); } } - } else if (status == 137 /* GATT AUTH FAIL */) { - // Bonding failed or was cancelled. - Log.w(TAG, "Reading failed with status " + status); - // The bond state receiver will fail the request. Stop here. - return; } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION - || status == 8 /* GATT INSUF AUTHORIZATION */) { + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { // This is called when bonding attempt failed, but the app is still trying to read. // We need to cancel the request here, as bonding won't start. log(Log.WARN, () -> "Authentication required (" + status + ")"); @@ -2578,13 +2585,9 @@ public void onCharacteristicWrite(final BluetoothGatt gatt, wr.notifySuccess(gatt.getDevice()); } } - } else if (status == 137 /* GATT AUTH FAIL */) { - // This never happens for Write operations, for some reason. - Log.w(TAG, "Writing failed with status " + status); - // The bond state receiver will fail the request. Stop here. - return; } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION - || status == 8 /* GATT INSUF AUTHORIZATION */) { + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { // This is called when bonding attempt failed, but the app is still trying to write. // We need to cancel the request here, as bonding won't start. log(Log.WARN, () -> "Authentication required (" + status + ")"); @@ -2595,6 +2598,9 @@ public void onCharacteristicWrite(final BluetoothGatt gatt, } if (request instanceof final WriteRequest wr) { wr.notifyFail(gatt.getDevice(), status); + // Automatically abort Reliable Write when write error happen + if (requestQueue instanceof final ReliableWriteRequest rwr) + rwr.notifyAndCancelQueue(gatt.getDevice()); } } else { Log.e(TAG, "onCharacteristicWrite error " + status + ", bond state: " + gatt.getDevice().getBondState()); @@ -2658,13 +2664,9 @@ public void onDescriptorRead(final @NonNull BluetoothGatt gatt, rr.notifySuccess(gatt.getDevice()); } } - } else if (status == 137 /* GATT AUTH FAIL */) { - // Bonding failed or was cancelled. - Log.w(TAG, "Reading descriptor failed with status " + status); - // The bond state receiver will fail the request. Stop here. - return; } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION - || status == 8 /* GATT INSUF AUTHORIZATION */) { + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { // This is called when bonding attempt failed, but the app is still trying to read. // We need to cancel the request here, as bonding won't start. log(Log.WARN, () -> "Authentication required (" + status + ")"); @@ -2722,13 +2724,9 @@ public void onDescriptorWrite(final BluetoothGatt gatt, wr.notifySuccess(gatt.getDevice()); } } - } else if (status == 137 /* GATT AUTH FAIL */) { - // This never happens for Write operations, for some reason. - Log.w(TAG, "Writing descriptor failed with status " + status); - // The bond state receiver will fail the request. Stop here. - return; } else if (status == BluetoothGatt.GATT_INSUFFICIENT_AUTHENTICATION - || status == 8 /* GATT INSUF AUTHORIZATION */) { + || status == 8 /* GATT INSUF AUTHORIZATION */ + || status == 137 /* GATT AUTH FAIL */) { // This is called when bonding attempt failed, but the app is still trying to write. // We need to cancel the request here, as bonding won't start. log(Log.WARN, () -> "Authentication required (" + status + ")"); @@ -2739,6 +2737,9 @@ public void onDescriptorWrite(final BluetoothGatt gatt, } if (request instanceof final WriteRequest wr) { wr.notifyFail(gatt.getDevice(), status); + // Automatically abort Reliable Write when write error happen + if (requestQueue instanceof final ReliableWriteRequest rwr) + rwr.notifyAndCancelQueue(gatt.getDevice()); } } else { Log.e(TAG, "onDescriptorWrite error " + status + ", bond state: " + gatt.getDevice().getBondState()); @@ -2958,6 +2959,9 @@ public void onPhyUpdate(@NonNull final BluetoothGatt gatt, log(Log.INFO, () -> "PHY updated (TX: " + ParserUtils.phyToString(txPhy) + ", RX: " + ParserUtils.phyToString(rxPhy) + ")"); + // Samsung S8 fails to reconnect when PHY LE 2M request is sent before service discovery. + earlyPhyLe2MRequest = earlyPhyLe2MRequest || + (txPhy == BluetoothDevice.PHY_LE_2M && !servicesDiscovered); if (request instanceof final PhyRequest pr) { pr.notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); pr.notifySuccess(gatt.getDevice()); diff --git a/ble/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java b/ble/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java index fc09d474..b2b2c9ba 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/callback/FailCallback.java @@ -23,12 +23,19 @@ package no.nordicsemi.android.ble.callback; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothGatt; +import no.nordicsemi.android.ble.BleManager; import androidx.annotation.NonNull; @FunctionalInterface public interface FailCallback { int REASON_DEVICE_DISCONNECTED = -1; + /** + * Returned when the {@link BleManager#isRequiredServiceSupported(BluetoothGatt)} + * returns false, that is when at least required GATT service was not discovered + * on the connected device. + */ int REASON_DEVICE_NOT_SUPPORTED = -2; int REASON_NULL_ATTRIBUTE = -3; int REASON_REQUEST_FAILED = -4; @@ -36,6 +43,17 @@ public interface FailCallback { int REASON_VALIDATION = -6; int REASON_CANCELLED = -7; int REASON_NOT_ENABLED = -8; + /** + * The Android device is unable to reconnect to the peripheral because of internal failure. + * Most probably it cannot respond properly to PHY LE 2M update procedure, causing the + * remote device to terminate the connection. + *

+ * Try disabling PHY LE 2M on the peripheral side, or update the Android version. + * If that's not possible, the connection to your device may not work on the given + * Android device at all. If the device is bonded, try removing bonding and connect, + * but this seems to fix the problem only before a new bond is created. + */ + int REASON_UNSUPPORTED_CONFIGURATION = -9; int REASON_BLUETOOTH_DISABLED = -100; /** @@ -47,7 +65,8 @@ public interface FailCallback { * {@link #REASON_DEVICE_DISCONNECTED}, {@link #REASON_TIMEOUT}, * {@link #REASON_DEVICE_NOT_SUPPORTED} (only for Connect request), * {@link #REASON_BLUETOOTH_DISABLED}, {@link #REASON_NULL_ATTRIBUTE}, - * {@link #REASON_VALIDATION}, {@link #REASON_CANCELLED}, {@link #REASON_NOT_ENABLED} + * {@link #REASON_VALIDATION}, {@link #REASON_CANCELLED}, {@link #REASON_NOT_ENABLED}, + * {@link #REASON_UNSUPPORTED_CONFIGURATION}, * or {@link #REASON_REQUEST_FAILED} (for other reason). */ void onRequestFailed(@NonNull final BluetoothDevice device, final int status); diff --git a/ble/src/main/java/no/nordicsemi/android/ble/error/GattError.java b/ble/src/main/java/no/nordicsemi/android/ble/error/GattError.java index f0fea34e..7644da02 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/error/GattError.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/error/GattError.java @@ -104,6 +104,7 @@ public static String parseConnectionError(final int error) { return switch (error) { case GATT_SUCCESS -> "SUCCESS"; case GATT_CONN_L2C_FAILURE -> "GATT CONN L2C FAILURE"; + case GATT_INSUF_AUTHENTICATION -> "GATT INSUF AUTHENTICATION"; case GATT_CONN_TIMEOUT -> "GATT CONN TIMEOUT"; case GATT_CONN_TERMINATE_PEER_USER -> "GATT CONN TERMINATE PEER USER"; case GATT_CONN_TERMINATE_LOCAL_HOST -> "GATT CONN TERMINATE LOCAL HOST"; diff --git a/ble/src/main/java/no/nordicsemi/android/ble/observer/ConnectionObserver.java b/ble/src/main/java/no/nordicsemi/android/ble/observer/ConnectionObserver.java index 20b5d139..4fba475a 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/observer/ConnectionObserver.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/observer/ConnectionObserver.java @@ -49,7 +49,7 @@ public interface ConnectionObserver { * Android will try to reconnect automatically. */ int REASON_LINK_LOSS = 3; - /** The device does not hav required services. */ + /** The device does not have required services. */ int REASON_NOT_SUPPORTED = 4; /** Connection attempt was cancelled. */ int REASON_CANCELLED = 5; @@ -58,6 +58,17 @@ public interface ConnectionObserver { * or doesn't respond for another reason. */ int REASON_TIMEOUT = 10; + /** + * The Android device is unable to reconnect to the peripheral because of internal failure. + * Most probably it cannot respond properly to PHY LE 2M update procedure, causing the + * remote device to terminate the connection. + *

+ * Try disabling PHY LE 2M on the peripheral side, or update the Android version. + * If that's not possible, the connection to your device may not work on the given + * Android device at all. If the device is bonded, try removing bonding and connect, + * but this seems to fix the problem only before a new bond is created. + */ + int REASON_UNSUPPORTED_CONFIGURATION = 11; /** * Called when the Android device started connecting to given device.