Change BluetoothLe.advertise
Instead of returning a flow, it accepts a block to run
and it completes when the advertising ends.
Bug: 297952535
Relnote: N/A
Test: ./gradlew bluetooth:bluetooth:check
bluetooth:bluetooth:connectedCheck
bluetooth:bluetooth-testing:check
Change-Id: Ie4ed73b180b91ab32786f150f744d19de947a89a
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
index 34276ac..8c6a2e1 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricAdvertiseTest.kt
@@ -21,7 +21,7 @@
import androidx.bluetooth.AdvertiseResult
import androidx.bluetooth.BluetoothLe
import java.util.UUID
-import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
import org.junit.Assert
@@ -40,8 +40,10 @@
fun advertiseSuccess() = runTest {
val params = AdvertiseParams()
launch {
- val result = bluetoothLe.advertise(params).first()
- Assert.assertEquals(AdvertiseResult.ADVERTISE_STARTED, result)
+ bluetoothLe.advertise(params) { result ->
+ Assert.assertEquals(AdvertiseResult.ADVERTISE_STARTED, result)
+ cancel()
+ }
}
}
@@ -59,8 +61,9 @@
)
launch {
- val result = bluetoothLe.advertise(advertiseParams).first()
- Assert.assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, result)
+ bluetoothLe.advertise(advertiseParams) { result ->
+ Assert.assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, result)
+ }
}
}
}
diff --git a/bluetooth/bluetooth/api/current.txt b/bluetooth/bluetooth/api/current.txt
index 2bfa8e7..507013b 100644
--- a/bluetooth/bluetooth/api/current.txt
+++ b/bluetooth/bluetooth/api/current.txt
@@ -2,15 +2,16 @@
package androidx.bluetooth {
public final class AdvertiseParams {
- ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional int timeoutMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+ ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional @IntRange(from=0L, to=655350L) int durationMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+ method public int getDurationMillis();
method public java.util.Map<java.lang.Integer,byte[]> getManufacturerData();
method public java.util.Map<java.util.UUID,byte[]> getServiceData();
method public java.util.List<java.util.UUID> getServiceUuids();
method public boolean getShouldIncludeDeviceAddress();
method public boolean getShouldIncludeDeviceName();
- method public int getTimeoutMillis();
method public boolean isConnectable();
method public boolean isDiscoverable();
+ property public final int durationMillis;
property public final boolean isConnectable;
property public final boolean isDiscoverable;
property public final java.util.Map<java.lang.Integer,byte[]> manufacturerData;
@@ -18,7 +19,6 @@
property public final java.util.List<java.util.UUID> serviceUuids;
property public final boolean shouldIncludeDeviceAddress;
property public final boolean shouldIncludeDeviceName;
- property public final int timeoutMillis;
}
public final class AdvertiseResult {
@@ -63,7 +63,7 @@
public final class BluetoothLe {
ctor public BluetoothLe(android.content.Context context);
- method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public kotlinx.coroutines.flow.Flow<java.lang.Integer> advertise(androidx.bluetooth.AdvertiseParams advertiseParams);
+ method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
diff --git a/bluetooth/bluetooth/api/restricted_current.txt b/bluetooth/bluetooth/api/restricted_current.txt
index 2bfa8e7..507013b 100644
--- a/bluetooth/bluetooth/api/restricted_current.txt
+++ b/bluetooth/bluetooth/api/restricted_current.txt
@@ -2,15 +2,16 @@
package androidx.bluetooth {
public final class AdvertiseParams {
- ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional int timeoutMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+ ctor public AdvertiseParams(optional boolean shouldIncludeDeviceAddress, optional boolean shouldIncludeDeviceName, optional boolean isConnectable, optional boolean isDiscoverable, optional @IntRange(from=0L, to=655350L) int durationMillis, optional java.util.Map<java.lang.Integer,byte[]> manufacturerData, optional java.util.Map<java.util.UUID,byte[]> serviceData, optional java.util.List<java.util.UUID> serviceUuids);
+ method public int getDurationMillis();
method public java.util.Map<java.lang.Integer,byte[]> getManufacturerData();
method public java.util.Map<java.util.UUID,byte[]> getServiceData();
method public java.util.List<java.util.UUID> getServiceUuids();
method public boolean getShouldIncludeDeviceAddress();
method public boolean getShouldIncludeDeviceName();
- method public int getTimeoutMillis();
method public boolean isConnectable();
method public boolean isDiscoverable();
+ property public final int durationMillis;
property public final boolean isConnectable;
property public final boolean isDiscoverable;
property public final java.util.Map<java.lang.Integer,byte[]> manufacturerData;
@@ -18,7 +19,6 @@
property public final java.util.List<java.util.UUID> serviceUuids;
property public final boolean shouldIncludeDeviceAddress;
property public final boolean shouldIncludeDeviceName;
- property public final int timeoutMillis;
}
public final class AdvertiseResult {
@@ -63,7 +63,7 @@
public final class BluetoothLe {
ctor public BluetoothLe(android.content.Context context);
- method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public kotlinx.coroutines.flow.Flow<java.lang.Integer> advertise(androidx.bluetooth.AdvertiseParams advertiseParams);
+ method @RequiresPermission("android.permission.BLUETOOTH_ADVERTISE") public suspend Object? advertise(androidx.bluetooth.AdvertiseParams advertiseParams, optional kotlin.jvm.functions.Function2<? super java.lang.Integer,? super kotlin.coroutines.Continuation<? super kotlin.Unit>,?>? block, optional kotlin.coroutines.Continuation<? super kotlin.Unit>);
method @RequiresPermission("android.permission.BLUETOOTH_CONNECT") public suspend <R> Object? connectGatt(androidx.bluetooth.BluetoothDevice device, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattClientScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
method public suspend <R> Object? openGattServer(java.util.List<androidx.bluetooth.GattService> services, kotlin.jvm.functions.Function2<? super androidx.bluetooth.BluetoothLe.GattServerConnectScope,? super kotlin.coroutines.Continuation<? super R>,?> block, kotlin.coroutines.Continuation<? super kotlin.Result<? extends R>>);
method @RequiresPermission("android.permission.BLUETOOTH_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<androidx.bluetooth.ScanFilter> filters);
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
index 6b59576..dc7d197 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/AdvertiseParamsTest.kt
@@ -33,7 +33,7 @@
assertEquals(false, advertiseParams.shouldIncludeDeviceName)
assertEquals(false, advertiseParams.isConnectable)
assertEquals(false, advertiseParams.isDiscoverable)
- assertEquals(0, advertiseParams.timeoutMillis)
+ assertEquals(0, advertiseParams.durationMillis)
assertEquals(0, advertiseParams.manufacturerData.size)
assertEquals(0, advertiseParams.serviceData.size)
assertEquals(0, advertiseParams.serviceUuids.size)
diff --git a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
index 2b6b036..822fd61 100644
--- a/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
+++ b/bluetooth/bluetooth/src/androidTest/java/androidx/bluetooth/BluetoothLeTest.kt
@@ -24,7 +24,6 @@
import java.util.UUID
import junit.framework.TestCase.assertEquals
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.first
import kotlinx.coroutines.test.runTest
import org.junit.Assume
import org.junit.Before
@@ -71,10 +70,9 @@
fun advertise() = runTest {
val advertiseParams = AdvertiseParams()
- val advertiseResultStarted = bluetoothLe.advertise(advertiseParams)
- .first()
-
- assertEquals(AdvertiseResult.ADVERTISE_STARTED, advertiseResultStarted)
+ bluetoothLe.advertise(advertiseParams) {
+ assertEquals(AdvertiseResult.ADVERTISE_STARTED, it)
+ }
}
@Test
@@ -86,9 +84,8 @@
serviceData = mapOf(parcelUuid to serviceData)
)
- val advertiseResultStarted = bluetoothLe.advertise(advertiseParams)
- .first()
-
- assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, advertiseResultStarted)
+ bluetoothLe.advertise(advertiseParams) {
+ assertEquals(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE, it)
+ }
}
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
index f45a905..a929b44 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/AdvertiseParams.kt
@@ -16,6 +16,7 @@
package androidx.bluetooth
+import androidx.annotation.IntRange
import java.util.UUID
/**
@@ -35,8 +36,14 @@
val isConnectable: Boolean = false,
/* Whether the advertisement will be discoverable. */
val isDiscoverable: Boolean = false,
- /* Advertising time limit in milliseconds. */
- val timeoutMillis: Int = 0,
+ /**
+ * Advertising duration in milliseconds
+ *
+ * It must not exceed 655350 milliseconds. A value of 0 means advertising continues
+ * until it is stopped explicitly.
+ */
+ @IntRange(from = 0, to = 655350) val durationMillis: Int = 0,
+
/**
* A map of manufacturer specific data.
* <p>
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index e59357f..4cf0a51 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -31,10 +31,15 @@
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import java.util.UUID
+import kotlin.coroutines.coroutineContext
+import kotlinx.coroutines.CompletableDeferred
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.job
/**
* Entry point for BLE related operations. This class provides a way to perform Bluetooth LE
@@ -67,39 +72,44 @@
var onStartScanListener: OnStartScanListener? = null
/**
- * Returns a _cold_ [Flow] to start Bluetooth LE Advertising.
- * When the flow is successfully collected, the operation status [AdvertiseResult] will be
- * delivered via the flow [kotlinx.coroutines.channels.Channel].
+ * Starts Bluetooth LE advertising
*
- * @param advertiseParams [AdvertiseParams] for Bluetooth LE advertising
- * @return a _cold_ [Flow] with [AdvertiseResult] status in the data stream
+ * Note that this method may not complete if the duration is set to 0.
+ * To stop advertising, in that case, you should cancel the coroutine.
+ *
+ * @param advertiseParams [AdvertiseParams] for Bluetooth LE advertising.
+ * @param block an optional block of code that is invoked when advertising is started or failed.
+ *
+ * @throws IllegalArgumentException if the advertise parameters are not valid.
*/
@RequiresPermission("android.permission.BLUETOOTH_ADVERTISE")
- fun advertise(advertiseParams: AdvertiseParams): Flow<@AdvertiseResult.ResultType Int> =
- callbackFlow {
+ suspend fun advertise(
+ advertiseParams: AdvertiseParams,
+ block: (suspend (@AdvertiseResult.ResultType Int) -> Unit)? = null
+ ) {
+ val result = CompletableDeferred<Int>()
+
val callback = object : AdvertiseCallback() {
override fun onStartFailure(errorCode: Int) {
Log.d(TAG, "onStartFailure() called with: errorCode = $errorCode")
when (errorCode) {
ADVERTISE_FAILED_DATA_TOO_LARGE ->
- trySend(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE)
+ result.complete(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE)
ADVERTISE_FAILED_FEATURE_UNSUPPORTED ->
- trySend(AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
+ result.complete(AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
ADVERTISE_FAILED_INTERNAL_ERROR ->
- trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
+ result.complete(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
ADVERTISE_FAILED_TOO_MANY_ADVERTISERS ->
- trySend(AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
+ result.complete(AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
}
}
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
- Log.d(TAG, "onStartSuccess() called with: settingsInEffect = $settingsInEffect")
-
- trySend(AdvertiseResult.ADVERTISE_STARTED)
+ result.complete(AdvertiseResult.ADVERTISE_STARTED)
}
}
@@ -107,7 +117,11 @@
val advertiseSettings = with(AdvertiseSettings.Builder()) {
setConnectable(advertiseParams.isConnectable)
- setTimeout(advertiseParams.timeoutMillis)
+ advertiseParams.durationMillis.let {
+ if (it !in 0..655350)
+ throw IllegalArgumentException("advertise duration must be in [0, 655350]")
+ setTimeout(it)
+ }
// TODO(b/290697177) Add when AndroidX is targeting Android U
// setDiscoverable(advertiseParams.isDiscoverable)
build()
@@ -127,13 +141,21 @@
build()
}
- Log.d(TAG, "bleAdvertiser.startAdvertising($advertiseSettings, $advertiseData) called")
bleAdvertiser?.startAdvertising(advertiseSettings, advertiseData, callback)
- awaitClose {
- Log.d(TAG, "bleAdvertiser.stopAdvertising() called")
+ coroutineContext.job.invokeOnCompletion {
bleAdvertiser?.stopAdvertising(callback)
}
+ result.await().let {
+ block?.invoke(it)
+ if (it == AdvertiseResult.ADVERTISE_STARTED) {
+ if (advertiseParams.durationMillis > 0) {
+ delay(advertiseParams.durationMillis.toLong())
+ } else {
+ awaitCancellation()
+ }
+ }
+ }
}
/**
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
index 89a87ca..64f2ae7 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserFragment.kt
@@ -325,32 +325,33 @@
advertiseJob = advertiseScope.launch {
isAdvertising = true
- bluetoothLe.advertise(viewModel.advertiseParams)
- .collect {
- Log.d(TAG, "AdvertiseResult collected: $it")
+ bluetoothLe.advertise(viewModel.advertiseParams) {
+ when (it) {
+ AdvertiseResult.ADVERTISE_STARTED -> {
+ toast("ADVERTISE_STARTED").show()
+ }
- when (it) {
- AdvertiseResult.ADVERTISE_STARTED -> {
- toast("ADVERTISE_STARTED").show()
- }
- AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE -> {
- isAdvertising = false
- toast("ADVERTISE_FAILED_DATA_TOO_LARGE").show()
- }
- AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
- isAdvertising = false
- toast("ADVERTISE_FAILED_FEATURE_UNSUPPORTED").show()
- }
- AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR -> {
- isAdvertising = false
- toast("ADVERTISE_FAILED_INTERNAL_ERROR").show()
- }
- AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
- isAdvertising = false
- toast("ADVERTISE_FAILED_TOO_MANY_ADVERTISERS").show()
- }
+ AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE -> {
+ isAdvertising = false
+ toast("ADVERTISE_FAILED_DATA_TOO_LARGE").show()
+ }
+
+ AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED -> {
+ isAdvertising = false
+ toast("ADVERTISE_FAILED_FEATURE_UNSUPPORTED").show()
+ }
+
+ AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR -> {
+ isAdvertising = false
+ toast("ADVERTISE_FAILED_INTERNAL_ERROR").show()
+ }
+
+ AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS -> {
+ isAdvertising = false
+ toast("ADVERTISE_FAILED_TOO_MANY_ADVERTISERS").show()
}
}
+ }
}
}
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
index ba55df3..99cb7f8 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/advertiser/AdvertiserViewModel.kt
@@ -32,7 +32,7 @@
var includeDeviceName = false
var connectable = false
var discoverable = false
- var timeoutMillis = 0
+ var durationMillis = 0
var manufacturerDatas = mutableListOf<Pair<Int, ByteArray>>()
var serviceDatas = mutableListOf<Pair<UUID, ByteArray>>()
var serviceUuids = mutableListOf<UUID>()
@@ -56,7 +56,7 @@
includeDeviceName,
connectable,
discoverable,
- timeoutMillis,
+ durationMillis,
manufacturerDatas.toMap(),
serviceDatas.toMap(),
serviceUuids