[go: nahoru, domu]

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