diff --git a/.github/workflows/deploy-all.yml b/.github/workflows/deploy-all.yml index bdfe8ec1..6a47afd3 100644 --- a/.github/workflows/deploy-all.yml +++ b/.github/workflows/deploy-all.yml @@ -44,7 +44,7 @@ jobs: # The following env variables are used by gradle/publish-root.gradle OSSR_USERNAME: ${{ secrets.OSSR_USERNAME }} OSSR_PASSWORD: ${{ secrets.OSSR_PASSWORD }} - SONATYPE_STATING_PROFILE_ID: ${{ secrets.SONATYPE_STATING_PROFILE_ID }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} # The script generates sec.gpg file that is required by gradle/publish-module.gradle # and starts :deployNexus lane using fastlane. run: | diff --git a/.github/workflows/deploy-to-nexus.yml b/.github/workflows/deploy-to-nexus.yml index aee59dfa..487d3383 100644 --- a/.github/workflows/deploy-to-nexus.yml +++ b/.github/workflows/deploy-to-nexus.yml @@ -20,7 +20,7 @@ jobs: # The following env variables are used by gradle/publish-root.gradle OSSR_USERNAME: ${{ secrets.OSSR_USERNAME }} OSSR_PASSWORD: ${{ secrets.OSSR_PASSWORD }} - SONATYPE_STATING_PROFILE_ID: ${{ secrets.SONATYPE_STATING_PROFILE_ID }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} # The script generates sec.gpg file that is required by gradle/publish-module.gradle # and starts :deployNexus lane using fastlane. run: | diff --git a/README.md b/README.md index 146422d4..c2898446 100644 --- a/README.md +++ b/README.md @@ -27,24 +27,24 @@ The library may be found on Maven Central repository. Add it to your project by adding the following dependency: ```groovy -implementation 'no.nordicsemi.android:ble:2.7.5' +implementation 'no.nordicsemi.android:ble:2.9.0-beta03' ``` The last version not migrated to AndroidX is 2.0.5. BLE library with Kotlin extension is available in: ```groovy -implementation 'no.nordicsemi.android:ble-ktx:2.7.5' +implementation 'no.nordicsemi.android:ble-ktx:2.9.0-beta03' ``` To import the BLE library with set of parsers for common Bluetooth SIG characteristics, use: ```groovy -implementation 'no.nordicsemi.android:ble-common:2.7.5' +implementation 'no.nordicsemi.android:ble-common:2.9.0-beta03' ``` For more information, read [this](BLE-COMMON.md). An extension for easier integration with `LiveData` is available after adding: ```groovy -implementation 'no.nordicsemi.android:ble-livedata:2.7.5' +implementation 'no.nordicsemi.android:ble-livedata:2.9.0-beta03' ``` This extension adds `ObservableBleManager` with `state` and `bondingState` properties, which notify about connection and bond state using `androidx.lifecycle.LiveData`. diff --git a/ble-ktx/build.gradle b/ble-ktx/build.gradle index 8506aaab..7a0bb3ef 100644 --- a/ble-ktx/build.gradle +++ b/ble-ktx/build.gradle @@ -38,7 +38,7 @@ android { dependencies { api project(':ble') - api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + api 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' } // === Maven Central configuration === diff --git a/ble-ktx/src/main/java/no/nordicsemi/android/ble/ktx/RequestSuspend.kt b/ble-ktx/src/main/java/no/nordicsemi/android/ble/ktx/RequestSuspend.kt index d9155d0b..5858c014 100644 --- a/ble-ktx/src/main/java/no/nordicsemi/android/ble/ktx/RequestSuspend.kt +++ b/ble-ktx/src/main/java/no/nordicsemi/android/ble/ktx/RequestSuspend.kt @@ -11,6 +11,7 @@ import no.nordicsemi.android.ble.data.Data import no.nordicsemi.android.ble.exception.* import no.nordicsemi.android.ble.response.ReadResponse import no.nordicsemi.android.ble.response.WriteResponse +import kotlin.coroutines.cancellation.CancellationException import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine @@ -240,12 +241,14 @@ suspend fun WaitForValueChangedRequest.suspend(): Data = suspendCancellableCoro .invalid { continuation.resumeWithException(InvalidRequestException(this)) } .fail { _, status -> val exception = when (status) { - FailCallback.REASON_CANCELLED -> return@fail + FailCallback.REASON_CANCELLED -> CancellationException("Request cancelled.") FailCallback.REASON_BLUETOOTH_DISABLED -> BluetoothDisabledException() FailCallback.REASON_DEVICE_DISCONNECTED -> DeviceDisconnectedException() else -> RequestFailedException(this, status) } - continuation.resumeWithException(exception) + if (continuation.isActive) { + continuation.resumeWithException(exception) + } } .done { continuation.resume(data!!) } // .then is called after both .done and .fail @@ -325,12 +328,14 @@ suspend fun WaitForReadRequest.suspend(): Data = suspendCancellableCoroutine { .invalid { continuation.resumeWithException(InvalidRequestException(this)) } .fail { _, status -> val exception = when (status) { - FailCallback.REASON_CANCELLED -> return@fail + FailCallback.REASON_CANCELLED -> CancellationException("Request cancelled.") FailCallback.REASON_BLUETOOTH_DISABLED -> BluetoothDisabledException() FailCallback.REASON_DEVICE_DISCONNECTED -> DeviceDisconnectedException() else -> RequestFailedException(this, status) } - continuation.resumeWithException(exception) + if (continuation.isActive) { + continuation.resumeWithException(exception) + } } .done { continuation.resume(data!!) } // .then is called after both .done and .fail @@ -394,12 +399,14 @@ private suspend fun TimeoutableRequest.suspendCancellable() = suspendCancellable .invalid { continuation.resumeWithException(InvalidRequestException(this)) } .fail { _, status -> val exception = when (status) { - FailCallback.REASON_CANCELLED -> return@fail + FailCallback.REASON_CANCELLED -> CancellationException("Request cancelled.") FailCallback.REASON_BLUETOOTH_DISABLED -> BluetoothDisabledException() FailCallback.REASON_DEVICE_DISCONNECTED -> DeviceDisconnectedException() else -> RequestFailedException(this, status) } - continuation.resumeWithException(exception) + if (continuation.isActive) { + continuation.resumeWithException(exception) + } } .done { continuation.resume(Unit) } // .then is called after both .done and .fail diff --git a/ble-livedata/build.gradle b/ble-livedata/build.gradle index 5c4f258e..0a649f7b 100644 --- a/ble-livedata/build.gradle +++ b/ble-livedata/build.gradle @@ -35,7 +35,7 @@ dependencies { api project(':ble') // https://developer.android.com/jetpack/androidx/releases/lifecycle - api 'androidx.lifecycle:lifecycle-livedata:2.6.2' + api 'androidx.lifecycle:lifecycle-livedata:2.8.6' } // === Maven Central configuration === diff --git a/ble/build.gradle b/ble/build.gradle index b994566a..9a4ef533 100644 --- a/ble/build.gradle +++ b/ble/build.gradle @@ -32,7 +32,9 @@ android { } dependencies { - api 'androidx.annotation:annotation:1.7.0' + api 'androidx.annotation:annotation:1.8.2' + //noinspection GradleDependency + implementation 'androidx.core:core:1.12.0' // Don't upgrade to 1.13.0, as it increases the minSdk to 19. testImplementation 'junit:junit:4.13.2' } diff --git a/ble/src/main/java/no/nordicsemi/android/ble/BleManager.java b/ble/src/main/java/no/nordicsemi/android/ble/BleManager.java index bb9e4653..529e86ac 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/BleManager.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/BleManager.java @@ -47,6 +47,8 @@ import androidx.annotation.RequiresPermission; import androidx.annotation.RestrictTo; import androidx.annotation.StringRes; +import androidx.core.content.ContextCompat; + import no.nordicsemi.android.ble.annotation.ConnectionPriority; import no.nordicsemi.android.ble.annotation.ConnectionState; import no.nordicsemi.android.ble.annotation.LogPriority; @@ -178,9 +180,10 @@ public BleManager(@NonNull final Context context, @NonNull final Handler handler this.requestHandler = getGattCallback(); this.requestHandler.init(this, handler); - context.registerReceiver(mPairingRequestBroadcastReceiver, + ContextCompat.registerReceiver(context, mPairingRequestBroadcastReceiver, // BluetoothDevice.ACTION_PAIRING_REQUEST - new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST")); + new IntentFilter("android.bluetooth.device.action.PAIRING_REQUEST"), + ContextCompat.RECEIVER_EXPORTED); } /** 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 d3a9edda..3cdfe377 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/BleManagerHandler.java @@ -29,6 +29,7 @@ import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; import androidx.annotation.RequiresPermission; +import androidx.core.content.ContextCompat; import java.lang.reflect.Method; import java.security.InvalidParameterException; @@ -260,8 +261,7 @@ public void onReceive(final Context context, final Intent intent) { && previousState != BluetoothAdapter.STATE_OFF) { // No more calls are possible operationInProgress = true; - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_BLUETOOTH_DISABLED); ready = false; final BluetoothDevice device = bluetoothDevice; @@ -337,7 +337,14 @@ public void onReceive(final Context context, final Intent intent) { postCallback(c -> c.onBondingFailed(device)); postBondingStateChange(o -> o.onBondingFailed(device)); log(Log.WARN, () -> "Bonding failed"); - if (request != null && (request.type == Request.Type.CREATE_BOND || request.type == Request.Type.ENSURE_BOND)) { + if (request != null && ( + request.type == Request.Type.CREATE_BOND || + request.type == Request.Type.ENSURE_BOND || + // The following requests may trigger bonding. + request.type == Request.Type.WRITE || + request.type == Request.Type.WRITE_DESCRIPTOR || + request.type == Request.Type.READ || + request.type == Request.Type.READ_DESCRIPTOR)) { request.notifyFail(device, FailCallback.REASON_REQUEST_FAILED); request = null; } @@ -545,8 +552,7 @@ void close() { // Setting this flag to false would allow to enqueue a new request before the // current one ends processing. The following line should not be uncommented. // mGattCallback.operationInProgress = false; - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_DEVICE_DISCONNECTED); initialization = false; bluetoothDevice = null; connected = false; @@ -560,6 +566,37 @@ void close() { } } + /** + * This method clears the task queues and notifies removed requests of cancellation. + * @param status the reason of cancellation. + */ + private void emptyTasks(final int status) { + final BluetoothDevice oldBluetoothDevice = bluetoothDevice; + if (initQueue != null) { + for (final Request task : initQueue) { + if (oldBluetoothDevice != null) + task.notifyFail(oldBluetoothDevice, status); + else + task.notifyInvalidRequest(); + } + initQueue = null; + } + for (final Request task : taskQueue) { + if (oldBluetoothDevice != null) { + if (status == FailCallback.REASON_BLUETOOTH_DISABLED || + task.characteristic != null || + task.descriptor != null) { + task.notifyFail(oldBluetoothDevice, status); + } else { + task.notifyFail(oldBluetoothDevice, FailCallback.REASON_CANCELLED); + } + } else { + task.notifyInvalidRequest(); + } + } + taskQueue.clear(); + } + public BluetoothDevice getBluetoothDevice() { return bluetoothDevice; } @@ -677,10 +714,10 @@ private boolean internalConnect(@NonNull final BluetoothDevice device, } else { if (connectRequest != null) { // Register bonding broadcast receiver - context.registerReceiver(bluetoothStateBroadcastReceiver, - new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); - context.registerReceiver(mBondingBroadcastReceiver, - new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); + ContextCompat.registerReceiver(context, bluetoothStateBroadcastReceiver, + new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED), ContextCompat.RECEIVER_EXPORTED); + ContextCompat.registerReceiver(context, mBondingBroadcastReceiver, + new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED), ContextCompat.RECEIVER_EXPORTED); } } } @@ -749,7 +786,7 @@ private boolean internalConnect(@NonNull final BluetoothDevice device, return true; } - private boolean internalDisconnect(final int reason) { + private void internalDisconnect(final int reason) { userDisconnected = true; initialConnection = false; ready = false; @@ -775,7 +812,7 @@ private boolean internalDisconnect(final int reason) { log(Log.DEBUG, () -> "gatt.disconnect()"); gatt.disconnect(); if (wasConnected) - return true; + return; // If the device wasn't connected, there will be no callback after calling // gatt.disconnect(), the connection attempt will be stopped. @@ -795,7 +832,6 @@ private boolean internalDisconnect(final int reason) { r.notifyInvalidRequest(); } nextRequest(true); - return true; } @RequiresPermission(Manifest.permission.BLUETOOTH_ADMIN) @@ -1450,7 +1486,9 @@ ValueChangedCallback getValueChangedCallback(@Nullable final Object attribute) { if (callback == null) { callback = new ValueChangedCallback(this); if (attribute != null) { - valueChangedCallbacks.put(attribute, callback); + synchronized (valueChangedCallbacks) { + valueChangedCallbacks.put(attribute, callback); + } } } else if (bluetoothDevice != null) { callback.notifyClosed(); @@ -1465,9 +1503,11 @@ ValueChangedCallback getValueChangedCallback(@Nullable final Object attribute) { * @param attribute attribute to unbind the callback from. */ void removeValueChangedCallback(@Nullable final Object attribute) { - final ValueChangedCallback callback = valueChangedCallbacks.remove(attribute); - if (callback != null) { - callback.notifyClosed(); + synchronized (valueChangedCallbacks) { + final ValueChangedCallback callback = valueChangedCallbacks.remove(attribute); + if (callback != null) { + callback.notifyClosed(); + } } } @@ -1613,8 +1653,7 @@ final void enqueue(@NonNull final Request request) { @Override final void cancelQueue() { - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_CANCELLED); initialization = false; final BluetoothDevice device = bluetoothDevice; @@ -1639,18 +1678,18 @@ final void cancelCurrent() { return; log(Log.WARN, () -> "Request cancelled"); - if (request instanceof TimeoutableRequest) { - request.notifyFail(device, FailCallback.REASON_CANCELLED); + if (request instanceof final TimeoutableRequest r) { + r.notifyFail(device, FailCallback.REASON_CANCELLED); } if (awaitingRequest != null) { awaitingRequest.notifyFail(device, FailCallback.REASON_CANCELLED); awaitingRequest = null; } - if (requestQueue instanceof ReliableWriteRequest) { + if (requestQueue instanceof final ReliableWriteRequest rwr) { // Cancelling a Reliable Write request requires sending Abort command. // Instead of notifying failure, we will remove all enqueued tasks and // let the nextRequest to sent Abort command. - requestQueue.cancelQueue(); + rwr.notifyAndCancelQueue(device); } else if (requestQueue != null) { requestQueue.notifyFail(device, FailCallback.REASON_CANCELLED); requestQueue = null; @@ -1660,13 +1699,13 @@ final void cancelCurrent() { @Override final void onRequestTimeout(@NonNull final BluetoothDevice device, @NonNull final TimeoutableRequest tr) { - if (tr instanceof SleepRequest) { - tr.notifySuccess(device); + if (tr instanceof final SleepRequest sr) { + sr.notifySuccess(device); } else { log(Log.WARN, () -> "Request timed out"); } - if (request instanceof TimeoutableRequest) { - request.notifyFail(device, FailCallback.REASON_TIMEOUT); + if (request instanceof final TimeoutableRequest r) { + r.notifyFail(device, FailCallback.REASON_TIMEOUT); } if (awaitingRequest != null) { awaitingRequest.notifyFail(device, FailCallback.REASON_TIMEOUT); @@ -1975,10 +2014,12 @@ void notifyDeviceDisconnected(@NonNull final BluetoothDevice device, final int s // automatically. // This may be only called when the shouldAutoConnect() method returned true. } - for (final ValueChangedCallback callback : valueChangedCallbacks.values()) { - callback.notifyClosed(); + synchronized (valueChangedCallbacks) { + for (final ValueChangedCallback callback : valueChangedCallbacks.values()) { + callback.notifyClosed(); + } + valueChangedCallbacks.clear(); } - valueChangedCallbacks.clear(); dataProviders.clear(); batteryLevelNotificationCallback = null; batteryValue = -1; @@ -2229,8 +2270,7 @@ public void onConnectionStateChange(@NonNull final BluetoothGatt gatt, return; } - if (cr != null && cr.shouldAutoConnect() && initialConnection - && gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { + if (cr != null && cr.shouldAutoConnect() && initialConnection) { log(Log.DEBUG, () -> "autoConnect = false called failed; retrying with autoConnect = true" + (connected ? "; reset connected to false" : "")); // fix:https://github.com/NordicSemiconductor/Android-BLE-Library/issues/497 @@ -2245,8 +2285,7 @@ public void onConnectionStateChange(@NonNull final BluetoothGatt gatt, } operationInProgress = true; // no more calls are possible - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_DEVICE_DISCONNECTED); ready = false; // Store the current value of the connected and deviceNotSupported flags... @@ -2300,6 +2339,12 @@ else if (status == GattError.GATT_ERROR && timeout) // Reset flag, so the next Connect could be enqueued. operationInProgress = false; + + // return because Request.Type.REMOVE_BOND not handled + if (r != null && r.type == Request.Type.REMOVE_BOND) { + return; + } + // Try to reconnect if the initial connection was lost because of a link loss, // and shouldAutoConnect() returned true during connection attempt. // This time it will set the autoConnect flag to true (gatt.connect() forces @@ -2435,8 +2480,7 @@ public void onServiceChanged(@NonNull final BluetoothGatt gatt) { manager.onServicesInvalidated(); onDeviceDisconnected(); // Clear queues, services are no longer valid. - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_NULL_ATTRIBUTE); // And discover services again serviceDiscoveryRequested = true; servicesDiscovered = false; @@ -2473,21 +2517,28 @@ 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 == 137 /* GATT AUTH FAIL */) { + || status == 8 /* GATT INSUF AUTHORIZATION */) { + // 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 + ")"); - if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); postCallback(c -> c.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status)); } - // The request will be repeated when the bond state changes to BONDED. - return; + if (request instanceof final ReadRequest wr) { + wr.notifyFail(gatt.getDevice(), status); + } } else { - Log.e(TAG, "onCharacteristicRead error " + status); - if (request instanceof ReadRequest) { - request.notifyFail(gatt.getDevice(), status); + Log.e(TAG, "onCharacteristicRead error " + status + ", bond state: " + gatt.getDevice().getBondState()); + if (request instanceof final ReadRequest rr) { + rr.notifyFail(gatt.getDevice(), status); } awaitingRequest = null; onError(gatt.getDevice(), ERROR_READ_CHARACTERISTIC, status); @@ -2511,33 +2562,40 @@ public void onCharacteristicWrite(final BluetoothGatt gatt, // This method also compares the data written with the data received in the callback // if the write type is WRITE_TYPE_DEFAULT. final boolean valid = wr.notifyPacketSent(gatt.getDevice(), characteristic.getValue()); - if (!valid && requestQueue instanceof ReliableWriteRequest) { + if (!valid && requestQueue instanceof final ReliableWriteRequest rwr) { wr.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION); - requestQueue.cancelQueue(); + rwr.notifyAndCancelQueue(gatt.getDevice()); } else if (wr.hasMore()) { enqueueFirst(wr); } else { 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 == 137 /* GATT AUTH FAIL */) { + || status == 8 /* GATT INSUF AUTHORIZATION */) { + // 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 + ")"); - if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); postCallback(c -> c.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status)); } - // The request will be repeated when the bond state changes to BONDED. - return; + if (request instanceof final WriteRequest wr) { + wr.notifyFail(gatt.getDevice(), status); + } } else { - Log.e(TAG, "onCharacteristicWrite error " + status); - if (request instanceof WriteRequest) { - request.notifyFail(gatt.getDevice(), status); + Log.e(TAG, "onCharacteristicWrite error " + status + ", bond state: " + gatt.getDevice().getBondState()); + if (request instanceof final WriteRequest wr) { + wr.notifyFail(gatt.getDevice(), status); // Automatically abort Reliable Write when write error happen - if (requestQueue instanceof ReliableWriteRequest) - requestQueue.cancelQueue(); + if (requestQueue instanceof final ReliableWriteRequest rwr) + rwr.notifyAndCancelQueue(gatt.getDevice()); } awaitingRequest = null; onError(gatt.getDevice(), ERROR_WRITE_CHARACTERISTIC, status); @@ -2570,38 +2628,51 @@ public void onReliableWriteCompleted(@NonNull final BluetoothGatt gatt, } @Override - public void onDescriptorRead(final BluetoothGatt gatt, final BluetoothGattDescriptor descriptor, final int status) { - final byte[] data = descriptor.getValue(); + public void onDescriptorRead(final BluetoothGatt gatt, + final BluetoothGattDescriptor descriptor, + final int status) { + onDescriptorRead(gatt, descriptor, status, descriptor.getValue()); + } + @Override + public void onDescriptorRead(final @NonNull BluetoothGatt gatt, + final @NonNull BluetoothGattDescriptor descriptor, + final int status, final @NonNull byte[] data) { if (status == BluetoothGatt.GATT_SUCCESS) { log(Log.INFO, () -> "Read Response received from descr. " + descriptor.getUuid() + ", value: " + ParserUtils.parse(data)); BleManagerHandler.this.onDescriptorRead(gatt, descriptor); - if (request instanceof ReadRequest) { - final ReadRequest request = (ReadRequest) BleManagerHandler.this.request; - request.notifyValueChanged(gatt.getDevice(), data); - if (request.hasMore()) { - enqueueFirst(request); + if (request instanceof final ReadRequest rr) { + rr.notifyValueChanged(gatt.getDevice(), data); + if (rr.hasMore()) { + enqueueFirst(rr); } else { - request.notifySuccess(gatt.getDevice()); + 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 == 137 /* GATT AUTH FAIL */) { + || status == 8 /* GATT INSUF AUTHORIZATION */) { + // 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 + ")"); - if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); postCallback(c -> c.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status)); } - // The request will be repeated when the bond state changes to BONDED. - return; + if (request instanceof final ReadRequest wr) { + wr.notifyFail(gatt.getDevice(), status); + } } else { - Log.e(TAG, "onDescriptorRead error " + status); - if (request instanceof ReadRequest) { - request.notifyFail(gatt.getDevice(), status); + Log.e(TAG, "onDescriptorRead error " + status + ", bond state: " + gatt.getDevice().getBondState()); + if (request instanceof final ReadRequest rr) { + rr.notifyFail(gatt.getDevice(), status); } awaitingRequest = null; onError(gatt.getDevice(), ERROR_READ_DESCRIPTOR, status); @@ -2635,33 +2706,40 @@ public void onDescriptorWrite(final BluetoothGatt gatt, } if (request instanceof final WriteRequest wr) { final boolean valid = wr.notifyPacketSent(gatt.getDevice(), data); - if (!valid && requestQueue instanceof ReliableWriteRequest) { + if (!valid && requestQueue instanceof final ReliableWriteRequest rwr) { wr.notifyFail(gatt.getDevice(), FailCallback.REASON_VALIDATION); - requestQueue.cancelQueue(); + rwr.notifyAndCancelQueue(gatt.getDevice()); } else if (wr.hasMore()) { enqueueFirst(wr); } else { 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 == 137 /* GATT AUTH FAIL */) { + || status == 8 /* GATT INSUF AUTHORIZATION */) { + // 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 + ")"); - if (gatt.getDevice().getBondState() != BluetoothDevice.BOND_NONE) { + if (gatt.getDevice().getBondState() == BluetoothDevice.BOND_BONDED) { // This should never happen but it used to: http://stackoverflow.com/a/20093695/2115352 Log.w(TAG, ERROR_AUTH_ERROR_WHILE_BONDED); postCallback(c -> c.onError(gatt.getDevice(), ERROR_AUTH_ERROR_WHILE_BONDED, status)); } - // The request will be repeated when the bond state changes to BONDED. - return; + if (request instanceof final WriteRequest wr) { + wr.notifyFail(gatt.getDevice(), status); + } } else { - Log.e(TAG, "onDescriptorWrite error " + status); - if (request instanceof WriteRequest) { - request.notifyFail(gatt.getDevice(), status); + Log.e(TAG, "onDescriptorWrite error " + status + ", bond state: " + gatt.getDevice().getBondState()); + if (request instanceof final WriteRequest wr) { + wr.notifyFail(gatt.getDevice(), status); // Automatically abort Reliable Write when write error happen - if (requestQueue instanceof ReliableWriteRequest) - requestQueue.cancelQueue(); + if (requestQueue instanceof final ReliableWriteRequest rwr) + rwr.notifyAndCancelQueue(gatt.getDevice()); } awaitingRequest = null; onError(gatt.getDevice(), ERROR_WRITE_DESCRIPTOR, status); @@ -2695,8 +2773,7 @@ public void onCharacteristicChanged( manager.onServicesInvalidated(); onDeviceDisconnected(); // Clear queues, services are no longer valid. - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_NULL_ATTRIBUTE); serviceDiscoveryRequested = true; log(Log.VERBOSE, () -> "Discovering Services..."); log(Log.DEBUG, () -> "gatt.discoverServices()"); @@ -2767,14 +2844,14 @@ public void onMtuChanged(@NonNull final BluetoothGatt gatt, log(Log.INFO, () -> "MTU changed to: " + mtu); BleManagerHandler.this.mtu = Math.min(515, mtu); BleManagerHandler.this.onMtuChanged(gatt, BleManagerHandler.this.mtu); - if (request instanceof MtuRequest) { - ((MtuRequest) request).notifyMtuChanged(gatt.getDevice(), BleManagerHandler.this.mtu); - request.notifySuccess(gatt.getDevice()); + if (request instanceof final MtuRequest mr) { + mr.notifyMtuChanged(gatt.getDevice(), BleManagerHandler.this.mtu); + mr.notifySuccess(gatt.getDevice()); } } else { Log.e(TAG, "onMtuChanged error: " + status + ", mtu: " + mtu); - if (request instanceof MtuRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final MtuRequest mr) { + mr.notifyFail(gatt.getDevice(), status); awaitingRequest = null; } onError(gatt.getDevice(), ERROR_MTU_REQUEST, status); @@ -2826,10 +2903,9 @@ public void onConnectionUpdated(@NonNull final BluetoothGatt gatt, cpuc.onConnectionUpdated(gatt.getDevice(), interval, latency, timeout); } // This callback may be called af any time, also when some other request is executed - if (request instanceof ConnectionPriorityRequest) { - ((ConnectionPriorityRequest) request) - .notifyConnectionPriorityChanged(gatt.getDevice(), interval, latency, timeout); - request.notifySuccess(gatt.getDevice()); + if (request instanceof final ConnectionPriorityRequest cpr) { + cpr.notifyConnectionPriorityChanged(gatt.getDevice(), interval, latency, timeout); + cpr.notifySuccess(gatt.getDevice()); } } else if (status == 0x3b) { // HCI_ERR_UNACCEPT_CONN_INTERVAL Log.e(TAG, "onConnectionUpdated received status: Unacceptable connection interval, " + @@ -2840,8 +2916,8 @@ public void onConnectionUpdated(@NonNull final BluetoothGatt gatt, "latency: " + latency + ", timeout: " + (timeout * 10) + "ms)"); // This callback may be called af any time, also when some other request is executed - if (request instanceof ConnectionPriorityRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final ConnectionPriorityRequest cpr) { + cpr.notifyFail(gatt.getDevice(), status); awaitingRequest = null; } } else { @@ -2853,8 +2929,8 @@ public void onConnectionUpdated(@NonNull final BluetoothGatt gatt, "latency: " + latency + ", timeout: " + (timeout * 10) + "ms)"); // This callback may be called af any time, also when some other request is executed - if (request instanceof ConnectionPriorityRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final ConnectionPriorityRequest cpr) { + cpr.notifyFail(gatt.getDevice(), status); awaitingRequest = null; } postCallback(c -> c.onError(gatt.getDevice(), ERROR_CONNECTION_PRIORITY_REQUEST, status)); @@ -2875,14 +2951,14 @@ public void onPhyUpdate(@NonNull final BluetoothGatt gatt, log(Log.INFO, () -> "PHY updated (TX: " + ParserUtils.phyToString(txPhy) + ", RX: " + ParserUtils.phyToString(rxPhy) + ")"); - if (request instanceof PhyRequest) { - ((PhyRequest) request).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); - request.notifySuccess(gatt.getDevice()); + if (request instanceof final PhyRequest pr) { + pr.notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); + pr.notifySuccess(gatt.getDevice()); } } else { log(Log.WARN, () -> "PHY updated failed with status " + status); - if (request instanceof PhyRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final PhyRequest pr) { + pr.notifyFail(gatt.getDevice(), status); awaitingRequest = null; } postCallback(c -> c.onError(gatt.getDevice(), ERROR_PHY_UPDATE, status)); @@ -2903,14 +2979,14 @@ public void onPhyRead(@NonNull final BluetoothGatt gatt, log(Log.INFO, () -> "PHY read (TX: " + ParserUtils.phyToString(txPhy) + ", RX: " + ParserUtils.phyToString(rxPhy) + ")"); - if (request instanceof PhyRequest) { - ((PhyRequest) request).notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); + if (request instanceof final PhyRequest pr) { + pr.notifyPhyChanged(gatt.getDevice(), txPhy, rxPhy); request.notifySuccess(gatt.getDevice()); } } else { log(Log.WARN, () -> "PHY read failed with status " + status); - if (request instanceof PhyRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final PhyRequest pr) { + pr.notifyFail(gatt.getDevice(), status); } awaitingRequest = null; postCallback(c -> c.onError(gatt.getDevice(), ERROR_READ_PHY, status)); @@ -2925,14 +3001,14 @@ public void onReadRemoteRssi(@NonNull final BluetoothGatt gatt, final int status) { if (status == BluetoothGatt.GATT_SUCCESS) { log(Log.INFO, () -> "Remote RSSI received: " + rssi + " dBm"); - if (request instanceof ReadRssiRequest) { - ((ReadRssiRequest) request).notifyRssiRead(gatt.getDevice(), rssi); - request.notifySuccess(gatt.getDevice()); + if (request instanceof final ReadRssiRequest rrr) { + rrr.notifyRssiRead(gatt.getDevice(), rssi); + rrr.notifySuccess(gatt.getDevice()); } } else { log(Log.WARN, () -> "Reading remote RSSI failed with status " + status); - if (request instanceof ReadRssiRequest) { - request.notifyFail(gatt.getDevice(), status); + if (request instanceof final ReadRssiRequest rrr) { + rrr.notifyFail(gatt.getDevice(), status); } awaitingRequest = null; postCallback(c -> c.onError(gatt.getDevice(), ERROR_READ_RSSI, status)); @@ -3238,8 +3314,8 @@ final void onNotificationSent(@NonNull final BluetoothGattServer server, notifyNotificationSent(device); } else { Log.e(TAG, "onNotificationSent error " + status); - if (request instanceof WriteRequest) { - request.notifyFail(device, status); + if (request instanceof final WriteRequest wr) { + wr.notifyFail(device, status); } awaitingRequest = null; onError(device, ERROR_NOTIFY, status); @@ -3552,6 +3628,10 @@ private synchronized void nextRequest(final boolean force) { } } + // At this point the bluetoothDevice is either null, and the request is a ConnectRequest, + // or not a null. + assert bluetoothDevice != null || request.type == Request.Type.CONNECT; + switch (request.type) { case CONNECT: { //noinspection DataFlowIssue @@ -3562,7 +3642,10 @@ private synchronized void nextRequest(final boolean force) { break; } case DISCONNECT: { - result = internalDisconnect(ConnectionObserver.REASON_SUCCESS); + internalDisconnect(ConnectionObserver.REASON_SUCCESS); + // If a disconnect request failed, it has already been notified at this point, + // therefore result is a success (true). + result = true; break; } case ENSURE_BOND: { @@ -3814,8 +3897,7 @@ private synchronized void nextRequest(final boolean force) { awaitingRequest.notifyFail(bluetoothDevice, FailCallback.REASON_NULL_ATTRIBUTE); awaitingRequest = null; } - taskQueue.clear(); - initQueue = null; + emptyTasks(FailCallback.REASON_NULL_ATTRIBUTE); final BluetoothGatt bluetoothGatt = this.bluetoothGatt; if (connected && bluetoothGatt != null) { // Invalidate all services and characteristics diff --git a/ble/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java b/ble/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java index f4f19036..e151f4cc 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/ConnectionPriorityRequest.java @@ -25,6 +25,7 @@ import android.bluetooth.BluetoothDevice; import android.os.Build; import android.os.Handler; +import android.util.Log; import androidx.annotation.IntRange; import androidx.annotation.NonNull; @@ -191,8 +192,14 @@ void notifyConnectionPriorityChanged(@NonNull final BluetoothDevice device, @IntRange(from = 6, to = 3200) final int interval, @IntRange(from = 0, to = 499) final int latency, @IntRange(from = 10, to = 3200) final int timeout) { - if (valueCallback != null) - valueCallback.onConnectionUpdated(device, interval, latency, timeout); + handler.post(() -> { + if (valueCallback != null) + try { + valueCallback.onConnectionUpdated(device, interval, latency, timeout); + } catch (final Throwable t) { + Log.e(TAG, "Exception in Value callback", t); + } + }); } @ConnectionPriority diff --git a/ble/src/main/java/no/nordicsemi/android/ble/ReadRequest.java b/ble/src/main/java/no/nordicsemi/android/ble/ReadRequest.java index 1fd7d290..406cf6ce 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/ReadRequest.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/ReadRequest.java @@ -51,7 +51,7 @@ import no.nordicsemi.android.ble.exception.RequestFailedException; @SuppressWarnings({"unused", "WeakerAccess"}) -public final class ReadRequest extends SimpleValueRequest implements Operation { +public final class ReadRequest extends TimeoutableValueRequest implements Operation { private ReadProgressCallback progressCallback; private DataMerger dataMerger; private DataStream buffer; @@ -198,6 +198,8 @@ public ReadRequest merge(@NonNull final DataMerger merger, * @return The object with the response. * @throws RequestFailedException thrown when the BLE request finished with status other * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. * @throws IllegalStateException thrown when you try to call this method from the main * (UI) thread. * @throws IllegalArgumentException thrown when the response class could not be instantiated. @@ -214,7 +216,7 @@ public ReadRequest merge(@NonNull final DataMerger merger, @NonNull public E awaitValid(@NonNull final Class responseClass) throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, - BluetoothDisabledException, InvalidRequestException { + BluetoothDisabledException, InterruptedException, InvalidRequestException { final E response = await(responseClass); if (!response.isValid()) { throw new InvalidDataException(response); @@ -232,6 +234,8 @@ public E awaitValid(@NonNull final Class resp * @return The object with the response. * @throws RequestFailedException thrown when the BLE request finished with status other * than {@link BluetoothGatt#GATT_SUCCESS}. + * @throws InterruptedException thrown if the timeout occurred before the request has + * finished. * @throws IllegalStateException thrown when you try to call this method from the main * (UI) thread. * @throws DeviceDisconnectedException thrown when the device disconnected before the request @@ -247,7 +251,7 @@ public E awaitValid(@NonNull final Class resp @NonNull public E awaitValid(@NonNull final E response) throws RequestFailedException, InvalidDataException, DeviceDisconnectedException, - BluetoothDisabledException, InvalidRequestException { + BluetoothDisabledException, InterruptedException, InvalidRequestException { await(response); if (!response.isValid()) { throw new InvalidDataException(response); @@ -315,6 +319,6 @@ void notifyValueChanged(@NonNull final BluetoothDevice device, @Nullable final b @SuppressWarnings("BooleanMethodIsAlwaysInverted") boolean hasMore() { - return !complete; + return !complete && !cancelled && !finished; } } diff --git a/ble/src/main/java/no/nordicsemi/android/ble/Request.java b/ble/src/main/java/no/nordicsemi/android/ble/Request.java index a6f05dbf..a9da4d68 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/Request.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/Request.java @@ -100,8 +100,8 @@ enum Type { SLEEP, } - protected RequestHandler requestHandler; - protected CallbackHandler handler; + RequestHandler requestHandler; + CallbackHandler handler; final ConditionVariable syncLock; final Type type; diff --git a/ble/src/main/java/no/nordicsemi/android/ble/RequestQueue.java b/ble/src/main/java/no/nordicsemi/android/ble/RequestQueue.java index a5df46c7..50c4fdc4 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/RequestQueue.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/RequestQueue.java @@ -22,6 +22,7 @@ package no.nordicsemi.android.ble; +import android.bluetooth.BluetoothDevice; import android.os.Handler; import androidx.annotation.IntRange; @@ -171,6 +172,9 @@ public boolean isEmpty() { * Cancels all the enqueued operations that were not executed yet. * The one currently executed will be cancelled, if it is {@link TimeoutableRequest}. *

