• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.bluetooth
18 
19 import android.annotation.SuppressLint
20 import android.content.BroadcastReceiver
21 import android.content.Context
22 import android.content.Intent
23 import android.content.IntentFilter
24 import android.util.Log
25 import com.google.common.truth.Truth
26 import com.google.common.truth.Truth.assertThat
27 import java.io.Closeable
28 import kotlin.time.Duration.Companion.seconds
29 import kotlinx.coroutines.CoroutineScope
30 import kotlinx.coroutines.Dispatchers
31 import kotlinx.coroutines.cancel
32 import kotlinx.coroutines.channels.awaitClose
33 import kotlinx.coroutines.channels.trySendBlocking
34 import kotlinx.coroutines.flow.Flow
35 import kotlinx.coroutines.flow.SharingStarted
36 import kotlinx.coroutines.flow.callbackFlow
37 import kotlinx.coroutines.flow.filter
38 import kotlinx.coroutines.flow.first
39 import kotlinx.coroutines.flow.shareIn
40 import kotlinx.coroutines.launch
41 import kotlinx.coroutines.runBlocking
42 import kotlinx.coroutines.withTimeout
43 
44 @SuppressLint("MissingPermission")
45 @kotlinx.coroutines.ExperimentalCoroutinesApi
46 public class Host(context: Context) : Closeable {
47     private val TAG = "PandoraHost"
48 
49     private val flow: Flow<Intent>
50     private val scope: CoroutineScope
51     private val bluetoothManager = context.getSystemService(BluetoothManager::class.java)
52     private val bluetoothAdapter = bluetoothManager!!.adapter
53 
54     init {
55         scope = CoroutineScope(Dispatchers.Default.limitedParallelism(1))
56         val intentFilter = IntentFilter()
57         intentFilter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
58         intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
59         intentFilter.addAction(BluetoothDevice.ACTION_ACL_DISCONNECTED)
60 
61         flow = intentFlow(context, intentFilter, scope).shareIn(scope, SharingStarted.Eagerly)
62     }
63 
closenull64     override fun close() {
65         scope.cancel()
66     }
67 
createBondAndVerifynull68     public fun createBondAndVerify(remoteDevice: BluetoothDevice) {
69         Log.d(TAG, "createBondAndVerify: $remoteDevice")
70         if (bluetoothAdapter.bondedDevices.contains(remoteDevice)) {
71             Log.d(TAG, "createBondAndVerify: already bonded")
72             return
73         }
74 
75         runBlocking(scope.coroutineContext) {
76             withTimeout(TIMEOUT) {
77                 Truth.assertThat(remoteDevice.createBond()).isTrue()
78                 val pairingRequestJob = launch {
79                     Log.d(TAG, "Waiting for ACTION_PAIRING_REQUEST")
80                     flow
81                         .filter { it.action == BluetoothDevice.ACTION_PAIRING_REQUEST }
82                         .filter { it.getBluetoothDeviceExtra() == remoteDevice }
83                         .first()
84 
85                     remoteDevice.setPairingConfirmation(true)
86                 }
87 
88                 Log.d(TAG, "Waiting for ACTION_BOND_STATE_CHANGED")
89                 flow
90                     .filter { it.action == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
91                     .filter { it.getBluetoothDeviceExtra() == remoteDevice }
92                     .filter {
93                         it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
94                             BluetoothDevice.BOND_BONDED
95                     }
96                     .first()
97 
98                 if (pairingRequestJob.isActive) {
99                     pairingRequestJob.cancel()
100                 }
101 
102                 Log.d(TAG, "createBondAndVerify: bonded")
103             }
104         }
105     }
106 
removeBondAndVerifynull107     fun removeBondAndVerify(remoteDevice: BluetoothDevice) {
108         Log.d(TAG, "removeBondAndVerify: $remoteDevice")
109         runBlocking(scope.coroutineContext) {
110             withTimeout(TIMEOUT) {
111                 assertThat(remoteDevice.removeBond()).isTrue()
112                 flow
113                     .filter { it.getAction() == BluetoothDevice.ACTION_BOND_STATE_CHANGED }
114                     .filter { it.getBluetoothDeviceExtra() == remoteDevice }
115                     .filter {
116                         it.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothAdapter.ERROR) ==
117                             BluetoothDevice.BOND_NONE
118                     }
119                     .first()
120                 Log.d(TAG, "removeBondAndVerify: done")
121             }
122         }
123     }
124 
disconnectAndVerifynull125     fun disconnectAndVerify(remoteDevice: BluetoothDevice) {
126         Log.d(TAG, "disconnectAndVerify: $remoteDevice")
127         runBlocking(scope.coroutineContext) {
128             withTimeout(TIMEOUT) {
129                 assertThat(remoteDevice.disconnect()).isEqualTo(BluetoothStatusCodes.SUCCESS)
130                 flow
131                     .filter { it.getAction() == BluetoothDevice.ACTION_ACL_DISCONNECTED }
132                     .filter {
133                         it.getIntExtra(
134                             BluetoothDevice.EXTRA_TRANSPORT,
135                             BluetoothDevice.TRANSPORT_AUTO,
136                         ) == BluetoothDevice.TRANSPORT_BREDR
137                     }
138                     .filter { it.getBluetoothDeviceExtra() == remoteDevice }
139                     .first()
140                 Log.d(TAG, "disconnectAndVerify: done")
141             }
142         }
143     }
144 
Intentnull145     fun Intent.getBluetoothDeviceExtra(): BluetoothDevice =
146         this.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE, BluetoothDevice::class.java)!!
147 
148     @kotlinx.coroutines.ExperimentalCoroutinesApi
149     fun intentFlow(context: Context, intentFilter: IntentFilter, scope: CoroutineScope) =
150         callbackFlow {
151             val broadcastReceiver: BroadcastReceiver =
152                 object : BroadcastReceiver() {
153                     override fun onReceive(context: Context, intent: Intent) {
154                         Log.d(TAG, "intentFlow: onReceive: ${intent.action}")
155                         scope.launch { trySendBlocking(intent) }
156                     }
157                 }
158             context.registerReceiver(broadcastReceiver, intentFilter)
159 
160             awaitClose { context.unregisterReceiver(broadcastReceiver) }
161         }
162 
163     companion object {
164         private val TIMEOUT = 20.seconds
165     }
166 }
167