• 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 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