+ * Cancelled operations will NOT be notified with {@link FailCallback#REASON_CANCELLED}, + * they will be just removed from the queue. + *

* It is safe to call this method in {@link Request#done(SuccessCallback)} or * {@link Request#fail(FailCallback)} callback; */ @@ -210,4 +214,21 @@ boolean hasMore() { void cancelQueue() { requests.clear(); } + + /** + * Clears the queue, but first notifies all remaining tasks about cancellation. + * @param device the connected device. + */ + void notifyAndCancelQueue(final @NonNull BluetoothDevice device) { + for (final Request request : requests) { + request.notifyFail(device, FailCallback.REASON_CANCELLED); + } + cancelQueue(); + } + + @Override + void notifyFail(@NonNull BluetoothDevice device, int status) { + super.notifyFail(device, status); + notifyAndCancelQueue(device); + } } diff --git a/ble/src/main/java/no/nordicsemi/android/ble/WriteRequest.java b/ble/src/main/java/no/nordicsemi/android/ble/WriteRequest.java index d4f853d8..21a1d00d 100644 --- a/ble/src/main/java/no/nordicsemi/android/ble/WriteRequest.java +++ b/ble/src/main/java/no/nordicsemi/android/ble/WriteRequest.java @@ -47,7 +47,7 @@ import no.nordicsemi.android.ble.data.DefaultMtuSplitter; @SuppressWarnings({"unused", "WeakerAccess"}) -public final class WriteRequest extends SimpleValueRequest implements Operation { +public final class WriteRequest extends TimeoutableValueRequest implements Operation { private final static DataSplitter MTU_SPLITTER = new DefaultMtuSplitter(); private WriteProgressCallback progressCallback; @@ -310,7 +310,7 @@ boolean notifyPacketSent(@NonNull final BluetoothDevice device, @Nullable final * @return True if not all data were sent, false if the request is complete. */ boolean hasMore() { - return !complete; + return !complete && !cancelled && !finished; } /** diff --git a/build.gradle b/build.gradle index 1b8ba89c..c8c6e4aa 100644 --- a/build.gradle +++ b/build.gradle @@ -1,20 +1,22 @@ buildscript { // https://kotlinlang.org/docs/releases.html#release-details - ext.kotlin_version = '1.9.10' + ext.kotlin_version = '2.0.21' // https://plugins.gradle.org/plugin/io.github.gradle-nexus.publish-plugin - ext.gradle_nexus_publish_plugin = '1.3.0' + ext.gradle_nexus_publish_plugin = '2.0.0' repositories { google() mavenCentral() + gradlePluginPortal() maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath 'com.android.tools.build:gradle:8.1.2' + classpath 'com.android.tools.build:gradle:8.7.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - classpath "com.google.dagger:hilt-android-gradle-plugin:2.48" + classpath "org.jetbrains.kotlin:compose-compiler-gradle-plugin:$kotlin_version" + classpath "com.google.dagger:hilt-android-gradle-plugin:2.52" classpath "io.github.gradle-nexus:publish-plugin:$gradle_nexus_publish_plugin" - classpath "com.squareup.wire:wire-gradle-plugin:4.7.0" + classpath "com.squareup.wire:wire-gradle-plugin:5.1.0" } } @@ -26,8 +28,8 @@ allprojects { } } -task clean(type: Delete) { - delete rootProject.buildDir +tasks.register('clean', Delete) { + delete rootProject.layout.buildDirectory } // Maven Central publishing diff --git a/examples/ble-gatt-client/build.gradle b/examples/ble-gatt-client/build.gradle index 52a172c7..07d43d29 100644 --- a/examples/ble-gatt-client/build.gradle +++ b/examples/ble-gatt-client/build.gradle @@ -39,13 +39,13 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(':ble-ktx') testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } diff --git a/examples/ble-gatt-server/build.gradle b/examples/ble-gatt-server/build.gradle index 7a0f02ec..f43cb5be 100644 --- a/examples/ble-gatt-server/build.gradle +++ b/examples/ble-gatt-server/build.gradle @@ -39,13 +39,13 @@ android { } dependencies { - implementation 'androidx.core:core-ktx:1.12.0' - implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'androidx.core:core-ktx:1.13.1' + implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation project(':ble-ktx') testImplementation 'junit:junit:4.13.2' - androidTestImplementation 'androidx.test.ext:junit:1.1.5' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' + androidTestImplementation 'androidx.test.ext:junit:1.2.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1' } diff --git a/examples/ble-gatt-server/src/main/AndroidManifest.xml b/examples/ble-gatt-server/src/main/AndroidManifest.xml index 2b9ed8dd..e40b2a52 100644 --- a/examples/ble-gatt-server/src/main/AndroidManifest.xml +++ b/examples/ble-gatt-server/src/main/AndroidManifest.xml @@ -6,6 +6,8 @@ + diff --git a/examples/ble-gatt-server/src/main/java/no/nordicsemi/android/ble/ble_gatt_server/GattService.kt b/examples/ble-gatt-server/src/main/java/no/nordicsemi/android/ble/ble_gatt_server/GattService.kt index e6d3902d..b5c8585d 100644 --- a/examples/ble-gatt-server/src/main/java/no/nordicsemi/android/ble/ble_gatt_server/GattService.kt +++ b/examples/ble-gatt-server/src/main/java/no/nordicsemi/android/ble/ble_gatt_server/GattService.kt @@ -34,6 +34,7 @@ import android.os.Binder import android.os.IBinder import android.util.Log import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat import no.nordicsemi.android.ble.BleManager import no.nordicsemi.android.ble.BleServerManager import no.nordicsemi.android.ble.observer.ServerObserver @@ -111,7 +112,9 @@ class GattService : Service() { } } } - registerReceiver(bluetoothObserver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) + ContextCompat.registerReceiver(this, + bluetoothObserver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED), + ContextCompat.RECEIVER_EXPORTED) // Startup BLE if we have it diff --git a/examples/trivia/build.gradle b/examples/trivia/build.gradle index 9a78c0e4..752495ae 100644 --- a/examples/trivia/build.gradle +++ b/examples/trivia/build.gradle @@ -5,6 +5,7 @@ plugins { id 'kotlin-kapt' id 'dagger.hilt.android.plugin' id 'com.squareup.wire' + id 'org.jetbrains.kotlin.plugin.compose' } android { @@ -37,9 +38,6 @@ android { compose true buildConfig true } - composeOptions { - kotlinCompilerExtensionVersion '1.5.3' - } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' @@ -55,22 +53,23 @@ dependencies { implementation project(path: ':ble-ktx') // Dagger and Hilt - implementation 'com.google.dagger:hilt-android:2.48' - kapt 'com.google.dagger:hilt-compiler:2.48' - implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' - kapt 'androidx.hilt:hilt-compiler:1.0.0' + implementation 'com.google.dagger:hilt-android:2.52' + kapt 'com.google.dagger:hilt-compiler:2.52' + implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' + kapt 'androidx.hilt:hilt-compiler:1.2.0' // Nordic theme - implementation 'no.nordicsemi.android.common:theme:1.8.4' - implementation 'no.nordicsemi.android.common:permissions-ble:1.8.4' - implementation 'no.nordicsemi.android.common:navigation:1.8.4' + implementation 'no.nordicsemi.android.common:ui:2.1.0' + implementation 'no.nordicsemi.android.common:theme:2.1.0' + implementation 'no.nordicsemi.android.common:permissions-ble:2.1.0' + implementation 'no.nordicsemi.android.common:navigation:2.1.0' // Jetpack Compose bom - implementation platform('androidx.compose:compose-bom:2023.08.00') + implementation platform('androidx.compose:compose-bom:2024.09.03') // Text, color, Surface implementation "androidx.compose.material3:material3" - implementation 'androidx.activity:activity-compose:1.8.0' + implementation 'androidx.activity:activity-compose:1.9.2' implementation "androidx.compose.runtime:runtime-livedata" // To show Preview @@ -78,15 +77,15 @@ dependencies { implementation 'androidx.compose.ui:ui-tooling-preview' // Retrofit - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-moshi:2.9.0' - implementation 'com.squareup.okhttp3:okhttp:4.10.0' + implementation 'com.squareup.retrofit2:retrofit:2.11.0' + implementation 'com.squareup.retrofit2:converter-moshi:2.11.0' + implementation 'com.squareup.okhttp3:okhttp:4.12.0' // https://square.github.io/okhttp/changelog/ - implementation 'com.squareup.okhttp3:logging-interceptor:4.10.0' + implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0' // parse JSON - implementation'com.squareup.moshi:moshi-kotlin:1.14.0' + implementation 'com.squareup.moshi:moshi-kotlin:1.15.1' // Note: Switching to KSP doesn't work with hilt 2.47 and 2.48: // https://github.com/google/dagger/issues/3965 - kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.14.0' + kapt 'com.squareup.moshi:moshi-kotlin-codegen:1.15.1' } \ No newline at end of file diff --git a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/ClientScreen.kt b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/ClientScreen.kt index fb3a24d8..07455f00 100644 --- a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/ClientScreen.kt +++ b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/ClientScreen.kt @@ -3,6 +3,7 @@ package no.nordicsemi.android.ble.trivia.client import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier @@ -18,7 +19,7 @@ import no.nordicsemi.android.ble.trivia.server.view.ResultView import no.nordicsemi.android.ble.ktx.state.ConnectionState import no.nordicsemi.android.common.permissions.ble.RequireBluetooth import no.nordicsemi.android.common.permissions.ble.RequireLocation -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -28,9 +29,13 @@ fun ClientScreen( Column { var playersName by rememberSaveable { mutableStateOf("") } NordicAppBar( - text = when (playersName.isNotEmpty()) { - true -> stringResource(id = R.string.good_luck_player, playersName) - else -> stringResource(id = R.string.good_luck_player, "") + title = { + Text( + text = when (playersName.isNotEmpty()) { + true -> stringResource(id = R.string.good_luck_player, playersName) + else -> stringResource(id = R.string.good_luck_player, "") + } + ) }, onNavigationButtonClick = onNavigationUp ) diff --git a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/viewmodel/ClientViewModel.kt b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/viewmodel/ClientViewModel.kt index 8b7bf1fc..57d60a26 100644 --- a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/viewmodel/ClientViewModel.kt +++ b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/client/viewmodel/ClientViewModel.kt @@ -12,13 +12,12 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch -import no.nordicsemi.android.ble.ktx.state.ConnectionState +import no.nordicsemi.android.ble.ktx.stateAsFlow import no.nordicsemi.android.ble.trivia.client.data.ClientViewState import no.nordicsemi.android.ble.trivia.client.repository.ClientConnection import no.nordicsemi.android.ble.trivia.client.repository.ScannerRepository import no.nordicsemi.android.ble.trivia.server.viewmodel.Timer import no.nordicsemi.android.ble.trivia.server.viewmodel.TimerViewModel -import no.nordicsemi.android.ble.ktx.stateAsFlow import javax.inject.Inject @HiltViewModel @@ -47,7 +46,7 @@ class ClientViewModel @Inject constructor( error .onEach { _clientState.value = _clientState.value.copy(nameResult = it) } .launchIn(viewModelScope) - userJoined + userJoined .onEach { _clientState.value = _clientState.value.copy(userJoined = it) } .launchIn(viewModelScope) question diff --git a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/server/ServerScreen.kt b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/server/ServerScreen.kt index 63222bdb..552a71eb 100644 --- a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/server/ServerScreen.kt +++ b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/server/ServerScreen.kt @@ -3,6 +3,7 @@ package no.nordicsemi.android.ble.trivia.server import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier @@ -22,7 +23,7 @@ import no.nordicsemi.android.ble.trivia.server.view.StartGameView import no.nordicsemi.android.ble.trivia.server.view.WaitingForClientsView import no.nordicsemi.android.ble.trivia.server.viewmodel.ServerViewModel import no.nordicsemi.android.common.permissions.ble.RequireBluetooth -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -32,10 +33,15 @@ fun ServerScreen( Column { var playersName by rememberSaveable { mutableStateOf("") } NordicAppBar( - text = when (playersName.isNotEmpty()) { - true -> stringResource(id = R.string.good_luck_player, playersName) - else -> stringResource(id = R.string.good_luck_player, "") + title = { + Text( + text = when (playersName.isNotEmpty()) { + true -> stringResource(id = R.string.good_luck_player, playersName) + else -> stringResource(id = R.string.good_luck_player, "") + } + ) }, + onNavigationButtonClick = onNavigationUp ) diff --git a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/spec/PacketMerger.kt b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/spec/PacketMerger.kt index 392f768e..3f1b6fd2 100644 --- a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/spec/PacketMerger.kt +++ b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/spec/PacketMerger.kt @@ -6,37 +6,38 @@ import java.nio.ByteBuffer class PacketMerger : DataMerger { private var expectedSize = 0 + private var receivedDataSize = 0 /** - * A method that merges the last packet into the output message. All bytes from the lastPacket - * are simply copied to the output stream until null is returned. + * A method that merges the last packet into the output message. + * If its a fist packet, the first two bytes of the packet contain a header which includes the expected size + * of the message to be received. The remaining bytes of the packet contain the message, which is copied to the output stream. + * For all subsequent packets, the message bytes are simply copied to the output stream + * until the size of the packet received matches the expected size of the message delivered through header file. * * @param output the stream for the output message, initially empty. * @param lastPacket the data received in the last read/notify/indicate operation. * @param index an index of the packet, 0-based. - * @return True, if the message is complete, false if more data are expected. + * @return True, if the message is complete, false if more data are expected. */ override fun merge(output: DataStream, lastPacket: ByteArray?, index: Int): Boolean { if (lastPacket == null) return false + if (index == 0) { + receivedDataSize = 0 + } + val buffer = ByteBuffer.wrap(lastPacket) + receivedDataSize += buffer.remaining() if (index == 0) { expectedSize = buffer.short.toInt() } - - if (buffer.remaining() == expectedSize) { - ByteArray(expectedSize) - .apply { buffer.get(this) } - .also { output.write(it) } - .let { return true } - } else { - ByteArray(buffer.remaining()) - .apply { buffer.get(this) } - .also { output.write(it) } - .let { return false } - } + ByteArray(buffer.remaining()) + .apply { buffer.get(this) } + .also { output.write(it) } + return receivedDataSize - 2 >= expectedSize } } \ No newline at end of file diff --git a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/view/StartScreen.kt b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/view/StartScreen.kt index 364ed454..48dfe270 100644 --- a/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/view/StartScreen.kt +++ b/examples/trivia/src/main/java/no/nordicsemi/android/ble/trivia/view/StartScreen.kt @@ -10,7 +10,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import no.nordicsemi.android.ble.trivia.R -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -20,7 +20,7 @@ fun StartScreen( ) { Column { NordicAppBar( - text = stringResource(id = R.string.welcome_message) + title = { Text(text = stringResource(id = R.string.welcome_message)) }, ) Column( diff --git a/gradle/publish-module.gradle b/gradle/publish-module.gradle index 3f5fe29b..1c250aa8 100644 --- a/gradle/publish-module.gradle +++ b/gradle/publish-module.gradle @@ -5,7 +5,7 @@ apply from: rootProject.file("gradle/git-tag-version.gradle") group = GROUP version = getVersionNameFromTags() -task androidSourcesJar(type: Jar) { +tasks.register('androidSourcesJar', Jar) { from android.sourceSets.main.java.srcDirs } diff --git a/gradle/publish-root.gradle b/gradle/publish-root.gradle index e9730631..c6bc12e0 100644 --- a/gradle/publish-root.gradle +++ b/gradle/publish-root.gradle @@ -13,7 +13,7 @@ nexusPublishing { repositories { sonatype { - stagingProfileId = System.env.SONATYPE_STATING_PROFILE_ID + stagingProfileId = System.env.SONATYPE_STAGING_PROFILE_ID username = System.env.OSSR_USERNAME password = System.env.OSSR_PASSWORD } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 752a82bb..e0bbc7df 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Thu Apr 15 11:00:45 CEST 2021 +#Tue Aug 27 10:46:52 CEST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip diff --git a/test/build.gradle b/test/build.gradle index 7ff604e0..3fb5bd48 100644 --- a/test/build.gradle +++ b/test/build.gradle @@ -3,6 +3,7 @@ plugins { id 'org.jetbrains.kotlin.android' id 'kotlin-kapt' id 'dagger.hilt.android.plugin' + id 'org.jetbrains.kotlin.plugin.compose' } android { @@ -38,9 +39,6 @@ android { buildFeatures { compose true } - composeOptions { - kotlinCompilerExtensionVersion '1.5.3' - } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' @@ -51,29 +49,30 @@ android { dependencies { implementation project(path: ':ble-ktx') // Nordic theme - implementation "no.nordicsemi.android.common:theme:1.8.4" - implementation 'no.nordicsemi.android.common:permissions-ble:1.8.4' - implementation 'no.nordicsemi.android.common:navigation:1.8.4' + implementation "no.nordicsemi.android.common:ui:2.1.0" + implementation "no.nordicsemi.android.common:theme:2.1.0" + implementation 'no.nordicsemi.android.common:permissions-ble:2.1.0' + implementation 'no.nordicsemi.android.common:navigation:2.1.0' - implementation 'com.github.jeziellago:compose-markdown:0.3.6' + implementation 'com.github.jeziellago:compose-markdown:0.4.1' // Jetpack Compose bom - implementation platform('androidx.compose:compose-bom:2023.08.00') + implementation platform('androidx.compose:compose-bom:2024.09.03') // Text, Color, Surface implementation 'androidx.compose.material3:material3' - implementation 'androidx.activity:activity-compose:1.8.0' - implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.2" + implementation 'androidx.activity:activity-compose:1.9.2' + implementation 'androidx.lifecycle:lifecycle-runtime-compose:2.8.6' // Preview debugImplementation "androidx.compose.ui:ui-tooling" implementation "androidx.compose.ui:ui-tooling-preview" // Dagger and Hilt - implementation 'com.google.dagger:hilt-android:2.48' - kapt 'com.google.dagger:hilt-compiler:2.48' - implementation 'androidx.hilt:hilt-navigation-compose:1.0.0' - kapt 'androidx.hilt:hilt-compiler:1.0.0' + implementation 'com.google.dagger:hilt-android:2.52' + kapt 'com.google.dagger:hilt-compiler:2.52' + implementation 'androidx.hilt:hilt-navigation-compose:1.2.0' + kapt 'androidx.hilt:hilt-compiler:1.2.0' // Test testImplementation 'junit:junit:4.13.2' diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/HomeScreen.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/HomeScreen.kt index c773a641..2872d60b 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/HomeScreen.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/HomeScreen.kt @@ -9,7 +9,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -18,7 +18,9 @@ fun HomeScreen( onScanNavigation: () -> Unit, ) { Column { - NordicAppBar(text = stringResource(id = R.string.welcome_message)) + NordicAppBar( + title = { Text(text = stringResource(id = R.string.welcome_message)) } + ) Column( modifier = Modifier .padding(16.dp) diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ClientConnection.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ClientConnection.kt index e12e2f41..b4914ab8 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ClientConnection.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ClientConnection.kt @@ -89,11 +89,11 @@ class ClientConnection @Inject constructor( * Two requests [BleManager.setWriteCallback], [BleManager.readCharacteristic] have been added and * when both requests are enqueued successfully, reliable write process will start automatically. */ - fun testReliableWrite(reliableRequest: ByteArray) { + suspend fun testReliableWrite(reliableRequest: ByteArray) { beginReliableWrite() .add(writeCharacteristic(reliableCharacteristics, reliableRequest, WRITE_TYPE_DEFAULT)) .add(readCharacteristic(readCharacteristics)) - .enqueue() + .suspend() } /** @@ -125,30 +125,29 @@ class ClientConnection @Inject constructor( /** * Wait for Notification [BleManager.waitForNotification]. It waits until the notification is sent - * from the remote device. Once notification is received, then [WaitForReadRequest.then] it will - * disable notification [BleManager.disableNotifications] request for the given characteristics. + * from the remote device. */ fun testWaitForNotification(): WaitForValueChangedRequest { return waitForNotification(characteristic) - .then { disableNotifications(characteristic)} } /** * Sends the read request to the given characteristic [BleManager.readCharacteristic]. */ - fun testReadCharacteristics(): ReadRequest { + fun testReadCharacteristics(): ReadRequest { return readCharacteristic(readCharacteristics) } /** - * It initiates the atomic request queue [BleManager.beginAtomicRequestQueue] and it will execute the requests in the queue in the order. + * It initiates the atomic request queue [BleManager.beginAtomicRequestQueue] and it will execute + * the requests in the queue in the order. * The method has two requests and they will execute together. The method is particularly useful * when the user wants to execute multiple requests simultaneously and ensure they are executed together. */ fun testBeginAtomicRequestQueue(request: ByteArray): RequestQueue { return beginAtomicRequestQueue() + .add(writeCharacteristic(characteristic, request, WRITE_TYPE_DEFAULT)) .add(readCharacteristic(readCharacteristics)) - .before { writeCharacteristic(characteristic, request, WRITE_TYPE_DEFAULT) } } /** diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ScanningManager.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ScanningManager.kt index 6d53c07e..28d1fa5e 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ScanningManager.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/repository/ScanningManager.kt @@ -28,10 +28,15 @@ class ScanningManager @Inject constructor( suspend fun scanningForServer(): BluetoothDevice = suspendCancellableCoroutine { continuation -> val callback = object : ScanCallback() { + var found = false override fun onScanResult(callbackType: Int, result: ScanResult?) { + if (found) return result - ?.let { continuation.resume(it.device) {} } + ?.let { + found = true + continuation.resume(it.device) {} + } .also { bluetoothLeScanner.stopScan(this) } } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/task/ClientTaskPerformer.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/task/ClientTaskPerformer.kt index 15897665..e7927b96 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/task/ClientTaskPerformer.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/task/ClientTaskPerformer.kt @@ -18,9 +18,9 @@ class ClientTaskPerformer @Inject constructor( tasks.forEach { try { it.start() - _testCases.value = _testCases.value + listOf(TestCase(it.taskName(), true)) + _testCases.value += listOf(TestCase(it.taskName(), true)) } catch (e: Exception) { - _testCases.value = _testCases.value + listOf(TestCase(it.taskName(), false)) + _testCases.value += listOf(TestCase(it.taskName(), false)) } } } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestBeginAtomicRequestQueue.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestBeginAtomicRequestQueue.kt index 8dff50df..efeaa276 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestBeginAtomicRequestQueue.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestBeginAtomicRequestQueue.kt @@ -6,17 +6,19 @@ import no.nordicsemi.andorid.ble.test.spec.Callbacks.ATOMIC_REQUEST_QUEUE import no.nordicsemi.andorid.ble.test.spec.Requests.atomicRequestQueue import no.nordicsemi.android.ble.ktx.suspend +/** + * Begins an atomic request queue. + */ class TestBeginAtomicRequestQueue( private val clientConnection: ClientConnection ) : TaskManager { - // Start the task override suspend fun start() { - clientConnection.testBeginAtomicRequestQueue(atomicRequestQueue) + clientConnection + .testBeginAtomicRequestQueue(atomicRequestQueue) .suspend() } - // Return task name override fun taskName(): String { return ATOMIC_REQUEST_QUEUE } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestIndication.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestIndication.kt index f2839848..497ca6be 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestIndication.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestIndication.kt @@ -7,21 +7,24 @@ import no.nordicsemi.andorid.ble.test.spec.Callbacks.ENABLE_INDICATION import no.nordicsemi.andorid.ble.test.spec.FlagBasedPacketMerger import no.nordicsemi.android.ble.ktx.suspend +/** + * Waits until an indication is received and [FlagBasedPacketMerger] + * efficiently merges and processes the data received from the remote device. + */ class TestIndication( private val clientConnection: ClientConnection ) : TaskManager { - /** - * Enables Indication and waits until indication response is received and [FlagBasedPacketMerger] to - * efficiently merge and process the data received from the remote device. - */ + override suspend fun start() { - clientConnection.testEnableIndication().suspend() - clientConnection.testWaitForIndication() + clientConnection + .testWaitForIndication() .merge(FlagBasedPacketMerger()) + .trigger( + clientConnection.testEnableIndication() + ) .suspend() } - // Return task name override fun taskName(): String { val indications = listOf( ENABLE_INDICATION, diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestNotification.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestNotification.kt index bab6752e..45a2d04e 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestNotification.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestNotification.kt @@ -6,21 +6,24 @@ import no.nordicsemi.andorid.ble.test.spec.Callbacks import no.nordicsemi.andorid.ble.test.spec.HeaderBasedPacketMerger import no.nordicsemi.android.ble.ktx.suspend +/** + * Waits until a notification is received and [HeaderBasedPacketMerger] + * efficiently merges and processes the data received from the remote device. + */ class TestNotification( private val clientConnection: ClientConnection ) : TaskManager { - /** - * Enable Notification and waits until notification response is received and [HeaderBasedPacketMerger] to - * efficiently merge and process the data received from the remote device. - */ + override suspend fun start() { - clientConnection.testEnableNotification().suspend() - clientConnection.testWaitForNotification() + clientConnection + .testWaitForNotification() .merge(HeaderBasedPacketMerger()) + .trigger( + clientConnection.testEnableNotification() + ) .suspend() } - // Return task name override fun taskName(): String { val indications = listOf( Callbacks.ENABLE_NOTIFICATION, diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReadCharacteristics.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReadCharacteristics.kt index bcaae40a..d2d5a8e1 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReadCharacteristics.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReadCharacteristics.kt @@ -5,15 +5,19 @@ import no.nordicsemi.andorid.ble.test.server.tasks.TaskManager import no.nordicsemi.andorid.ble.test.spec.Callbacks.READ_CHA import no.nordicsemi.android.ble.ktx.suspend +/** + * Reads the characteristics from the remote device. + */ class TestReadCharacteristics( private val clientConnection: ClientConnection ) : TaskManager { - // Start the task + override suspend fun start() { - clientConnection.testReadCharacteristics().suspend() + clientConnection + .testReadCharacteristics() + .suspend() } - // Return task name override fun taskName(): String { return READ_CHA } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReliableWrite.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReliableWrite.kt index bba447c0..12699d93 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReliableWrite.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestReliableWrite.kt @@ -5,15 +5,18 @@ import no.nordicsemi.andorid.ble.test.server.tasks.TaskManager import no.nordicsemi.andorid.ble.test.spec.Callbacks.RELIABLE_WRITE import no.nordicsemi.andorid.ble.test.spec.Requests +/** + * Writes and reads a characteristic value to the remote device using reliable write. + */ class TestReliableWrite( private val clientConnection: ClientConnection ) : TaskManager { - // Start the task + override suspend fun start() { - clientConnection.testReliableWrite(Requests.reliableRequest) + clientConnection + .testReliableWrite(Requests.reliableRequest) } - // Return task name override fun taskName(): String { return RELIABLE_WRITE } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWrite.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWrite.kt index 48ec7571..54072f92 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWrite.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWrite.kt @@ -6,16 +6,19 @@ import no.nordicsemi.andorid.ble.test.spec.Callbacks.WRITE_CHARACTERISTICS import no.nordicsemi.andorid.ble.test.spec.Requests.writeRequest import no.nordicsemi.android.ble.ktx.suspend +/** + * Writes the characteristics to the remote device. + */ class TestWrite( private val clientConnection: ClientConnection ) : TaskManager { - // Start the task + override suspend fun start() { - clientConnection.testWrite(writeRequest) + clientConnection + .testWrite(writeRequest) .suspend() } - // Return task name override fun taskName(): String { return WRITE_CHARACTERISTICS } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithDefaultSplitter.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithDefaultSplitter.kt index 48599fed..4bc9bdba 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithDefaultSplitter.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithDefaultSplitter.kt @@ -7,22 +7,22 @@ import no.nordicsemi.andorid.ble.test.spec.Requests.splitterRequest import no.nordicsemi.android.ble.WriteRequest import no.nordicsemi.android.ble.ktx.suspend +/** + * Writes the request data to the given characteristics. It utilizes the [WriteRequest.split] callback + * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. + */ class TestWriteWithDefaultSplitter( private val clientConnection: ClientConnection ) : TaskManager { - /** - * Writes the request data to the given characteristics. It utilizes the [WriteRequest.split] callback - * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. - */ override suspend fun start() { val requestToSend = clientConnection.checkSizeOfRequest(splitterRequest) - clientConnection.testWrite(requestToSend) + clientConnection + .testWrite(requestToSend) .split() .suspend() } - // Return task name override fun taskName(): String { return DEFAULT_MTU_SPLITTER } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithFlagBasedSplitter.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithFlagBasedSplitter.kt index db206c6b..235ceebc 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithFlagBasedSplitter.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithFlagBasedSplitter.kt @@ -8,15 +8,18 @@ import no.nordicsemi.andorid.ble.test.spec.Requests.splitterRequest import no.nordicsemi.android.ble.WriteRequest import no.nordicsemi.android.ble.ktx.suspend +/** + * Writes the request data to the given characteristics. + * It utilizes the [WriteRequest.split] callback with [FlagBasedPacketSplitter] + * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. + */ class TestWriteWithFlagBasedSplitter( private val clientConnection: ClientConnection ) : TaskManager { - /** - * Writes the request data to the given characteristics. It utilizes the [WriteRequest.split] callback with [FlagBasedPacketSplitter] - * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. - */ + override suspend fun start() { - clientConnection.testWrite(splitterRequest) + clientConnection + .testWrite(splitterRequest) .split(FlagBasedPacketSplitter()) .suspend() } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithHeaderBasedSplitter.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithHeaderBasedSplitter.kt index 849e472d..4b8ab202 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithHeaderBasedSplitter.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/tests/TestWriteWithHeaderBasedSplitter.kt @@ -10,21 +10,20 @@ import no.nordicsemi.android.ble.WriteRequest import no.nordicsemi.android.ble.callback.WriteProgressCallback import no.nordicsemi.android.ble.ktx.suspend +/** + * Writes the request data to the given characteristics. It utilizes the [WriteRequest.split] callback with [HeaderBasedPacketSplitter] + * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. The [WriteProgressCallback] is used to observe the + * packet on each time a packet has been sent. + */ class TestWriteWithHeaderBasedSplitter( private val clientConnection: ClientConnection ) : TaskManager { private val TAG = "WriteProgressCallback" - /** - * Writes the request data to the given characteristics. It utilizes the [WriteRequest.split] callback with [HeaderBasedPacketSplitter] - * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. The [WriteProgressCallback] is used to observe the - * packet on each time a packet has been sent. - */ override suspend fun start() { - clientConnection.testWrite(splitterRequest) - .split( - HeaderBasedPacketSplitter() - ) { _, data, index -> + clientConnection + .testWrite(splitterRequest) + .split(HeaderBasedPacketSplitter()) { _, data, index -> Log.i( TAG, "onPacketSent: Packet size ${data?.size} and index $index " diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/view/ClientScreen.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/view/ClientScreen.kt index 3f69fdd1..42258313 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/view/ClientScreen.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/view/ClientScreen.kt @@ -2,6 +2,7 @@ package no.nordicsemi.andorid.ble.test.client.view import androidx.compose.foundation.layout.Column import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource @@ -14,13 +15,15 @@ import no.nordicsemi.andorid.ble.test.server.view.ResultView import no.nordicsemi.android.ble.ktx.state.ConnectionState import no.nordicsemi.android.common.permissions.ble.RequireBluetooth import no.nordicsemi.android.common.permissions.ble.RequireLocation -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable fun ClientScreen() { Column { - NordicAppBar(text = stringResource(id = R.string.scanner)) + NordicAppBar( + title = { Text(text = stringResource(id = R.string.scanner)) } + ) RequireBluetooth { RequireLocation { val clientViewModel: ClientViewModel = hiltViewModel() diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/viewmodel/ClientViewModel.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/viewmodel/ClientViewModel.kt index 0935d427..367bb160 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/client/viewmodel/ClientViewModel.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/client/viewmodel/ClientViewModel.kt @@ -51,12 +51,12 @@ class ClientViewModel @Inject constructor( .apply { connectDevice(device) // Start testing tasks after client connection - clientTaskPerformer.startTasks() clientTaskPerformer.testCases .onEach { it.forEach { tc -> updateTestList(tc) } } .launchIn(viewModelScope) + clientTaskPerformer.startTasks() } } } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/repository/ServerConnection.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/repository/ServerConnection.kt index 36433c62..2facfcc4 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/repository/ServerConnection.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/repository/ServerConnection.kt @@ -105,14 +105,15 @@ class ServerConnection @Inject constructor( */ fun testWriteCallback(): ValueChangedCallback { return setWriteCallback(serverCharacteristics) + .with { _, data -> Log.i(TAG, "Data writtenL: $data") } } /** * Waits until the Indication is enabled by the remote device [BleManager.waitUntilIndicationsEnabled]. */ - fun testWaiUntilIndicationEnabled(readRequestInTrigger: ByteArray) { - waitUntilIndicationsEnabled(indicationCharacteristics).enqueue() - setCharacteristicValue(readCharacteristics, readRequestInTrigger).enqueue() + suspend fun testWaiUntilIndicationEnabled(readRequestInTrigger: ByteArray) { + waitUntilIndicationsEnabled(indicationCharacteristics).suspend() + setCharacteristicValue(readCharacteristics, readRequestInTrigger).suspend() } /** @@ -130,13 +131,15 @@ class ServerConnection @Inject constructor( */ suspend fun testWaitNotificationEnabled(request: ByteArray) { waitUntilNotificationsEnabled(serverCharacteristics) - .then { - sendNotification(serverCharacteristics, request) - .split(HeaderBasedPacketSplitter()) - .done {scope.launch { _testCase.emit(TestCase(SEND_NOTIFICATION, true)) } } - .fail { _, _ -> - scope.launch {_testCase.emit(TestCase(SEND_NOTIFICATION,false))}} - .enqueue() + .suspend() + + sendNotification(serverCharacteristics, request) + .split(HeaderBasedPacketSplitter()) + .done { + scope.launch { _testCase.emit(TestCase(SEND_NOTIFICATION, true)) } + } + .fail { _, _ -> + scope.launch {_testCase.emit(TestCase(SEND_NOTIFICATION,false))} } .suspend() } @@ -151,24 +154,25 @@ class ServerConnection @Inject constructor( /** * Handles values changes in the [BleManager.beginReliableWrite] procedure initiated by the remote device. */ - fun testReliableWriteCallback(secondReliableRequest: ByteArray) { - setWriteCallback(reliableCharacteristics) - waitForRead(readCharacteristics, secondReliableRequest).enqueue() + suspend fun testReliableWriteCallback(secondReliableRequest: ByteArray) { + waitForWrite(reliableCharacteristics).suspend() + waitForRead(readCharacteristics, secondReliableRequest).suspend() } /** - * Facilitates the transfer of data by setting the specified data to the readable characteristic using [BleManager.setCharacteristicValue]. + * Facilitates the transfer of data by setting the specified data to the readable + * characteristic using [BleManager.setCharacteristicValue]. */ - fun testSetReadCharacteristics(request: ByteArray) { - setCharacteristicValue(readCharacteristics, request).enqueue() + suspend fun testSetReadCharacteristics(request: ByteArray) { + setCharacteristicValue(readCharacteristics, request).suspend() } /** * Handles values changes in the [BleManager.beginAtomicRequestQueue] procedure initiated by the remote device. */ - fun testBeginAtomicRequestQueue(atomicRequest: ByteArray) { - waitForWrite(serverCharacteristics).enqueue() - waitForRead(readCharacteristics, atomicRequest).enqueue() + suspend fun testBeginAtomicRequestQueue(atomicRequest: ByteArray) { + waitForWrite(serverCharacteristics).suspend() + waitForRead(readCharacteristics, atomicRequest).suspend() } /** diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/ServerTaskPerformer.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/ServerTaskPerformer.kt index 3b1f9496..c211be71 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/ServerTaskPerformer.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/ServerTaskPerformer.kt @@ -18,9 +18,9 @@ class ServerTaskPerformer @Inject constructor( tasks.forEach { try { it.start() - _testCases.value = _testCases.value + listOf(TestCase(it.taskName(), true)) + _testCases.value += listOf(TestCase(it.taskName(), true)) } catch (e: Exception) { - _testCases.value = _testCases.value + listOf(TestCase(it.taskName(), false)) + _testCases.value += listOf(TestCase(it.taskName(), false)) } } } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/TaskManager.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/TaskManager.kt index b53144c9..3d592cde 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/TaskManager.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tasks/TaskManager.kt @@ -1,6 +1,13 @@ package no.nordicsemi.andorid.ble.test.server.tasks interface TaskManager { + /** + * Starts the task. + */ suspend fun start() + + /** + * Returns the name of the task. + */ fun taskName(): String } \ No newline at end of file diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestBeginAtomicRequestQueue.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestBeginAtomicRequestQueue.kt index 802c0f19..4802e8b9 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestBeginAtomicRequestQueue.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestBeginAtomicRequestQueue.kt @@ -5,12 +5,16 @@ import no.nordicsemi.andorid.ble.test.server.repository.ServerConnection import no.nordicsemi.andorid.ble.test.spec.Callbacks.ATOMIC_REQUEST_QUEUE import no.nordicsemi.andorid.ble.test.spec.Requests.atomicRequest +/** + * Begins an atomic request queue. + */ class TestBeginAtomicRequestQueue( private val serverConnection: ServerConnection, ) : TaskManager { - // Start the task + override suspend fun start() { - serverConnection.testBeginAtomicRequestQueue(atomicRequest) + serverConnection + .testBeginAtomicRequestQueue(atomicRequest) } override fun taskName(): String { diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestIndications.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestIndications.kt index 2f2b58e7..cb890b83 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestIndications.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestIndications.kt @@ -9,16 +9,19 @@ import no.nordicsemi.andorid.ble.test.spec.Requests import no.nordicsemi.android.ble.WriteRequest import no.nordicsemi.android.ble.ktx.suspend +/** + * Waits until the indication is enabled and sends an Indication response. It utilizes the [WriteRequest.split] callback + * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. + */ class TestIndications( private val serverConnection: ServerConnection, ) : TaskManager { - /** - * Waits until the indication is enabled and sends an Indication response. It utilizes the [WriteRequest.split] callback - * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. - */ + override suspend fun start() { - serverConnection.testWaiUntilIndicationEnabled(Requests.readRequestInTrigger) - serverConnection.testSendIndication(Requests.indicationRequest) + serverConnection + .testWaiUntilIndicationEnabled(Requests.readRequestInTrigger) + serverConnection + .testSendIndication(Requests.indicationRequest) .split(FlagBasedPacketSplitter()) .suspend() } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestNotification.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestNotification.kt index 36b454a0..633b1380 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestNotification.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestNotification.kt @@ -8,19 +8,23 @@ import no.nordicsemi.andorid.ble.test.spec.HeaderBasedPacketSplitter import no.nordicsemi.andorid.ble.test.spec.Requests import no.nordicsemi.andorid.ble.test.spec.Requests.sendNotificationInThenCallback import no.nordicsemi.android.ble.WriteRequest +import no.nordicsemi.android.ble.ktx.suspend +/** + * Waits until the notification is enabled and sends a Notification response. It utilizes the [WriteRequest.split] callback + * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. + */ class TestNotification( private val serverConnection: ServerConnection, ) : TaskManager { - /** - * Waits until the notification is enabled and sends a Notification response. It utilizes the [WriteRequest.split] callback - * to chunk the data into multiple packets, if the data cannot be sent in a single write operation. - */ + override suspend fun start() { - serverConnection.testWaitNotificationEnabled(sendNotificationInThenCallback) - serverConnection.testSendNotification(Requests.notificationRequest) + serverConnection + .testWaitNotificationEnabled(sendNotificationInThenCallback) + serverConnection + .testSendNotification(Requests.notificationRequest) .split(HeaderBasedPacketSplitter()) - .enqueue() + .suspend() } override fun taskName(): String { diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestReliableWrite.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestReliableWrite.kt index 86b7c867..83c44735 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestReliableWrite.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestReliableWrite.kt @@ -5,12 +5,16 @@ import no.nordicsemi.andorid.ble.test.server.repository.ServerConnection import no.nordicsemi.andorid.ble.test.spec.Callbacks.RELIABLE_WRITE import no.nordicsemi.andorid.ble.test.spec.Requests +/** + * Awaits write and read operations from the remote device using reliable write. + */ class TestReliableWrite( private val serverConnection: ServerConnection, ) : TaskManager { - // Start the task + override suspend fun start() { - serverConnection.testReliableWriteCallback(Requests.secondReliableRequest) + serverConnection + .testReliableWriteCallback(Requests.secondReliableRequest) } override fun taskName(): String { diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetReadCharacteristics.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetReadCharacteristics.kt index 7706b8f7..f1f34031 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetReadCharacteristics.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetReadCharacteristics.kt @@ -2,18 +2,21 @@ package no.nordicsemi.andorid.ble.test.server.tests import no.nordicsemi.andorid.ble.test.server.tasks.TaskManager import no.nordicsemi.andorid.ble.test.server.repository.ServerConnection -import no.nordicsemi.andorid.ble.test.spec.Callbacks.READ_CHARACTERISTICS +import no.nordicsemi.andorid.ble.test.spec.Callbacks.SET_CHARACTERISTIC_VALUE import no.nordicsemi.andorid.ble.test.spec.Requests.readRequest +/** + * Reads the characteristics from the remote device. + */ class TestSetReadCharacteristics( private val serverConnection: ServerConnection, ) : TaskManager { - // Start the task + override suspend fun start() { serverConnection.testSetReadCharacteristics(readRequest) } override fun taskName(): String { - return READ_CHARACTERISTICS + return SET_CHARACTERISTIC_VALUE } } \ No newline at end of file diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetWriteCallback.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetWriteCallback.kt index 7aa3b009..26124059 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetWriteCallback.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestSetWriteCallback.kt @@ -4,10 +4,13 @@ import no.nordicsemi.andorid.ble.test.server.tasks.TaskManager import no.nordicsemi.andorid.ble.test.server.repository.ServerConnection import no.nordicsemi.andorid.ble.test.spec.Callbacks.WRITE_CALLBACK +/** + * Tests the write callback. + */ class TestSetWriteCallback( private val serverConnection: ServerConnection, ) : TaskManager { - // Start the task + override suspend fun start() { serverConnection.testWriteCallback() } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithFlagMerger.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithFlagMerger.kt index 9b07c08c..205d33cf 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithFlagMerger.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithFlagMerger.kt @@ -11,16 +11,17 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +/** + * Observes the value changes on the give characteristics and [FlagBasedPacketMerger] to + * efficiently merge and process the data sent from the remote device. + * It also utilizes the [ValueChangedCallback.with] to monitor the size of the data and log + * respective messages accordingly. + */ class TestWriteWithFlagMerger( private val serverConnection: ServerConnection, ) : TaskManager { private val TAG = "WithCallBack" - /** - * Observes the value changes on the give characteristics and [FlagBasedPacketMerger] to - * efficiently merge and process the data sent from the remote device. - * It also utilizes the [ValueChangedCallback.with] to monitor the size of the data and log respective messages accordingly. - */ override suspend fun start() = suspendCoroutine { continuation -> serverConnection.testWriteCallback() .merge(FlagBasedPacketMerger()) diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithHeaderMerger.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithHeaderMerger.kt index cacb9562..6a01d6c7 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithHeaderMerger.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithHeaderMerger.kt @@ -9,14 +9,14 @@ import kotlin.coroutines.resume import kotlin.coroutines.resumeWithException import kotlin.coroutines.suspendCoroutine +/** + * Observe the data written to the given characteristics and [HeaderBasedPacketMerger] to + * efficiently merge and process the data received from the remote device. + */ class TestWriteWithHeaderMerger( private val serverConnection: ServerConnection, ) : TaskManager { - /** - * Observe the data written to the given characteristics and [HeaderBasedPacketMerger] to - * efficiently merge and process the data received from the remote device. - */ override suspend fun start() = suspendCoroutine { continuation -> serverConnection.testWriteCallback() .merge(HeaderBasedPacketMerger()) diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithMtuMerger.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithMtuMerger.kt index c6b2759f..adc48c77 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithMtuMerger.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/tests/TestWriteWithMtuMerger.kt @@ -6,15 +6,15 @@ import no.nordicsemi.andorid.ble.test.spec.Flags.MTU_SIZE_MERGER import no.nordicsemi.andorid.ble.test.spec.MtuBasedMerger import no.nordicsemi.android.ble.ValueChangedCallback +/** + * Combines the packets received using [MtuBasedMerger]. + * The [ValueChangedCallback.filterPacket] is utilized to pre-screen packets before merging, discarding any that do not meet the necessary criteria. + * Additionally, the [ValueChangedCallback.filter] is employed to further refine the data after merging, discarding any that do not meet the specified requirements. + */ class TestWriteWithMtuMerger( private val serverConnection: ServerConnection, ) : TaskManager { - /** - * Combines the packets received using [MtuBasedMerger]. - * The [ValueChangedCallback.filterPacket] is utilized to pre-screen packets before merging, discarding any that do not meet the necessary criteria. - * Additionally, the [ValueChangedCallback.filter] is employed to further refine the data after merging, discarding any that do not meet the specified requirements. - */ override suspend fun start() { serverConnection.testWriteCallback() .filterPacket { data -> data != null && data.size > 2 } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/view/ServerScreen.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/view/ServerScreen.kt index 18f4700e..38afb8e9 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/view/ServerScreen.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/view/ServerScreen.kt @@ -2,6 +2,7 @@ package no.nordicsemi.andorid.ble.test.server.view import androidx.compose.foundation.layout.Column import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.ui.res.stringResource @@ -11,13 +12,15 @@ import no.nordicsemi.andorid.ble.test.R import no.nordicsemi.andorid.ble.test.server.viewmodel.ServerViewModel import no.nordicsemi.andorid.ble.test.server.viewmodel.WaitingForClient import no.nordicsemi.android.common.permissions.ble.RequireBluetooth -import no.nordicsemi.android.common.theme.view.NordicAppBar +import no.nordicsemi.android.common.ui.view.NordicAppBar @OptIn(ExperimentalMaterial3Api::class) @Composable fun ServerScreen() { Column { - NordicAppBar(text = stringResource(id = R.string.advertiser)) + NordicAppBar( + title = { Text(text = stringResource(id = R.string.advertiser)) } + ) RequireBluetooth { val serverViewModel: ServerViewModel = hiltViewModel() val serverViewState by serverViewModel.serverViewState.collectAsStateWithLifecycle() diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/viewmodel/ServerViewModel.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/viewmodel/ServerViewModel.kt index 737f1703..dd3b2ce5 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/server/viewmodel/ServerViewModel.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/server/viewmodel/ServerViewModel.kt @@ -66,12 +66,12 @@ class ServerViewModel @Inject constructor( connectDevice(device) stopAdvertising() // Start the testing tasks after server connection - serverTaskPerformer.startTasks() serverTaskPerformer.testCases .onEach { it.forEach { tc -> updateTestList(tc) } } .launchIn(viewModelScope) + serverTaskPerformer.startTasks() } } diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/Constants.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/Constants.kt index 93006593..f42f305e 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/Constants.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/Constants.kt @@ -41,23 +41,21 @@ object Flags { } object Callbacks { - const val WRITE_CHARACTERISTICS = "Write Characteristics" - const val RELIABLE_WRITE = "Begin Reliable Write" - const val ATOMIC_REQUEST_QUEUE = "Begin Atomic Request Queue" - const val WRITE_CALLBACK = "Write Callback" + const val WRITE_CHARACTERISTICS = "Writing Characteristic" + const val RELIABLE_WRITE = "Reliable Write" + const val ATOMIC_REQUEST_QUEUE = "Atomic Request Queue" + const val WRITE_CALLBACK = "Setting Write callback" + const val ENABLE_INDICATION = "Enabling Indications" + const val WAIT_FOR_INDICATION_CALLBACK = "Waiting for Indication" + const val WAIT_UNTIL_INDICATION_ENABLED = "Waiting until Indications enabled" + const val SEND_INDICATION = "Sending indication" + const val ENABLE_NOTIFICATION = "Enabling Notifications" + const val WAIT_FOR_NOTIFICATION_CALLBACK = "Waiting for notification" + const val WAIT_UNTIL_NOTIFICATION_ENABLED = "Waiting until Notifications enabled" + const val SEND_NOTIFICATION = "Sending Notifications" - const val ENABLE_INDICATION = "Enable Indication" - const val WAIT_FOR_INDICATION_CALLBACK = "Wait for Indication" - const val WAIT_UNTIL_INDICATION_ENABLED = "Wait Until Indication Enabled" - const val SEND_INDICATION = "Send Indication" - - const val ENABLE_NOTIFICATION = "Enable Notification" - const val WAIT_FOR_NOTIFICATION_CALLBACK = "Wait for Notification" - const val WAIT_UNTIL_NOTIFICATION_ENABLED = "Wait Until Notification Enabled" - const val SEND_NOTIFICATION = "Send Notification" - - const val READ_CHA = "Set Characteristics for read operation" - const val READ_CHARACTERISTICS = "Read Characteristics" + const val READ_CHA = "Reading Characteristic" + const val SET_CHARACTERISTIC_VALUE = "Setting Characteristic value" } object Connections { diff --git a/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/HeaderBasedPacketMerger.kt b/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/HeaderBasedPacketMerger.kt index f74272b0..cf210026 100644 --- a/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/HeaderBasedPacketMerger.kt +++ b/test/src/main/java/no/nordicsemi/andorid/ble/test/spec/HeaderBasedPacketMerger.kt @@ -24,23 +24,20 @@ class HeaderBasedPacketMerger: DataMerger { if (lastPacket == null) return false + if (index == 0) { + receivedDataSize = 0 + } + val buffer = ByteBuffer.wrap(lastPacket) receivedDataSize += buffer.remaining() if (index == 0) { expectedSize = buffer.short.toInt() } - if ((receivedDataSize-2) == expectedSize) { - ByteArray(buffer.remaining()) - .apply { buffer.get(this) } - .also { output.write(it) } - .let { return true } - } else { - ByteArray(buffer.remaining()) - .apply { buffer.get(this) } - .also { output.write(it) } - .let { return false } - } + ByteArray(buffer.remaining()) + .apply { buffer.get(this) } + .also { output.write(it) } + return receivedDataSize - 2 >= expectedSize } } \ No newline at end of file