1 /* 2 * Copyright (C) 2021 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 com.google.android.tv.btservices.pairing; 18 19 import android.bluetooth.BluetoothA2dp; 20 import android.bluetooth.BluetoothAdapter; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.BluetoothHidHost; 23 import android.bluetooth.BluetoothProfile; 24 import android.bluetooth.BluetoothProfile.ServiceListener; 25 import android.content.BroadcastReceiver; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.os.Handler; 30 import android.os.Looper; 31 import android.os.Message; 32 import android.text.TextUtils; 33 import android.util.Log; 34 35 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapper; 36 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapperA2dp; 37 import com.google.android.tv.btservices.pairing.profiles.PairingProfileWrapperHidHost; 38 39 public final class BluetoothPairer { 40 41 private static final String TAG = "Atv.BtPairer"; 42 43 // A typical device pairing process will proceed in order or bond, service discovery, and 44 // profile connection. 45 46 public static final int STATUS_ERROR = -1; 47 public static final int STATUS_INIT = 0; 48 public static final int STATUS_PAIRING = 3; 49 public static final int STATUS_CONNECTING = 4; 50 public static final int STATUS_DONE = 5; 51 public static final int STATUS_CANCELLED = 6; 52 public static final int STATUS_TIMEOUT = 7; 53 54 private static final int MSG_PAIR = 1; 55 private static final int MSG_ADD_DEVICE = 2; 56 private static final int ADD_DEVICE_RETRY_MS = 1000; 57 private static final int ADD_DEVICE_DELAY = 1000; 58 59 private static final int MSG_TIMEOUT = 3; 60 private static final int PAIRING_TIMEOUT_MS = 25000; 61 private static final int CONNECTING_TIMEOUT_MS = 15000; 62 63 private final Context context; 64 private Listener mListener; 65 private boolean mForgetOnFail = true; 66 private BluetoothDevice mDevice; 67 private int mBluetoothProfile; 68 private PairingProfileWrapper mPairingProfileWrapper; 69 70 private int status = STATUS_INIT; 71 72 protected interface Listener { onStatusChanged(BluetoothDevice device, int status)73 void onStatusChanged(BluetoothDevice device, int status); 74 } 75 76 private final Handler mHandler = new Handler(Looper.getMainLooper()) { 77 @Override 78 public void handleMessage(Message msg) { 79 switch (msg.what) { 80 case MSG_PAIR: 81 startBonding(); 82 break; 83 case MSG_ADD_DEVICE: 84 addDevice(); 85 break; 86 case MSG_TIMEOUT: 87 timeout(); 88 break; 89 default: 90 Log.i(TAG, "No handler case available for message: " + msg.what); 91 } 92 } 93 }; 94 95 private final BroadcastReceiver linkStatusReceiver = new BroadcastReceiver() { 96 @Override 97 public void onReceive(Context context, Intent intent) { 98 final String action = intent.getAction(); 99 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 100 onBondStateChanged(intent); 101 } else if (getBroadcastListeningState(mBluetoothProfile).equals(action)) { 102 onConnectionStateChanged(intent); 103 } else if (BluetoothDevice.ACTION_UUID.equals(intent.getAction())) { 104 onServicesDiscovered(); 105 } 106 } 107 }; 108 BluetoothPairer(Context context, int bluetoothProfile)109 protected BluetoothPairer(Context context, int bluetoothProfile) { 110 this.context = context.getApplicationContext(); 111 if (bluetoothProfile != BluetoothProfile.A2DP 112 && bluetoothProfile != BluetoothProfile.HID_HOST) { 113 throw new IllegalArgumentException(bluetoothProfile + " is not a supported profile"); 114 } 115 mBluetoothProfile = bluetoothProfile; 116 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 117 ServiceListener serviceConnection = 118 new BluetoothProfile.ServiceListener() { 119 @Override 120 public void onServiceDisconnected(int profile) { 121 // TODO handle unexpected disconnection 122 Log.i(TAG, "Service disconnected, perhaps unexpectedly"); 123 mPairingProfileWrapper = null; 124 } 125 126 @Override 127 public void onServiceConnected(int profile, BluetoothProfile proxy) { 128 Log.i(TAG, "Connection made to bluetooth proxy."); 129 mPairingProfileWrapper = getPairingProfileWrapper(profile, proxy); 130 } 131 }; 132 adapter.getProfileProxy(this.context, serviceConnection, bluetoothProfile); 133 registerReceiver(); 134 } 135 getPairingProfileWrapper(int bluetoothProfile, BluetoothProfile proxy)136 private static PairingProfileWrapper getPairingProfileWrapper(int bluetoothProfile, 137 BluetoothProfile proxy) { 138 if (BluetoothProfile.A2DP == bluetoothProfile) { 139 return new PairingProfileWrapperA2dp(proxy); 140 } else if (BluetoothProfile.HID_HOST == bluetoothProfile) { 141 return new PairingProfileWrapperHidHost(proxy); 142 } else { 143 return null; 144 } 145 } 146 147 // Bond and add the device as input after bonded. startPairing(BluetoothDevice device, Listener listener, boolean forgetOnFail)148 protected void startPairing(BluetoothDevice device, Listener listener, boolean forgetOnFail) { 149 Log.i(TAG, "startPairing: " + device.getAddress() + " " + device.getName()); 150 if (isInProgress()) { 151 Log.e(TAG, "Pairing already in progress, you must cancel the " 152 + "previous request first"); 153 return; 154 } 155 mForgetOnFail = forgetOnFail; 156 mDevice = device; 157 mListener = listener; 158 if (device.getBondState() == BluetoothDevice.BOND_BONDED) { 159 Log.i(TAG, "Already bonded " + device); 160 onBonded(); 161 return; 162 } 163 mHandler.removeMessages(MSG_PAIR); 164 mHandler.sendEmptyMessage(MSG_PAIR); 165 mHandler.removeMessages(MSG_TIMEOUT); 166 mHandler.sendEmptyMessageDelayed(MSG_TIMEOUT, PAIRING_TIMEOUT_MS); 167 } 168 dispose()169 protected void dispose() { 170 if (isInProgress()) { 171 doCancel(STATUS_CANCELLED); 172 } 173 mHandler.removeCallbacksAndMessages(null); 174 unregisterReceiver(); 175 if (mPairingProfileWrapper != null) { 176 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 177 adapter.closeProfileProxy(mBluetoothProfile, mPairingProfileWrapper.getProxy()); 178 } 179 } 180 registerReceiver()181 private void registerReceiver() { 182 IntentFilter filter = new IntentFilter(); 183 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 184 filter.addAction(BluetoothDevice.ACTION_UUID); 185 filter.addAction(getBroadcastListeningState(mBluetoothProfile)); 186 context.registerReceiver(linkStatusReceiver, filter); 187 } 188 getBroadcastListeningState(int bluetoothProfile)189 private static String getBroadcastListeningState(int bluetoothProfile) { 190 if (BluetoothProfile.A2DP == bluetoothProfile) { 191 return BluetoothA2dp.ACTION_CONNECTION_STATE_CHANGED; 192 } else if (BluetoothProfile.HID_HOST == bluetoothProfile) { 193 return BluetoothHidHost.ACTION_CONNECTION_STATE_CHANGED; 194 } else { 195 return null; 196 } 197 } 198 unregisterReceiver()199 private void unregisterReceiver() { 200 context.unregisterReceiver(linkStatusReceiver); 201 } 202 isInProgress()203 private boolean isInProgress() { 204 return status != STATUS_INIT && status != STATUS_ERROR && status != STATUS_CANCELLED 205 && status != STATUS_DONE && status != STATUS_TIMEOUT; 206 } 207 onBondStateChanged(Intent intent)208 private void onBondStateChanged(Intent intent) { 209 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 210 if (!device.equals(mDevice)) { 211 return; 212 } 213 214 int bondState = 215 intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, BluetoothDevice.BOND_NONE); 216 int previousBondState = intent.getIntExtra( 217 BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE); 218 if (bondState == BluetoothDevice.BOND_NONE && 219 previousBondState == BluetoothDevice.BOND_BONDING) { 220 onBondFailed(); 221 } else if (bondState == BluetoothDevice.BOND_BONDED) { 222 onBonded(); 223 } 224 } 225 onConnectionStateChanged(Intent intent)226 private void onConnectionStateChanged(Intent intent) { 227 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 228 int state = intent.getIntExtra(BluetoothProfile.EXTRA_STATE, -1); 229 if (mDevice == null || !mDevice.equals(device)) { 230 Log.e(TAG, "addAsInput (handler): non-device connecting: " + device); 231 return; 232 } 233 switch (state) { 234 case BluetoothProfile.STATE_CONNECTED: 235 mHandler.post(BluetoothPairer.this::onAdded); 236 break; 237 case BluetoothProfile.STATE_DISCONNECTED: 238 mHandler.post(BluetoothPairer.this::onAddFailed); 239 break; 240 case BluetoothProfile.STATE_CONNECTING: 241 case BluetoothProfile.STATE_DISCONNECTING: 242 Log.i(TAG, "addAsInput (handler): no action for transient states"); 243 break; 244 default: 245 Log.e(TAG, "addAsInput (handler): unknown state " + state); 246 } 247 } 248 onServicesDiscovered()249 private void onServicesDiscovered() { 250 // regardless of the UUID content, at this point, we're sure we can initiate a 251 // profile connection. 252 if (!mHandler.hasMessages(MSG_ADD_DEVICE)) { 253 mHandler.sendEmptyMessageDelayed(MSG_ADD_DEVICE, ADD_DEVICE_DELAY); 254 } 255 } 256 startBonding()257 private void startBonding() { 258 updateStatus(STATUS_PAIRING); 259 if (mDevice.getBondState() != BluetoothDevice.BOND_BONDED) { 260 if (!mDevice.createBond()) { 261 onBondFailed(); 262 } 263 } else { 264 onBonded(); 265 } 266 } 267 268 // Open a connection to the profile host. onBonded()269 private void onBonded() { 270 mHandler.removeMessages(MSG_TIMEOUT); 271 mHandler.sendEmptyMessageDelayed(MSG_TIMEOUT, CONNECTING_TIMEOUT_MS); 272 serviceDiscovery(); 273 } 274 onBondFailed()275 private void onBondFailed() { 276 Log.e(TAG, "There was an error bonding with the device."); 277 updateStatus(STATUS_ERROR); 278 unpairDevice(mDevice); 279 mDevice = null; 280 } 281 282 // For a2dp devices, we must complete discovery before initiating a profile connection. 283 // See b/135210487. serviceDiscovery()284 private void serviceDiscovery() { 285 if (mDevice == null) { 286 Log.e(TAG, "serviceDiscovery(): mDevice is null"); 287 return; 288 } 289 mDevice.fetchUuidsWithSdp(); 290 } 291 292 // Adding as input = CONNECTING addDevice()293 private void addDevice() { 294 if (mDevice == null) { 295 Log.i(TAG, "No device to add"); 296 mHandler.post(this::onAddFailed); 297 return; 298 } 299 if (mPairingProfileWrapper == null) { 300 Log.i(TAG, "No Bluetooth proxy"); 301 mHandler.removeMessages(MSG_ADD_DEVICE); 302 mHandler.sendEmptyMessageDelayed(MSG_ADD_DEVICE, ADD_DEVICE_RETRY_MS); 303 return; 304 } 305 updateStatus(STATUS_CONNECTING); 306 if (isDeviceAdded()) { 307 mHandler.post(this::onAdded); 308 return; 309 } 310 mPairingProfileWrapper.connect(mDevice); 311 } 312 onAdded()313 private void onAdded() { 314 updateStatus(STATUS_DONE); 315 mHandler.removeMessages(MSG_TIMEOUT); 316 mDevice = null; 317 } 318 onAddFailed()319 private void onAddFailed() { 320 Log.e(TAG, "There was an error adding the device as input."); 321 updateStatus(STATUS_ERROR); 322 mHandler.removeMessages(MSG_TIMEOUT); 323 unpairDevice(mDevice); 324 mDevice = null; 325 } 326 updateStatus(int status)327 private void updateStatus(int status) { 328 this.status = status; 329 if (mListener != null) { 330 mListener.onStatusChanged(mDevice, status); 331 } 332 } 333 isDeviceAdded()334 private boolean isDeviceAdded() { 335 if (mPairingProfileWrapper == null) { 336 return false; 337 } 338 if (mDevice == null) { 339 return false; 340 } 341 for (BluetoothDevice device : mPairingProfileWrapper.getConnectedDevices()) { 342 Log.i(TAG, "Device connected: " + device); 343 if (TextUtils.equals(device.getAddress(), mDevice.getAddress())) { 344 return true; 345 } 346 } 347 return false; 348 } 349 doCancel(int status)350 private void doCancel(int status) { 351 if (isInProgress()) { 352 Log.i(TAG, "Pairing process has already begun, forcing cancel anyway."); 353 updateStatus(status); 354 } 355 mHandler.removeMessages(MSG_TIMEOUT); 356 mHandler.removeMessages(MSG_PAIR); 357 if (mDevice != null && mPairingProfileWrapper != null) { 358 mPairingProfileWrapper.disconnect(mDevice); 359 } 360 unpairDevice(mDevice); 361 mDevice = null; 362 updateStatus(STATUS_ERROR); 363 } 364 unpairDevice(BluetoothDevice device)365 private void unpairDevice(BluetoothDevice device) { 366 if (device == null) { 367 return; 368 } 369 if (BluetoothDevice.BOND_BONDING == device.getBondState()) { 370 device.cancelBondProcess(); 371 } 372 if (mForgetOnFail) { 373 final boolean successful = device.removeBond(); 374 if (successful) { 375 Log.i(TAG, "Bluetooth device successfully unpaired: " + device.getName()); 376 } else { 377 Log.i(TAG, "Failed to unpair device: " + device.getName()); 378 } 379 } 380 } 381 timeout()382 private void timeout() { 383 Log.e(TAG, "Bluetooth pairing timed out, cancelling..."); 384 doCancel(STATUS_TIMEOUT); 385 } 386 } 387