Add scan implementation inside BluetoothLe class
Implement scanning method with AndroidX classes
Bug: 269369543
Test: ./gradlew bluetooth:bluetooth:check &&
./gradlew bluetooth:bluetooth-testing:check
Relnote: add scan method
Change-Id: I8cb14e5686500a47364d40eab0a5683f0d00bbe3
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
new file mode 100644
index 0000000..b1201c1
--- /dev/null
+++ b/bluetooth/bluetooth-testing/src/test/kotlin/androidx/bluetooth/testing/RobolectricScanTest.kt
@@ -0,0 +1,53 @@
+/*
+ * 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.testing
+
+import android.content.Context
+import androidx.bluetooth.BluetoothLe
+import junit.framework.TestCase.fail
+import kotlinx.coroutines.TimeoutCancellationException
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.withTimeout
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.robolectric.RobolectricTestRunner
+import org.robolectric.RuntimeEnvironment
+
+@RunWith(RobolectricTestRunner::class)
+@OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+class RobolectricScanTest {
+ private val context: Context = RuntimeEnvironment.getApplication()
+ private var bluetoothLe = BluetoothLe(context)
+ private companion object {
+ private const val TIMEOUT_MS: Long = 2_000
+ }
+
+ @Test
+ fun scanTest() = runTest {
+ try {
+ withTimeout(TIMEOUT_MS) {
+ bluetoothLe.scan(listOf()).collect {
+ // Should not find any device
+ fail()
+ }
+ }
+ fail()
+ } catch (e: TimeoutCancellationException) {
+ // expected
+ }
+ }
+}
\ No newline at end of file
diff --git a/bluetooth/bluetooth/api/current.txt b/bluetooth/bluetooth/api/current.txt
index cdcca53..0cf385d 100644
--- a/bluetooth/bluetooth/api/current.txt
+++ b/bluetooth/bluetooth/api/current.txt
@@ -42,6 +42,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_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<android.bluetooth.le.ScanFilter> filters);
}
public final class ScanFilter {
diff --git a/bluetooth/bluetooth/api/restricted_current.txt b/bluetooth/bluetooth/api/restricted_current.txt
index cdcca53..0cf385d 100644
--- a/bluetooth/bluetooth/api/restricted_current.txt
+++ b/bluetooth/bluetooth/api/restricted_current.txt
@@ -42,6 +42,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_SCAN") public kotlinx.coroutines.flow.Flow<androidx.bluetooth.ScanResult> scan(optional java.util.List<android.bluetooth.le.ScanFilter> filters);
}
public final class ScanFilter {
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
index 306d336..db94cc1 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothDevice.kt
@@ -18,6 +18,7 @@
import android.bluetooth.BluetoothDevice as FwkBluetoothDevice
import androidx.annotation.RequiresPermission
+import androidx.annotation.RestrictTo
import java.util.UUID
/**
@@ -29,7 +30,10 @@
* @property bondState the bondState for this BluetoothDevice
*
*/
-class BluetoothDevice internal constructor(private val fwkDevice: FwkBluetoothDevice) {
+class BluetoothDevice internal constructor(
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ val fwkDevice: FwkBluetoothDevice
+) {
val id: UUID = UUID.randomUUID()
@get:RequiresPermission(
@@ -39,6 +43,10 @@
val name: String?
get() = fwkDevice.name
+ @get:RestrictTo(RestrictTo.Scope.LIBRARY)
+ val address: String
+ get() = fwkDevice.address
+
@get:RequiresPermission(
anyOf = ["android.permission.BLUETOOTH",
"android.permission.BLUETOOTH_CONNECT"]
diff --git a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
index 3a0bedf..bd54186c 100644
--- a/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
+++ b/bluetooth/bluetooth/src/main/java/androidx/bluetooth/BluetoothLe.kt
@@ -20,10 +20,15 @@
import android.bluetooth.le.AdvertiseCallback
import android.bluetooth.le.AdvertiseData
import android.bluetooth.le.AdvertiseSettings
+import android.bluetooth.le.ScanCallback
+import android.bluetooth.le.ScanFilter
+import android.bluetooth.le.ScanResult as FwkScanResult
+import android.bluetooth.le.ScanSettings
import android.content.Context
import android.os.ParcelUuid
import android.util.Log
import androidx.annotation.RequiresPermission
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
@@ -41,7 +46,6 @@
private val bluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
-
/**
* Returns a [Flow] to start Bluetooth LE Advertising. When the flow is successfully collected,
* the operation status [AdvertiseResult] will be delivered via the
@@ -60,10 +64,13 @@
when (errorCode) {
AdvertiseCallback.ADVERTISE_FAILED_DATA_TOO_LARGE ->
trySend(AdvertiseResult.ADVERTISE_FAILED_DATA_TOO_LARGE)
+
AdvertiseCallback.ADVERTISE_FAILED_FEATURE_UNSUPPORTED ->
trySend(AdvertiseResult.ADVERTISE_FAILED_FEATURE_UNSUPPORTED)
+
AdvertiseCallback.ADVERTISE_FAILED_INTERNAL_ERROR ->
trySend(AdvertiseResult.ADVERTISE_FAILED_INTERNAL_ERROR)
+
AdvertiseCallback.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS ->
trySend(AdvertiseResult.ADVERTISE_FAILED_TOO_MANY_ADVERTISERS)
}
@@ -110,5 +117,36 @@
}
}
- // TODO(ofy) Add remainder of class functions such as scan, connectGatt, openGattServer...
-}
+ /**
+ * Returns a cold [Flow] to start Bluetooth LE scanning. Scanning is used to
+ * discover advertising devices nearby.
+ *
+ * @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 : ScanCallback() {
+ 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 bluetoothAdapter = bluetoothManager.adapter
+ val bleScanner = bluetoothAdapter?.bluetoothLeScanner
+ val scanSettings = ScanSettings.Builder().build()
+
+ bleScanner?.startScan(filters, scanSettings, callback)
+
+ awaitClose {
+ Log.d(TAG, "awaitClose() called")
+ bleScanner?.stopScan(callback)
+ }
+ }
+}
\ No newline at end of file
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
index 53e105e..86fc8f5 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/data/connection/DeviceConnection.kt
@@ -16,12 +16,11 @@
package androidx.bluetooth.integration.testapp.data.connection
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothDevice
// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattService
-import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
import android.bluetooth.BluetoothGattService
+import androidx.bluetooth.BluetoothDevice
import java.util.UUID
import kotlinx.coroutines.Job
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerAdapter.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerAdapter.kt
index 3b59942..c755d82 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerAdapter.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerAdapter.kt
@@ -16,16 +16,14 @@
package androidx.bluetooth.integration.testapp.ui.scanner
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothDevice
-// TODO(ofy) Migrate to androidx.bluetooth.ScanResult
import android.annotation.SuppressLint
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.le.ScanResult
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.TextView
+import androidx.bluetooth.BluetoothDevice
+import androidx.bluetooth.ScanResult
import androidx.bluetooth.integration.testapp.R
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DiffUtil
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
index 6b5a1c23..64dc6bf 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerFragment.kt
@@ -17,12 +17,9 @@
package androidx.bluetooth.integration.testapp.ui.scanner
// TODO(ofy) Migrate to androidx.bluetooth.AdvertiseParams
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothDevice
// TODO(ofy) Migrate to androidx.bluetooth.BluetoothGattCharacteristic
import android.annotation.SuppressLint
-import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothGattCharacteristic
-import android.bluetooth.le.ScanSettings
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
@@ -32,12 +29,14 @@
import android.widget.EditText
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
+import androidx.bluetooth.BluetoothDevice
+import androidx.bluetooth.BluetoothLe
import androidx.bluetooth.integration.testapp.R
import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
import androidx.bluetooth.integration.testapp.data.connection.OnClickCharacteristic
import androidx.bluetooth.integration.testapp.data.connection.Status
import androidx.bluetooth.integration.testapp.databinding.FragmentScannerBinding
-import androidx.bluetooth.integration.testapp.experimental.BluetoothLe
+import androidx.bluetooth.integration.testapp.experimental.BluetoothLe as ExperimentalLe
import androidx.bluetooth.integration.testapp.ui.common.getColor
import androidx.bluetooth.integration.testapp.ui.common.toast
import androidx.core.view.isVisible
@@ -64,8 +63,9 @@
internal const val MANUAL_DISCONNECT = "MANUAL_DISCONNECT"
}
- // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
private lateinit var bluetoothLe: BluetoothLe
+ // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
+ private lateinit var experimenalLe: ExperimentalLe
private var deviceServicesAdapter: DeviceServicesAdapter? = null
@@ -146,6 +146,7 @@
super.onViewCreated(view, savedInstanceState)
bluetoothLe = BluetoothLe(requireContext())
+ experimenalLe = ExperimentalLe(requireContext())
binding.tabLayout.addOnTabSelectedListener(onTabSelectedListener)
@@ -194,15 +195,12 @@
viewModel.deviceConnections.map { it.bluetoothDevice }.forEach(::addNewTab)
}
+ @SuppressLint("MissingPermission")
private fun startScan() {
- // TODO(ofy) Migrate to androidx.bluetooth.BluetoothLe once scan API is in place
- val scanSettings = ScanSettings.Builder()
- .build()
-
scanJob = scanScope.launch {
isScanning = true
- bluetoothLe.scan(scanSettings)
+ bluetoothLe.scan()
.collect {
Log.d(TAG, "ScanResult collected: $it")
@@ -264,7 +262,8 @@
}
try {
- bluetoothLe.connectGatt(requireContext(), deviceConnection.bluetoothDevice) {
+ experimenalLe.connectGatt(requireContext(),
+ deviceConnection.bluetoothDevice.fwkDevice) {
Log.d(TAG, "connectGatt result: getServices() = ${getServices()}")
deviceConnection.status = Status.CONNECTED
diff --git a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerViewModel.kt b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerViewModel.kt
index 906188a..35f0072 100644
--- a/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerViewModel.kt
+++ b/bluetooth/integration-tests/testapp/src/main/java/androidx/bluetooth/integration/testapp/ui/scanner/ScannerViewModel.kt
@@ -16,10 +16,8 @@
package androidx.bluetooth.integration.testapp.ui.scanner
-// TODO(ofy) Migrate to androidx.bluetooth.BluetoothDevice
-// TODO(ofy) Migrate to androidx.bluetooth.ScanResult once in place
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.le.ScanResult
+import androidx.bluetooth.BluetoothDevice
+import androidx.bluetooth.ScanResult
import androidx.bluetooth.integration.testapp.data.connection.DeviceConnection
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData