1 /* 2 * Copyright (C) 2014 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.android.tv.settings.accessories; 18 19 import android.bluetooth.BluetoothAdapter; 20 import android.bluetooth.BluetoothClass; 21 import android.bluetooth.BluetoothDevice; 22 import android.bluetooth.IBluetoothA2dp; 23 import android.content.BroadcastReceiver; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.IntentFilter; 28 import android.content.pm.PackageManager; 29 import android.hardware.input.InputManager; 30 import android.os.Handler; 31 import android.os.Message; 32 import android.os.SystemClock; 33 import android.util.Log; 34 import android.view.InputDevice; 35 36 import com.android.tv.settings.util.bluetooth.BluetoothDeviceCriteria; 37 import com.android.tv.settings.util.bluetooth.BluetoothScanner; 38 39 import java.util.ArrayList; 40 import java.util.List; 41 42 /** 43 * Monitors available Bluetooth devices and manages process of pairing 44 * and connecting to the device. 45 */ 46 public class BluetoothDevicePairer { 47 48 /** 49 * This class operates in two modes, automatic and manual. 50 * 51 * AUTO MODE 52 * In auto mode we listen for an input device that looks like it can 53 * generate DPAD events. When one is found we wait 54 * {@link #DELAY_AUTO_PAIRING} milliseconds before starting the process of 55 * connecting to the device. The idea is that a UI making use of this class 56 * would give the user a chance to cancel pairing during this window. Once 57 * the connection process starts, it is considered uninterruptible. 58 * 59 * Connection is accomplished in two phases, bonding and socket connection. 60 * First we try to create a bond to the device and listen for bond status 61 * change broadcasts. Once the bond is made, we connect to the device. 62 * Connecting to the device actually opens a socket and hooks the device up 63 * to the input system. 64 * 65 * In auto mode if we see more than one compatible input device before 66 * bonding with a candidate device, we stop the process. We don't want to 67 * connect to the wrong device and it is up to the user of this class to 68 * tell us what to connect to. 69 * 70 * MANUAL MODE 71 * Manual mode is where a user of this class explicitly tells us which 72 * device to connect to. To switch to manual mode you can call 73 * {@link #cancelPairing()}. It is safe to call this method even if no 74 * device connection process is underway. You would then call 75 * {@link #start()} to resume scanning for devices. Once one is found 76 * that you want to connect to, call {@link #startPairing(BluetoothDevice)} 77 * to start the connection process. At this point the same process is 78 * followed as when we start connection in auto mode. 79 * 80 * Even in manual mode there is a timeout before we actually start 81 * connecting, but it is {@link #DELAY_MANUAL_PAIRING}. 82 */ 83 84 public static final String TAG = "BluetoothDevicePairer"; 85 public static final int STATUS_ERROR = -1; 86 public static final int STATUS_NONE = 0; 87 public static final int STATUS_SCANNING = 1; 88 /** 89 * A device to pair with has been identified, we're currently in the 90 * timeout period where the process can be cancelled. 91 */ 92 public static final int STATUS_WAITING_TO_PAIR = 2; 93 /** 94 * Pairing is in progress. 95 */ 96 public static final int STATUS_PAIRING = 3; 97 /** 98 * Device has been paired with, we are opening a connection to the device. 99 */ 100 public static final int STATUS_CONNECTING = 4; 101 102 103 public interface EventListener { 104 /** 105 * The status of the {@link BluetoothDevicePairer} changed. 106 */ statusChanged()107 void statusChanged(); 108 } 109 110 public interface BluetoothConnector { openConnection(BluetoothAdapter adapter)111 void openConnection(BluetoothAdapter adapter); 112 } 113 114 public interface OpenConnectionCallback { 115 /** 116 * Call back when BT device connection is completed. 117 */ succeeded()118 void succeeded(); failed()119 void failed(); 120 } 121 122 /** 123 * Time between when a single input device is found and pairing begins. If 124 * one or more other input devices are found before this timeout or 125 * {@link #cancelPairing()} is called then pairing will not proceed. 126 */ 127 public static final int DELAY_AUTO_PAIRING = 15 * 1000; 128 /** 129 * Time between when the call to {@link #startPairing(BluetoothDevice)} is 130 * called and when we actually start pairing. This gives the caller a 131 * chance to change their mind. 132 */ 133 public static final int DELAY_MANUAL_PAIRING = 5 * 1000; 134 /** 135 * If there was an error in pairing, we will wait this long before trying 136 * again. 137 */ 138 public static final int DELAY_RETRY = 5 * 1000; 139 140 private static final int MSG_PAIR = 1; 141 private static final int MSG_START = 2; 142 143 private static final boolean DEBUG = true; 144 145 private static final String[] INVALID_INPUT_KEYBOARD_DEVICE_NAMES = { 146 "gpio-keypad", "cec_keyboard", "Virtual", "athome_remote" 147 }; 148 149 private final BluetoothScanner.Listener mBtListener = new BluetoothScanner.Listener() { 150 @Override 151 public void onDeviceAdded(BluetoothScanner.Device device) { 152 if (DEBUG) { 153 Log.d(TAG, "Adding device: " + device.btDevice.getAddress()); 154 } 155 onDeviceFound(device.btDevice); 156 } 157 158 @Override 159 public void onDeviceRemoved(BluetoothScanner.Device device) { 160 if (DEBUG) { 161 Log.d(TAG, "Device lost: " + device.btDevice.getAddress()); 162 } 163 onDeviceLost(device.btDevice); 164 } 165 }; 166 hasValidInputDevice(Context context, int[] deviceIds)167 public static boolean hasValidInputDevice(Context context, int[] deviceIds) { 168 InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE); 169 170 for (int ptr = deviceIds.length - 1; ptr > -1; ptr--) { 171 InputDevice device = inMan.getInputDevice(deviceIds[ptr]); 172 int sources = device.getSources(); 173 174 boolean isCompatible = false; 175 176 if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) { 177 isCompatible = true; 178 } 179 180 if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { 181 isCompatible = true; 182 } 183 184 if ((sources & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) { 185 boolean isValidKeyboard = true; 186 String keyboardName = device.getName(); 187 for (int index = 0; index < INVALID_INPUT_KEYBOARD_DEVICE_NAMES.length; ++index) { 188 if (keyboardName.equals(INVALID_INPUT_KEYBOARD_DEVICE_NAMES[index])) { 189 isValidKeyboard = false; 190 break; 191 } 192 } 193 194 if (isValidKeyboard) { 195 isCompatible = true; 196 } 197 } 198 199 if (!device.isVirtual() && isCompatible) { 200 return true; 201 } 202 } 203 return false; 204 } 205 hasValidInputDevice(Context context)206 public static boolean hasValidInputDevice(Context context) { 207 InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE); 208 int[] inputDevices = inMan.getInputDeviceIds(); 209 210 return hasValidInputDevice(context, inputDevices); 211 } 212 213 private final BroadcastReceiver mLinkStatusReceiver = new BroadcastReceiver() { 214 @Override 215 public void onReceive(Context context, Intent intent) { 216 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 217 if (DEBUG) { 218 Log.d(TAG, "There was a link status change for: " + device.getAddress()); 219 } 220 221 if (device.equals(mTarget)) { 222 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 223 BluetoothDevice.BOND_NONE); 224 int previousBondState = intent.getIntExtra( 225 BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE); 226 227 if (DEBUG) { 228 Log.d(TAG, "Bond states: old = " + previousBondState + ", new = " + 229 bondState); 230 } 231 232 if (bondState == BluetoothDevice.BOND_NONE && 233 previousBondState == BluetoothDevice.BOND_BONDING) { 234 // we seem to have reverted, this is an error 235 // TODO inform user, start scanning again 236 unregisterLinkStatusReceiver(); 237 onBondFailed(); 238 } else if (bondState == BluetoothDevice.BOND_BONDED) { 239 unregisterLinkStatusReceiver(); 240 onBonded(); 241 } 242 } 243 } 244 }; 245 246 private BroadcastReceiver mBluetoothStateReceiver; 247 248 private final OpenConnectionCallback mOpenConnectionCallback = new OpenConnectionCallback() { 249 public void succeeded() { 250 setStatus(STATUS_NONE); 251 } 252 public void failed() { 253 setStatus(STATUS_ERROR); 254 } 255 }; 256 257 private final Context mContext; 258 private EventListener mListener; 259 private int mStatus = STATUS_NONE; 260 /** 261 * Set to {@code false} when {@link #cancelPairing()} or 262 * {@link #startPairing(BluetoothDevice)}. This instance 263 * will now no longer automatically start pairing. 264 */ 265 private boolean mAutoMode = true; 266 private final ArrayList<BluetoothDevice> mVisibleDevices = new ArrayList<>(); 267 private BluetoothDevice mTarget; 268 private final Handler mHandler; 269 private long mNextStageTimestamp = -1; 270 private boolean mLinkReceiverRegistered = false; 271 private final ArrayList<BluetoothDeviceCriteria> mBluetoothDeviceCriteria = new ArrayList<>(); 272 private InputDeviceCriteria mInputDeviceCriteria; 273 274 /** 275 * Should be instantiated on a thread with a Looper, perhaps the main thread! 276 */ BluetoothDevicePairer(Context context, EventListener listener)277 public BluetoothDevicePairer(Context context, EventListener listener) { 278 mContext = context.getApplicationContext(); 279 mListener = listener; 280 281 addBluetoothDeviceCriteria(); 282 283 mHandler = new Handler() { 284 @Override 285 public void handleMessage(Message msg) { 286 switch (msg.what) { 287 case MSG_PAIR: 288 startBonding(); 289 break; 290 case MSG_START: 291 start(); 292 break; 293 default: 294 Log.d(TAG, "No handler case available for message: " + msg.what); 295 } 296 } 297 }; 298 } 299 addBluetoothDeviceCriteria()300 private void addBluetoothDeviceCriteria() { 301 // Input is supported by all devices. 302 mInputDeviceCriteria = new InputDeviceCriteria(); 303 mBluetoothDeviceCriteria.add(mInputDeviceCriteria); 304 305 // Add Bluetooth a2dp on if the service is running and the 306 // setting profile_supported_a2dp is set to true. 307 Intent intent = new Intent(IBluetoothA2dp.class.getName()); 308 ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); 309 if (comp != null) { 310 int enabledState = mContext.getPackageManager().getComponentEnabledSetting(comp); 311 if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { 312 Log.d(TAG, "Adding A2dp device criteria for pairing"); 313 mBluetoothDeviceCriteria.add(new A2dpDeviceCriteria()); 314 } 315 } 316 } 317 318 /** 319 * Start listening for devices and begin the pairing process when 320 * criteria is met. 321 */ start()322 public void start() { 323 final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 324 if (!bluetoothAdapter.isEnabled()) { 325 Log.d(TAG, "Bluetooth not enabled, delaying startup."); 326 if (mBluetoothStateReceiver == null) { 327 mBluetoothStateReceiver = new BroadcastReceiver() { 328 @Override 329 public void onReceive(Context context, Intent intent) { 330 if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 331 BluetoothAdapter.STATE_OFF) == BluetoothAdapter.STATE_ON) { 332 Log.d(TAG, "Bluetooth now enabled, starting."); 333 start(); 334 } else { 335 Log.d(TAG, "Bluetooth not yet started, got broadcast: " + intent); 336 } 337 } 338 }; 339 mContext.registerReceiver(mBluetoothStateReceiver, 340 new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 341 } 342 343 bluetoothAdapter.enable(); 344 return; 345 } else { 346 if (mBluetoothStateReceiver != null) { 347 mContext.unregisterReceiver(mBluetoothStateReceiver); 348 mBluetoothStateReceiver = null; 349 } 350 } 351 352 // set status to scanning before we start listening since 353 // startListening may result in a transition to STATUS_WAITING_TO_PAIR 354 // which might seem odd from a client perspective 355 setStatus(STATUS_SCANNING); 356 357 BluetoothScanner.startListening(mContext, mBtListener, mBluetoothDeviceCriteria); 358 } 359 clearDeviceList()360 public void clearDeviceList() { 361 doCancel(); 362 mVisibleDevices.clear(); 363 } 364 365 /** 366 * Stop any pairing request that is in progress. 367 */ cancelPairing()368 public void cancelPairing() { 369 mAutoMode = false; 370 doCancel(); 371 } 372 373 374 /** 375 * Switch to manual pairing mode. 376 */ disableAutoPairing()377 public void disableAutoPairing() { 378 mAutoMode = false; 379 } 380 381 /** 382 * Stop doing anything we're doing, release any resources. 383 */ dispose()384 public void dispose() { 385 mHandler.removeCallbacksAndMessages(null); 386 if (mLinkReceiverRegistered) { 387 unregisterLinkStatusReceiver(); 388 } 389 if (mBluetoothStateReceiver != null) { 390 mContext.unregisterReceiver(mBluetoothStateReceiver); 391 } 392 stopScanning(); 393 } 394 395 /** 396 * Start pairing and connection to the specified device. 397 * @param device device 398 */ startPairing(BluetoothDevice device)399 public void startPairing(BluetoothDevice device) { 400 startPairing(device, true); 401 } 402 403 /** 404 * Return our state 405 * @return One of the STATE_ constants. 406 */ getStatus()407 public int getStatus() { 408 return mStatus; 409 } 410 411 /** 412 * Get the device that we're currently targeting. This will be null if 413 * there is no device that is in the process of being connected to. 414 */ getTargetDevice()415 public BluetoothDevice getTargetDevice() { 416 return mTarget; 417 } 418 419 /** 420 * When the timer to start the next stage will expire, in {@link SystemClock#elapsedRealtime()}. 421 * Will only be valid while waiting to pair and after an error from which we are restarting. 422 */ getNextStageTime()423 public long getNextStageTime() { 424 return mNextStageTimestamp; 425 } 426 getAvailableDevices()427 public List<BluetoothDevice> getAvailableDevices() { 428 ArrayList<BluetoothDevice> copy = new ArrayList<>(mVisibleDevices.size()); 429 copy.addAll(mVisibleDevices); 430 return copy; 431 } 432 setListener(EventListener listener)433 public void setListener(EventListener listener) { 434 mListener = listener; 435 } 436 invalidateDevice(BluetoothDevice device)437 public void invalidateDevice(BluetoothDevice device) { 438 onDeviceLost(device); 439 } 440 startPairing(BluetoothDevice device, boolean isManual)441 private void startPairing(BluetoothDevice device, boolean isManual) { 442 // TODO check if we're already paired/bonded to this device 443 444 // cancel auto-mode if applicable 445 mAutoMode = !isManual; 446 447 mTarget = device; 448 449 if (isInProgress()) { 450 throw new RuntimeException("Pairing already in progress, you must cancel the " + 451 "previous request first"); 452 } 453 454 mHandler.removeCallbacksAndMessages(null); 455 456 mNextStageTimestamp = SystemClock.elapsedRealtime() + 457 (mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 458 mHandler.sendEmptyMessageDelayed(MSG_PAIR, 459 mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 460 461 setStatus(STATUS_WAITING_TO_PAIR); 462 } 463 464 /** 465 * Pairing is in progress and is no longer cancelable. 466 */ isInProgress()467 public boolean isInProgress() { 468 return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING && 469 mStatus != STATUS_WAITING_TO_PAIR; 470 } 471 updateListener()472 private void updateListener() { 473 if (mListener != null) { 474 mListener.statusChanged(); 475 } 476 } 477 onDeviceFound(BluetoothDevice device)478 private void onDeviceFound(BluetoothDevice device) { 479 if (!mVisibleDevices.contains(device)) { 480 mVisibleDevices.add(device); 481 Log.d(TAG, "Added device to visible list. Name = " + device.getName() + " , class = " + 482 device.getBluetoothClass().getDeviceClass()); 483 } else { 484 return; 485 } 486 487 updatePairingState(); 488 // update the listener because a new device is visible 489 updateListener(); 490 } 491 onDeviceLost(BluetoothDevice device)492 private void onDeviceLost(BluetoothDevice device) { 493 // TODO validate removal works as expected 494 if (mVisibleDevices.remove(device)) { 495 updatePairingState(); 496 // update the listener because a device disappeared 497 updateListener(); 498 } 499 } 500 updatePairingState()501 private void updatePairingState() { 502 if (mAutoMode) { 503 BluetoothDevice candidate = getAutoPairDevice(); 504 if (null != candidate) { 505 mTarget = candidate; 506 startPairing(mTarget, false); 507 } else { 508 doCancel(); 509 } 510 } 511 } 512 513 /** 514 * @return returns the only visible input device if there is only one 515 */ getAutoPairDevice()516 private BluetoothDevice getAutoPairDevice() { 517 List<BluetoothDevice> inputDevices = new ArrayList<>(); 518 for (BluetoothDevice device : mVisibleDevices) { 519 if (mInputDeviceCriteria.isInputDevice(device.getBluetoothClass())) { 520 inputDevices.add(device); 521 } 522 } 523 if (inputDevices.size() == 1) { 524 return inputDevices.get(0); 525 } 526 return null; 527 } 528 doCancel()529 private void doCancel() { 530 // TODO allow cancel to be called from any state 531 if (isInProgress()) { 532 Log.d(TAG, "Pairing process has already begun, it can not be canceled."); 533 return; 534 } 535 536 // stop scanning, just in case we are 537 final boolean wasListening = BluetoothScanner.stopListening(mBtListener); 538 BluetoothScanner.stopNow(); 539 540 mHandler.removeCallbacksAndMessages(null); 541 542 // remove bond, if existing 543 unpairDevice(mTarget); 544 545 mTarget = null; 546 547 setStatus(STATUS_NONE); 548 549 // resume scanning 550 if (wasListening) { 551 start(); 552 } 553 } 554 555 /** 556 * Set the status and update any listener. 557 */ setStatus(int status)558 private void setStatus(int status) { 559 mStatus = status; 560 updateListener(); 561 } 562 startBonding()563 private void startBonding() { 564 stopScanning(); 565 setStatus(STATUS_PAIRING); 566 if (mTarget.getBondState() != BluetoothDevice.BOND_BONDED) { 567 registerLinkStatusReceiver(); 568 569 // create bond (pair) to the device 570 mTarget.createBond(); 571 } else { 572 onBonded(); 573 } 574 } 575 onBonded()576 private void onBonded() { 577 openConnection(); 578 } 579 openConnection()580 private void openConnection() { 581 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 582 BluetoothConnector btConnector = getBluetoothConnector(); 583 if (btConnector != null) { 584 setStatus(STATUS_CONNECTING); 585 btConnector.openConnection(adapter); 586 } else { 587 Log.w(TAG, "There was an error getting the BluetoothConnector."); 588 setStatus(STATUS_ERROR); 589 if (mLinkReceiverRegistered) { 590 unregisterLinkStatusReceiver(); 591 } 592 unpairDevice(mTarget); 593 } 594 } 595 onBondFailed()596 private void onBondFailed() { 597 Log.w(TAG, "There was an error bonding with the device."); 598 setStatus(STATUS_ERROR); 599 600 // remove bond, if existing 601 unpairDevice(mTarget); 602 603 // TODO do we need to check Bluetooth for the device and possible delete it? 604 mNextStageTimestamp = SystemClock.elapsedRealtime() + DELAY_RETRY; 605 mHandler.sendEmptyMessageDelayed(MSG_START, DELAY_RETRY); 606 } 607 registerLinkStatusReceiver()608 private void registerLinkStatusReceiver() { 609 mLinkReceiverRegistered = true; 610 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 611 mContext.registerReceiver(mLinkStatusReceiver, filter); 612 } 613 unregisterLinkStatusReceiver()614 private void unregisterLinkStatusReceiver() { 615 mLinkReceiverRegistered = false; 616 mContext.unregisterReceiver(mLinkStatusReceiver); 617 } 618 stopScanning()619 private void stopScanning() { 620 BluetoothScanner.stopListening(mBtListener); 621 BluetoothScanner.stopNow(); 622 } 623 unpairDevice(BluetoothDevice device)624 public boolean unpairDevice(BluetoothDevice device) { 625 if (device != null) { 626 int state = device.getBondState(); 627 628 if (state == BluetoothDevice.BOND_BONDING) { 629 device.cancelBondProcess(); 630 } 631 632 if (state != BluetoothDevice.BOND_NONE) { 633 final boolean successful = device.removeBond(); 634 if (successful) { 635 if (DEBUG) { 636 Log.d(TAG, "Bluetooth device successfully unpaired: " + device.getName()); 637 } 638 return true; 639 } else { 640 Log.e(TAG, "Failed to unpair Bluetooth Device: " + device.getName()); 641 } 642 } 643 } 644 return false; 645 } 646 getBluetoothConnector()647 private BluetoothConnector getBluetoothConnector() { 648 int majorDeviceClass = mTarget.getBluetoothClass().getMajorDeviceClass(); 649 switch (majorDeviceClass) { 650 case BluetoothClass.Device.Major.PERIPHERAL: 651 return new BluetoothInputDeviceConnector( 652 mContext, mTarget, mHandler, mOpenConnectionCallback); 653 case BluetoothClass.Device.Major.AUDIO_VIDEO: 654 return new BluetoothA2dpConnector(mContext, mTarget, mOpenConnectionCallback); 655 default: 656 Log.d(TAG, "Unhandle device class: " + majorDeviceClass); 657 break; 658 } 659 return null; 660 } 661 } 662