Extract scan functionality to its own class
Bug: 309745701
Test: build
Relnote: Extract scan functionality
Change-Id: I4d43f370b201c942bea0eda1230fef7ff6ef8aca
diff --git a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
index 3d727c3..63ca35b 100644
--- a/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
@@ -16,76 +16,82 @@
package androidx.bluetooth.testing
-import android.bluetooth.le.ScanResult
-import android.bluetooth.le.ScanSettings.CALLBACK_TYPE_ALL_MATCHES
+import android.bluetooth.BluetoothAdapter
+import android.bluetooth.BluetoothManager
+import android.bluetooth.le.ScanResult as FwkScanResult
+import android.content.Context
import androidx.bluetooth.BluetoothLe
import androidx.bluetooth.ScanFilter
-import java.util.concurrent.atomic.AtomicReference
-import kotlinx.coroutines.cancel
+import androidx.bluetooth.ScanImpl
+import androidx.bluetooth.ScanResult
+import junit.framework.TestCase.assertEquals
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.asFlow
import kotlinx.coroutines.flow.collectIndexed
-import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runTest
-import org.junit.Assert
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.RobolectricTestRunner
import org.robolectric.RuntimeEnvironment
-import org.robolectric.Shadows.shadowOf
-import org.robolectric.shadows.ShadowBluetoothDevice
-import org.robolectric.shadows.ShadowBluetoothLeScanner
@RunWith(RobolectricTestRunner::class)
@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
class RobolectricScanTest {
- private companion object {
- private const val TIMEOUT_MS: Long = 2_000
- }
+ private val context: Context = RuntimeEnvironment.getApplication()
+ private val bluetoothManager: BluetoothManager =
+ context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
+ private val bluetoothAdapter: BluetoothAdapter = bluetoothManager.adapter
private val bluetoothLe = BluetoothLe(RuntimeEnvironment.getApplication())
- @Test
- fun scanTest() = runTest {
- val scanResults = listOf(
- createScanResult("00:00:00:00:00:01"),
- createScanResult("00:00:00:00:00:02"),
- createScanResult("00:00:00:00:00:03"),
- )
+ private val scanResults = listOf(
+ createScanResult("00:00:00:00:00:01"),
+ createScanResult("00:00:00:00:00:02"),
+ createScanResult("00:00:00:00:00:03")
+ )
- val scannerRef = AtomicReference<ShadowBluetoothLeScanner>(null)
- bluetoothLe.onStartScanListener = BluetoothLe.OnStartScanListener { scanner ->
- val shadowScanner = shadowOf(scanner)
- scannerRef.set(shadowScanner)
-
- // Check if the scan is started
- Assert.assertEquals(1, shadowScanner.activeScans.size)
-
- shadowScanner.scanCallbacks.forEach { callback ->
- scanResults.forEach { res ->
- callback.onScanResult(CALLBACK_TYPE_ALL_MATCHES, res)
- }
- }
- }
-
- launch {
- bluetoothLe.scan(listOf(ScanFilter())).collectIndexed { index, value ->
- Assert.assertEquals(scanResults[index].device.address, value.deviceAddress.address)
- if (index == scanResults.size - 1) {
- this.cancel()
- }
- }
- }.join()
-
- // Check if the scan is stopped
- Assert.assertEquals(0, scannerRef.get().activeScans.size)
+ @Before
+ fun setUp() {
+ bluetoothLe.scanImpl = ScanImplForTesting(scanResults)
}
- @Suppress("DEPRECATION")
+ @Test
+ fun scanTest() = runTest {
+ bluetoothLe.scan(listOf(ScanFilter()))
+ .collectIndexed { index, value ->
+ assertEquals(scanResults[index].deviceAddress.address, value.deviceAddress.address)
+ }
+ }
+
private fun createScanResult(
address: String,
- rssi: Int = 0,
- timestampNanos: Long = 0
): ScanResult {
- return ScanResult(ShadowBluetoothDevice.newInstance(address), null, rssi, timestampNanos)
+ val fwkBluetoothDevice = bluetoothAdapter.getRemoteDevice(address)
+ val timeStampNanos: Long = 1
+ val rssi = 34
+ val periodicAdvertisingInterval = 8
+
+ // TODO(kihongs) Find a way to create framework ScanRecord and use in test
+ val fwkScanResult = FwkScanResult(
+ fwkBluetoothDevice,
+ 1,
+ 0,
+ 0,
+ 0,
+ 0,
+ rssi,
+ periodicAdvertisingInterval,
+ null,
+ timeStampNanos
+ )
+ return ScanResult(fwkScanResult)
+ }
+}
+
+class ScanImplForTesting(val scanResults: List<ScanResult>) : ScanImpl {
+ override fun scan(filters: List<ScanFilter>): Flow<ScanResult> {
+ return scanResults.asFlow()
}
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index aaf0e98..9e7c9b5 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -17,20 +17,14 @@
package androidx.bluetooth
import android.bluetooth.BluetoothManager as FwkBluetoothManager
-import android.bluetooth.le.BluetoothLeScanner as FwkBluetoothLeScanner
-import android.bluetooth.le.ScanCallback as FwkScanCallback
-import android.bluetooth.le.ScanResult as FwkScanResult
-import android.bluetooth.le.ScanSettings as FwkScanSettings
import android.content.Context
import androidx.annotation.IntDef
import androidx.annotation.RequiresPermission
import androidx.annotation.RestrictTo
import androidx.annotation.VisibleForTesting
import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flowOf
/**
@@ -74,6 +68,16 @@
context.getSystemService(Context.BLUETOOTH_SERVICE) as FwkBluetoothManager?
private val bluetoothAdapter = bluetoothManager?.adapter
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ var advertiseImpl: AdvertiseImpl? =
+ bluetoothAdapter?.bluetoothLeAdvertiser?.let(::getAdvertiseImpl)
+
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+ var scanImpl: ScanImpl? =
+ bluetoothAdapter?.bluetoothLeScanner?.let(::getScanImpl)
+
@VisibleForTesting
@get:RestrictTo(RestrictTo.Scope.LIBRARY)
val client: GattClient by lazy(LazyThreadSafetyMode.PUBLICATION) {
@@ -86,16 +90,6 @@
GattServer(context.applicationContext)
}
- @get:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- @set:RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
- var advertiseImpl: AdvertiseImpl? =
- bluetoothAdapter?.bluetoothLeAdvertiser?.let(::getAdvertiseImpl)
-
- @VisibleForTesting
- @get:RestrictTo(RestrictTo.Scope.LIBRARY)
- @set:RestrictTo(RestrictTo.Scope.LIBRARY)
- var onStartScanListener: OnStartScanListener? = null
-
/**
* Returns a _cold_ [Flow] to start Bluetooth LE advertising
*
@@ -117,33 +111,15 @@
/**
* Returns a _cold_ [Flow] to start Bluetooth LE scanning.
* Scanning is used to discover advertising devices nearby.
+ * Returns an `emptyFlow()` if bluetoothLeScanner is not available.
*
* @param filters [ScanFilter]s for finding exact Bluetooth LE devices
*
* @return a _cold_ [Flow] of [ScanResult] that matches with the given scan filter
*/
@RequiresPermission("android.permission.BLUETOOTH_SCAN")
- fun scan(filters: List<ScanFilter> = emptyList()): Flow<ScanResult> = callbackFlow {
- val callback = object : FwkScanCallback() {
- override fun onScanResult(callbackType: Int, result: FwkScanResult) {
- trySend(ScanResult(result))
- }
-
- override fun onScanFailed(errorCode: Int) {
- // TODO(b/270492198): throw precise exception
- cancel("onScanFailed() called with: errorCode = $errorCode")
- }
- }
-
- val bleScanner = bluetoothAdapter?.bluetoothLeScanner
- val fwkFilters = filters.map { it.fwkScanFilter }
- val scanSettings = FwkScanSettings.Builder().build()
- bleScanner?.startScan(fwkFilters, scanSettings, callback)
- onStartScanListener?.onStartScan(bleScanner)
-
- awaitClose {
- bleScanner?.stopScan(callback)
- }
+ fun scan(filters: List<ScanFilter> = emptyList()): Flow<ScanResult> {
+ return scanImpl?.scan(filters) ?: emptyFlow()
}
/**
@@ -184,10 +160,4 @@
): R {
return server.open(services, block)
}
-
- @VisibleForTesting
- @RestrictTo(RestrictTo.Scope.LIBRARY)
- fun interface OnStartScanListener {
- fun onStartScan(scanner: FwkBluetoothLeScanner?)
- }
}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/Scan.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/Scan.kt
new file mode 100644
index 0000000..785bfa6
--- /dev/null
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/Scan.kt
@@ -0,0 +1,64 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package androidx.bluetooth
+
+import android.bluetooth.le.BluetoothLeScanner as FwkBluetoothLeScanner
+import android.bluetooth.le.ScanCallback as FwkScanCallback
+import android.bluetooth.le.ScanResult as FwkScanResult
+import android.bluetooth.le.ScanSettings as FwkScanSettings
+import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
+
+@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
+interface ScanImpl {
+ fun scan(filters: List<ScanFilter> = emptyList()): Flow<ScanResult>
+}
+
+internal fun getScanImpl(bluetoothLeScanner: FwkBluetoothLeScanner): ScanImpl {
+ return ScanImplBase(bluetoothLeScanner)
+}
+
+private open class ScanImplBase(val bluetoothLeScanner: FwkBluetoothLeScanner) : ScanImpl {
+
+ @RequiresPermission("android.permission.BLUETOOTH_SCAN")
+ override fun scan(filters: List<ScanFilter>): Flow<ScanResult> = callbackFlow {
+ val callback = object : FwkScanCallback() {
+ override fun onScanResult(callbackType: Int, result: FwkScanResult) {
+ trySend(ScanResult(result))
+ }
+
+ override fun onScanFailed(errorCode: Int) {
+ // TODO(b/270492198): throw precise exception
+ cancel("onScanFailed() called with: errorCode = $errorCode")
+ }
+ }
+
+ val fwkFilters = filters.map { it.fwkScanFilter }
+ val fwkSettings = FwkScanSettings.Builder()
+ .build()
+
+ bluetoothLeScanner.startScan(fwkFilters, fwkSettings, callback)
+
+ awaitClose {
+ bluetoothLeScanner.stopScan(callback)
+ }
+ }
+}
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
index 09f6d3b..13515c9 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/ScanResult.kt
@@ -18,6 +18,7 @@
import android.bluetooth.le.ScanResult as FwkScanResult
import android.os.ParcelUuid
+import androidx.annotation.RestrictTo
import java.util.UUID
/**
@@ -35,7 +36,9 @@
* bluetooth GATT services.
*
*/
-class ScanResult internal constructor(private val fwkScanResult: FwkScanResult) {
+class ScanResult @RestrictTo(RestrictTo.Scope.LIBRARY) constructor(
+ private val fwkScanResult: FwkScanResult
+) {
companion object {
/**