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