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