1 /* 2 * Copyright (C) 2022 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.nearby.fastpair.provider.bluetooth 18 19 import android.bluetooth.BluetoothAdapter 20 import android.bluetooth.BluetoothDevice 21 import android.bluetooth.BluetoothManager 22 import android.bluetooth.BluetoothProfile 23 import android.content.BroadcastReceiver 24 import android.content.Context 25 import android.content.Intent 26 import android.content.IntentFilter 27 import android.nearby.fastpair.provider.FastPairSimulator 28 import android.nearby.fastpair.provider.utils.Logger 29 import android.os.SystemClock 30 import android.provider.Settings 31 32 /** Controls the local Bluetooth adapter for Fast Pair testing. */ 33 class BluetoothController( 34 private val context: Context, 35 private val listener: EventListener 36 ) : BroadcastReceiver() { 37 private val mLogger = Logger(TAG) 38 private val bluetoothAdapter: BluetoothAdapter = 39 (context.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter!! 40 private var remoteDevice: BluetoothDevice? = null 41 private var remoteDeviceConnectionState: Int = BluetoothAdapter.STATE_DISCONNECTED 42 private var a2dpSinkProxy: BluetoothProfile? = null 43 44 /** Turns on the local Bluetooth adapter */ enableBluetoothnull45 fun enableBluetooth() { 46 if (!bluetoothAdapter.isEnabled) { 47 bluetoothAdapter.enable() 48 waitForBluetoothState(BluetoothAdapter.STATE_ON) 49 } 50 } 51 52 /** 53 * Sets the Input/Output capability of the device for classic Bluetooth operations. 54 * Note: In order to let changes take effect, this method will make sure the Bluetooth stack is 55 * restarted by blocking calling thread. 56 * 57 * @param ioCapabilityClassic One of {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_NONE}, 58 * ``` 59 * {@link #IO_CAPABILITY_KBDISP} or more in {@link BluetoothAdapter}. 60 */ setIoCapabilitynull61 fun setIoCapability(ioCapabilityClassic: Int) { 62 bluetoothAdapter.ioCapability = ioCapabilityClassic 63 64 // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE. 65 try { 66 Settings.Global.putInt( 67 context.contentResolver, 68 Settings.Global.AIRPLANE_MODE_ON, 69 TURN_AIRPLANE_MODE_ON 70 ) 71 } catch (expectedOnNonCustomAndroid: SecurityException) { 72 mLogger.log( 73 expectedOnNonCustomAndroid, 74 "Requires custom Android to toggle airplane mode" 75 ) 76 // Fall back to turn off Bluetooth. 77 bluetoothAdapter.disable() 78 } 79 waitForBluetoothState(BluetoothAdapter.STATE_OFF) 80 try { 81 Settings.Global.putInt( 82 context.contentResolver, 83 Settings.Global.AIRPLANE_MODE_ON, 84 TURN_AIRPLANE_MODE_OFF 85 ) 86 } catch (expectedOnNonCustomAndroid: SecurityException) { 87 mLogger.log( 88 expectedOnNonCustomAndroid, 89 "SecurityException while toggled airplane mode." 90 ) 91 } finally { 92 // Double confirm that Bluetooth is turned on. 93 bluetoothAdapter.enable() 94 } 95 waitForBluetoothState(BluetoothAdapter.STATE_ON) 96 } 97 98 /** Registers this Bluetooth state change receiver. */ registerBluetoothStateReceivernull99 fun registerBluetoothStateReceiver() { 100 val bondStateFilter = 101 IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).apply { 102 addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) 103 addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) 104 } 105 context.registerReceiver( 106 this, 107 bondStateFilter, 108 /* broadcastPermission= */ null, 109 /* scheduler= */ null 110 ) 111 } 112 113 /** Unregisters this Bluetooth state change receiver. */ unregisterBluetoothStateReceivernull114 fun unregisterBluetoothStateReceiver() { 115 context.unregisterReceiver(this) 116 } 117 118 /** Clears current remote device. */ clearRemoteDevicenull119 fun clearRemoteDevice() { 120 remoteDevice = null 121 } 122 123 /** Gets current remote device. */ getRemoteDevicenull124 fun getRemoteDevice(): BluetoothDevice? = remoteDevice 125 126 /** Gets current remote device as string. */ 127 fun getRemoteDeviceAsString(): String = remoteDevice?.remoteDeviceToString() ?: "none" 128 129 /** Connects the Bluetooth A2DP sink profile service. */ 130 fun connectA2DPSinkProfile() { 131 // Get the A2DP proxy before continuing with initialization. 132 bluetoothAdapter.getProfileProxy( 133 context, 134 object : BluetoothProfile.ServiceListener { 135 override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { 136 // When Bluetooth turns off and then on again, this is called again. But we only care 137 // the first time. There doesn't seem to be a way to unregister our listener. 138 if (a2dpSinkProxy == null) { 139 a2dpSinkProxy = proxy 140 listener.onA2DPSinkProfileConnected() 141 } 142 } 143 144 override fun onServiceDisconnected(profile: Int) {} 145 }, 146 BluetoothProfile.A2DP_SINK 147 ) 148 } 149 150 /** Get the current Bluetooth scan mode of the local Bluetooth adapter. */ getScanModenull151 fun getScanMode(): Int = bluetoothAdapter.scanMode 152 153 /** Return true if the remote device is connected to the local adapter. */ 154 fun isConnected(): Boolean = remoteDeviceConnectionState == BluetoothAdapter.STATE_CONNECTED 155 156 /** Return true if the remote device is bonded (paired) to the local adapter. */ 157 fun isPaired(): Boolean = bluetoothAdapter.bondedDevices.contains(remoteDevice) 158 159 /** Gets the A2DP sink profile proxy. */ 160 fun getA2DPSinkProfileProxy(): BluetoothProfile? = a2dpSinkProxy 161 162 /** 163 * Callback method for receiving Intent broadcast of Bluetooth state. 164 * 165 * See [BroadcastReceiver#onReceive]. 166 * 167 * @param context the Context in which the receiver is running. 168 * @param intent the Intent being received. 169 */ 170 override fun onReceive(context: Context, intent: Intent) { 171 when (intent.action) { 172 BluetoothDevice.ACTION_BOND_STATE_CHANGED -> { 173 // After a device starts bonding, we only pay attention to intents about that device. 174 val device = 175 intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE) 176 val bondState = 177 intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR) 178 remoteDevice = 179 when (bondState) { 180 BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED -> device 181 BluetoothDevice.BOND_NONE -> null 182 else -> remoteDevice 183 } 184 mLogger.log( 185 "ACTION_BOND_STATE_CHANGED, the bound state of " + 186 "the remote device (%s) change to %s.", 187 remoteDevice?.remoteDeviceToString(), 188 bondState.bondStateToString() 189 ) 190 listener.onBondStateChanged(bondState) 191 } 192 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> { 193 remoteDeviceConnectionState = 194 intent.getIntExtra( 195 BluetoothAdapter.EXTRA_CONNECTION_STATE, 196 BluetoothAdapter.STATE_DISCONNECTED 197 ) 198 mLogger.log( 199 "ACTION_CONNECTION_STATE_CHANGED, the new connectionState: %s", 200 remoteDeviceConnectionState 201 ) 202 listener.onConnectionStateChanged(remoteDeviceConnectionState) 203 } 204 BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> { 205 val scanMode = 206 intent.getIntExtra( 207 BluetoothAdapter.EXTRA_SCAN_MODE, 208 BluetoothAdapter.SCAN_MODE_NONE 209 ) 210 mLogger.log( 211 "ACTION_SCAN_MODE_CHANGED, the new scanMode: %s", 212 FastPairSimulator.scanModeToString(scanMode) 213 ) 214 listener.onScanModeChange(scanMode) 215 } 216 else -> {} 217 } 218 } 219 waitForBluetoothStatenull220 private fun waitForBluetoothState(state: Int) { 221 while (bluetoothAdapter.state != state) { 222 SystemClock.sleep(1000) 223 } 224 } 225 remoteDeviceToStringnull226 private fun BluetoothDevice.remoteDeviceToString(): String = "${this.name}-${this.address}" 227 228 private fun Int.bondStateToString(): String = 229 when (this) { 230 BluetoothDevice.BOND_NONE -> "BOND_NONE" 231 BluetoothDevice.BOND_BONDING -> "BOND_BONDING" 232 BluetoothDevice.BOND_BONDED -> "BOND_BONDED" 233 else -> "BOND_ERROR" 234 } 235 236 /** Interface for listening the events from Bluetooth controller. */ 237 interface EventListener { 238 /** The callback for the first onServiceConnected of A2DP sink profile. */ onA2DPSinkProfileConnectednull239 fun onA2DPSinkProfileConnected() 240 241 /** 242 * Reports the current bond state of the remote device. 243 * 244 * @param bondState the bond state of the remote device. 245 */ 246 fun onBondStateChanged(bondState: Int) 247 248 /** 249 * Reports the current connection state of the remote device. 250 * 251 * @param connectionState the bond state of the remote device. 252 */ 253 fun onConnectionStateChanged(connectionState: Int) 254 255 /** 256 * Reports the current scan mode of the local Adapter. 257 * 258 * @param mode the current scan mode of the local Adapter. 259 */ 260 fun onScanModeChange(mode: Int) 261 } 262 263 companion object { 264 private const val TAG = "BluetoothController" 265 266 private const val TURN_AIRPLANE_MODE_OFF = 0 267 private const val TURN_AIRPLANE_MODE_ON = 1 268 } 269 } 270