• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 }