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 both classic Bluetooth and BLE 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 * @param ioCapabilityBLE 61 * ``` 62 * One of {@link #IO_CAPABILITY_IO}, {@link #IO_CAPABILITY_NONE}, {@link 63 * ``` 64 * #IO_CAPABILITY_KBDISP} or more in {@link BluetoothAdapter}. 65 * ``` 66 */ setIoCapabilitynull67 fun setIoCapability(ioCapabilityClassic: Int, ioCapabilityBLE: Int) { 68 bluetoothAdapter.ioCapability = ioCapabilityClassic 69 bluetoothAdapter.leIoCapability = ioCapabilityBLE 70 71 // Toggling airplane mode on/off to restart Bluetooth stack and reset the BLE. 72 try { 73 Settings.Global.putInt( 74 context.contentResolver, 75 Settings.Global.AIRPLANE_MODE_ON, 76 TURN_AIRPLANE_MODE_ON 77 ) 78 } catch (expectedOnNonCustomAndroid: SecurityException) { 79 mLogger.log( 80 expectedOnNonCustomAndroid, 81 "Requires custom Android to toggle airplane mode" 82 ) 83 // Fall back to turn off Bluetooth. 84 bluetoothAdapter.disable() 85 } 86 waitForBluetoothState(BluetoothAdapter.STATE_OFF) 87 try { 88 Settings.Global.putInt( 89 context.contentResolver, 90 Settings.Global.AIRPLANE_MODE_ON, 91 TURN_AIRPLANE_MODE_OFF 92 ) 93 } catch (expectedOnNonCustomAndroid: SecurityException) { 94 mLogger.log( 95 expectedOnNonCustomAndroid, 96 "SecurityException while toggled airplane mode." 97 ) 98 } finally { 99 // Double confirm that Bluetooth is turned on. 100 bluetoothAdapter.enable() 101 } 102 waitForBluetoothState(BluetoothAdapter.STATE_ON) 103 } 104 105 /** Registers this Bluetooth state change receiver. */ registerBluetoothStateReceivernull106 fun registerBluetoothStateReceiver() { 107 val bondStateFilter = 108 IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED).apply { 109 addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED) 110 addAction(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED) 111 } 112 context.registerReceiver( 113 this, 114 bondStateFilter, 115 /* broadcastPermission= */ null, 116 /* scheduler= */ null 117 ) 118 } 119 120 /** Unregisters this Bluetooth state change receiver. */ unregisterBluetoothStateReceivernull121 fun unregisterBluetoothStateReceiver() { 122 context.unregisterReceiver(this) 123 } 124 125 /** Clears current remote device. */ clearRemoteDevicenull126 fun clearRemoteDevice() { 127 remoteDevice = null 128 } 129 130 /** Gets current remote device. */ getRemoteDevicenull131 fun getRemoteDevice(): BluetoothDevice? = remoteDevice 132 133 /** Gets current remote device as string. */ 134 fun getRemoteDeviceAsString(): String = remoteDevice?.remoteDeviceToString() ?: "none" 135 136 /** Connects the Bluetooth A2DP sink profile service. */ 137 fun connectA2DPSinkProfile() { 138 // Get the A2DP proxy before continuing with initialization. 139 bluetoothAdapter.getProfileProxy( 140 context, 141 object : BluetoothProfile.ServiceListener { 142 override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { 143 // When Bluetooth turns off and then on again, this is called again. But we only care 144 // the first time. There doesn't seem to be a way to unregister our listener. 145 if (a2dpSinkProxy == null) { 146 a2dpSinkProxy = proxy 147 listener.onA2DPSinkProfileConnected() 148 } 149 } 150 151 override fun onServiceDisconnected(profile: Int) {} 152 }, 153 BluetoothProfile.A2DP_SINK 154 ) 155 } 156 157 /** Get the current Bluetooth scan mode of the local Bluetooth adapter. */ getScanModenull158 fun getScanMode(): Int = bluetoothAdapter.scanMode 159 160 /** Return true if the remote device is connected to the local adapter. */ 161 fun isConnected(): Boolean = remoteDeviceConnectionState == BluetoothAdapter.STATE_CONNECTED 162 163 /** Return true if the remote device is bonded (paired) to the local adapter. */ 164 fun isPaired(): Boolean = bluetoothAdapter.bondedDevices.contains(remoteDevice) 165 166 /** Gets the A2DP sink profile proxy. */ 167 fun getA2DPSinkProfileProxy(): BluetoothProfile? = a2dpSinkProxy 168 169 /** 170 * Callback method for receiving Intent broadcast of Bluetooth state. 171 * 172 * See [BroadcastReceiver#onReceive]. 173 * 174 * @param context the Context in which the receiver is running. 175 * @param intent the Intent being received. 176 */ 177 override fun onReceive(context: Context, intent: Intent) { 178 when (intent.action) { 179 BluetoothDevice.ACTION_BOND_STATE_CHANGED -> { 180 // After a device starts bonding, we only pay attention to intents about that device. 181 val device = 182 intent.getParcelableExtra<BluetoothDevice>(BluetoothDevice.EXTRA_DEVICE) 183 val bondState = 184 intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.ERROR) 185 remoteDevice = 186 when (bondState) { 187 BluetoothDevice.BOND_BONDING, BluetoothDevice.BOND_BONDED -> device 188 BluetoothDevice.BOND_NONE -> null 189 else -> remoteDevice 190 } 191 mLogger.log( 192 "ACTION_BOND_STATE_CHANGED, the bound state of " + 193 "the remote device (%s) change to %s.", 194 remoteDevice?.remoteDeviceToString(), 195 bondState.bondStateToString() 196 ) 197 listener.onBondStateChanged(bondState) 198 } 199 BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED -> { 200 remoteDeviceConnectionState = 201 intent.getIntExtra( 202 BluetoothAdapter.EXTRA_CONNECTION_STATE, 203 BluetoothAdapter.STATE_DISCONNECTED 204 ) 205 mLogger.log( 206 "ACTION_CONNECTION_STATE_CHANGED, the new connectionState: %s", 207 remoteDeviceConnectionState 208 ) 209 listener.onConnectionStateChanged(remoteDeviceConnectionState) 210 } 211 BluetoothAdapter.ACTION_SCAN_MODE_CHANGED -> { 212 val scanMode = 213 intent.getIntExtra( 214 BluetoothAdapter.EXTRA_SCAN_MODE, 215 BluetoothAdapter.SCAN_MODE_NONE 216 ) 217 mLogger.log( 218 "ACTION_SCAN_MODE_CHANGED, the new scanMode: %s", 219 FastPairSimulator.scanModeToString(scanMode) 220 ) 221 listener.onScanModeChange(scanMode) 222 } 223 else -> {} 224 } 225 } 226 waitForBluetoothStatenull227 private fun waitForBluetoothState(state: Int) { 228 while (bluetoothAdapter.state != state) { 229 SystemClock.sleep(1000) 230 } 231 } 232 remoteDeviceToStringnull233 private fun BluetoothDevice.remoteDeviceToString(): String = "${this.name}-${this.address}" 234 235 private fun Int.bondStateToString(): String = 236 when (this) { 237 BluetoothDevice.BOND_NONE -> "BOND_NONE" 238 BluetoothDevice.BOND_BONDING -> "BOND_BONDING" 239 BluetoothDevice.BOND_BONDED -> "BOND_BONDED" 240 else -> "BOND_ERROR" 241 } 242 243 /** Interface for listening the events from Bluetooth controller. */ 244 interface EventListener { 245 /** The callback for the first onServiceConnected of A2DP sink profile. */ onA2DPSinkProfileConnectednull246 fun onA2DPSinkProfileConnected() 247 248 /** 249 * Reports the current bond state of the remote device. 250 * 251 * @param bondState the bond state of the remote device. 252 */ 253 fun onBondStateChanged(bondState: Int) 254 255 /** 256 * Reports the current connection state of the remote device. 257 * 258 * @param connectionState the bond state of the remote device. 259 */ 260 fun onConnectionStateChanged(connectionState: Int) 261 262 /** 263 * Reports the current scan mode of the local Adapter. 264 * 265 * @param mode the current scan mode of the local Adapter. 266 */ 267 fun onScanModeChange(mode: Int) 268 } 269 270 companion object { 271 private const val TAG = "BluetoothController" 272 273 private const val TURN_AIRPLANE_MODE_OFF = 0 274 private const val TURN_AIRPLANE_MODE_ON = 1 275 } 276 }