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 static final int SCAN_MODE_NOT_SET = 0; 150 151 private final BluetoothScanner.Listener mBtListener = new BluetoothScanner.Listener() { 152 @Override 153 public void onDeviceAdded(BluetoothScanner.Device device) { 154 if (DEBUG) { 155 Log.d(TAG, "Adding device: " + device.btDevice.getAddress()); 156 } 157 onDeviceFound(device.btDevice); 158 } 159 160 @Override 161 public void onDeviceRemoved(BluetoothScanner.Device device) { 162 if (DEBUG) { 163 Log.d(TAG, "Device lost: " + device.btDevice.getAddress()); 164 } 165 onDeviceLost(device.btDevice); 166 } 167 }; 168 hasValidInputDevice(Context context, int[] deviceIds)169 public static boolean hasValidInputDevice(Context context, int[] deviceIds) { 170 InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE); 171 172 for (int ptr = deviceIds.length - 1; ptr > -1; ptr--) { 173 InputDevice device = inMan.getInputDevice(deviceIds[ptr]); 174 int sources = device.getSources(); 175 176 boolean isCompatible = false; 177 178 if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) { 179 isCompatible = true; 180 } 181 182 if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) { 183 isCompatible = true; 184 } 185 186 if ((sources & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) { 187 boolean isValidKeyboard = true; 188 String keyboardName = device.getName(); 189 for (int index = 0; index < INVALID_INPUT_KEYBOARD_DEVICE_NAMES.length; ++index) { 190 if (keyboardName.equals(INVALID_INPUT_KEYBOARD_DEVICE_NAMES[index])) { 191 isValidKeyboard = false; 192 break; 193 } 194 } 195 196 if (isValidKeyboard) { 197 isCompatible = true; 198 } 199 } 200 201 if (!device.isVirtual() && isCompatible) { 202 return true; 203 } 204 } 205 return false; 206 } 207 hasValidInputDevice(Context context)208 public static boolean hasValidInputDevice(Context context) { 209 InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE); 210 int[] inputDevices = inMan.getInputDeviceIds(); 211 212 return hasValidInputDevice(context, inputDevices); 213 } 214 215 private final BroadcastReceiver mLinkStatusReceiver = new BroadcastReceiver() { 216 @Override 217 public void onReceive(Context context, Intent intent) { 218 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 219 if (DEBUG) { 220 Log.d(TAG, "There was a link status change for: " + device.getAddress()); 221 } 222 223 if (device.equals(mTarget)) { 224 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 225 BluetoothDevice.BOND_NONE); 226 int previousBondState = intent.getIntExtra( 227 BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE); 228 229 if (DEBUG) { 230 Log.d(TAG, "Bond states: old = " + previousBondState + ", new = " + 231 bondState); 232 } 233 234 if (bondState == BluetoothDevice.BOND_NONE && 235 previousBondState == BluetoothDevice.BOND_BONDING) { 236 // we seem to have reverted, this is an error 237 // TODO inform user, start scanning again 238 unregisterLinkStatusReceiver(); 239 onBondFailed(); 240 } else if (bondState == BluetoothDevice.BOND_BONDED) { 241 unregisterLinkStatusReceiver(); 242 onBonded(); 243 } 244 } 245 } 246 }; 247 248 private BroadcastReceiver mBluetoothStateReceiver; 249 250 private final OpenConnectionCallback mOpenConnectionCallback = new OpenConnectionCallback() { 251 public void succeeded() { 252 setStatus(STATUS_NONE); 253 } 254 public void failed() { 255 setStatus(STATUS_ERROR); 256 } 257 }; 258 259 private final Context mContext; 260 private EventListener mListener; 261 private int mStatus = STATUS_NONE; 262 /** 263 * Set to {@code false} when {@link #cancelPairing()} or 264 * {@link #startPairing(BluetoothDevice)}. This instance 265 * will now no longer automatically start pairing. 266 */ 267 private boolean mAutoMode = true; 268 private final ArrayList<BluetoothDevice> mVisibleDevices = new ArrayList<>(); 269 private BluetoothDevice mTarget; 270 private final Handler mHandler; 271 private long mNextStageTimestamp = -1; 272 private boolean mLinkReceiverRegistered = false; 273 private final ArrayList<BluetoothDeviceCriteria> mBluetoothDeviceCriteria = new ArrayList<>(); 274 private InputDeviceCriteria mInputDeviceCriteria; 275 private int mDefaultScanMode = SCAN_MODE_NOT_SET; 276 277 /** 278 * Should be instantiated on a thread with a Looper, perhaps the main thread! 279 */ BluetoothDevicePairer(Context context, EventListener listener)280 public BluetoothDevicePairer(Context context, EventListener listener) { 281 mContext = context.getApplicationContext(); 282 mListener = listener; 283 284 addBluetoothDeviceCriteria(); 285 286 mHandler = new Handler() { 287 @Override 288 public void handleMessage(Message msg) { 289 switch (msg.what) { 290 case MSG_PAIR: 291 startBonding(); 292 break; 293 case MSG_START: 294 start(); 295 break; 296 default: 297 Log.d(TAG, "No handler case available for message: " + msg.what); 298 } 299 } 300 }; 301 } 302 addBluetoothDeviceCriteria()303 private void addBluetoothDeviceCriteria() { 304 // Input is supported by all devices. 305 mInputDeviceCriteria = new InputDeviceCriteria(); 306 mBluetoothDeviceCriteria.add(mInputDeviceCriteria); 307 308 // Add Bluetooth a2dp on if the service is running and the 309 // setting profile_supported_a2dp is set to true. 310 Intent intent = new Intent(IBluetoothA2dp.class.getName()); 311 ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0); 312 if (comp != null) { 313 int enabledState = mContext.getPackageManager().getComponentEnabledSetting(comp); 314 if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) { 315 Log.d(TAG, "Adding A2dp device criteria for pairing"); 316 mBluetoothDeviceCriteria.add(new A2dpDeviceCriteria()); 317 } 318 } 319 } 320 321 /** 322 * Start listening for devices and begin the pairing process when 323 * criteria is met. 324 */ start()325 public void start() { 326 final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 327 if (!bluetoothAdapter.isEnabled()) { 328 Log.d(TAG, "Bluetooth not enabled, delaying startup."); 329 if (mBluetoothStateReceiver == null) { 330 mBluetoothStateReceiver = new BroadcastReceiver() { 331 @Override 332 public void onReceive(Context context, Intent intent) { 333 if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, 334 BluetoothAdapter.STATE_OFF) == BluetoothAdapter.STATE_ON) { 335 Log.d(TAG, "Bluetooth now enabled, starting."); 336 start(); 337 } else { 338 Log.d(TAG, "Bluetooth not yet started, got broadcast: " + intent); 339 } 340 } 341 }; 342 mContext.registerReceiver(mBluetoothStateReceiver, 343 new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)); 344 } 345 346 bluetoothAdapter.enable(); 347 return; 348 } else { 349 if (mBluetoothStateReceiver != null) { 350 mContext.unregisterReceiver(mBluetoothStateReceiver); 351 mBluetoothStateReceiver = null; 352 } 353 } 354 355 // Another device may initiate pairing. To accommodate this, turn on discoverability 356 // if it isn't already. 357 final int scanMode = bluetoothAdapter.getScanMode(); 358 if (scanMode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { 359 Log.d(TAG, "Turning on discoverability, default scan mode: " + scanMode); 360 mDefaultScanMode = scanMode; 361 bluetoothAdapter.setScanMode( 362 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, 363 0 /* no timeout */); 364 } 365 366 // set status to scanning before we start listening since 367 // startListening may result in a transition to STATUS_WAITING_TO_PAIR 368 // which might seem odd from a client perspective 369 setStatus(STATUS_SCANNING); 370 371 BluetoothScanner.startListening(mContext, mBtListener, mBluetoothDeviceCriteria); 372 } 373 clearDeviceList()374 public void clearDeviceList() { 375 doCancel(); 376 mVisibleDevices.clear(); 377 } 378 379 /** 380 * Stop any pairing request that is in progress. 381 */ cancelPairing()382 public void cancelPairing() { 383 mAutoMode = false; 384 doCancel(); 385 } 386 387 388 /** 389 * Switch to manual pairing mode. 390 */ disableAutoPairing()391 public void disableAutoPairing() { 392 mAutoMode = false; 393 } 394 395 /** 396 * Stop doing anything we're doing, release any resources. 397 */ dispose()398 public void dispose() { 399 final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 400 if (mDefaultScanMode != SCAN_MODE_NOT_SET 401 && mDefaultScanMode != bluetoothAdapter.getScanMode()) { 402 Log.d(TAG, "Resetting discoverability to: " + mDefaultScanMode); 403 bluetoothAdapter.setScanMode(mDefaultScanMode); 404 } 405 406 mHandler.removeCallbacksAndMessages(null); 407 if (mLinkReceiverRegistered) { 408 unregisterLinkStatusReceiver(); 409 } 410 if (mBluetoothStateReceiver != null) { 411 mContext.unregisterReceiver(mBluetoothStateReceiver); 412 } 413 stopScanning(); 414 } 415 416 /** 417 * Start pairing and connection to the specified device. 418 * @param device device 419 */ startPairing(BluetoothDevice device)420 public void startPairing(BluetoothDevice device) { 421 startPairing(device, true); 422 } 423 424 /** 425 * Return our state 426 * @return One of the STATE_ constants. 427 */ getStatus()428 public int getStatus() { 429 return mStatus; 430 } 431 432 /** 433 * Get the device that we're currently targeting. This will be null if 434 * there is no device that is in the process of being connected to. 435 */ getTargetDevice()436 public BluetoothDevice getTargetDevice() { 437 return mTarget; 438 } 439 440 /** 441 * When the timer to start the next stage will expire, in {@link SystemClock#elapsedRealtime()}. 442 * Will only be valid while waiting to pair and after an error from which we are restarting. 443 */ getNextStageTime()444 public long getNextStageTime() { 445 return mNextStageTimestamp; 446 } 447 getAvailableDevices()448 public List<BluetoothDevice> getAvailableDevices() { 449 ArrayList<BluetoothDevice> copy = new ArrayList<>(mVisibleDevices.size()); 450 copy.addAll(mVisibleDevices); 451 return copy; 452 } 453 setListener(EventListener listener)454 public void setListener(EventListener listener) { 455 mListener = listener; 456 } 457 invalidateDevice(BluetoothDevice device)458 public void invalidateDevice(BluetoothDevice device) { 459 onDeviceLost(device); 460 } 461 startPairing(BluetoothDevice device, boolean isManual)462 private void startPairing(BluetoothDevice device, boolean isManual) { 463 // TODO check if we're already paired/bonded to this device 464 465 // cancel auto-mode if applicable 466 mAutoMode = !isManual; 467 468 mTarget = device; 469 470 if (isInProgress()) { 471 throw new RuntimeException("Pairing already in progress, you must cancel the " + 472 "previous request first"); 473 } 474 475 mHandler.removeCallbacksAndMessages(null); 476 477 mNextStageTimestamp = SystemClock.elapsedRealtime() + 478 (mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 479 mHandler.sendEmptyMessageDelayed(MSG_PAIR, 480 mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 481 482 setStatus(STATUS_WAITING_TO_PAIR); 483 } 484 485 /** 486 * Pairing is in progress and is no longer cancelable. 487 */ isInProgress()488 public boolean isInProgress() { 489 return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING && 490 mStatus != STATUS_WAITING_TO_PAIR; 491 } 492 updateListener()493 private void updateListener() { 494 if (mListener != null) { 495 mListener.statusChanged(); 496 } 497 } 498 onDeviceFound(BluetoothDevice device)499 private void onDeviceFound(BluetoothDevice device) { 500 if (!mVisibleDevices.contains(device)) { 501 mVisibleDevices.add(device); 502 Log.d(TAG, "Added device to visible list. Name = " + device.getName() + " , class = " + 503 device.getBluetoothClass().getDeviceClass()); 504 } else { 505 return; 506 } 507 508 updatePairingState(); 509 // update the listener because a new device is visible 510 updateListener(); 511 } 512 onDeviceLost(BluetoothDevice device)513 private void onDeviceLost(BluetoothDevice device) { 514 // TODO validate removal works as expected 515 if (mVisibleDevices.remove(device)) { 516 updatePairingState(); 517 // update the listener because a device disappeared 518 updateListener(); 519 } 520 } 521 updatePairingState()522 private void updatePairingState() { 523 if (mAutoMode) { 524 BluetoothDevice candidate = getAutoPairDevice(); 525 if (null != candidate) { 526 mTarget = candidate; 527 startPairing(mTarget, false); 528 } else { 529 doCancel(); 530 } 531 } 532 } 533 534 /** 535 * @return returns the only visible input device if there is only one 536 */ getAutoPairDevice()537 private BluetoothDevice getAutoPairDevice() { 538 List<BluetoothDevice> inputDevices = new ArrayList<>(); 539 for (BluetoothDevice device : mVisibleDevices) { 540 if (mInputDeviceCriteria.isInputDevice(device.getBluetoothClass())) { 541 inputDevices.add(device); 542 } 543 } 544 if (inputDevices.size() == 1) { 545 return inputDevices.get(0); 546 } 547 return null; 548 } 549 doCancel()550 private void doCancel() { 551 // TODO allow cancel to be called from any state 552 if (isInProgress()) { 553 Log.d(TAG, "Pairing process has already begun, it can not be canceled."); 554 return; 555 } 556 557 // stop scanning, just in case we are 558 final boolean wasListening = BluetoothScanner.stopListening(mBtListener); 559 BluetoothScanner.stopNow(); 560 561 mHandler.removeCallbacksAndMessages(null); 562 563 // remove bond, if existing 564 unpairDevice(mTarget); 565 566 mTarget = null; 567 568 setStatus(STATUS_NONE); 569 570 // resume scanning 571 if (wasListening) { 572 start(); 573 } 574 } 575 576 /** 577 * Set the status and update any listener. 578 */ setStatus(int status)579 private void setStatus(int status) { 580 mStatus = status; 581 updateListener(); 582 } 583 startBonding()584 private void startBonding() { 585 stopScanning(); 586 setStatus(STATUS_PAIRING); 587 if (mTarget.getBondState() != BluetoothDevice.BOND_BONDED) { 588 registerLinkStatusReceiver(); 589 590 // create bond (pair) to the device 591 mTarget.createBond(); 592 } else { 593 onBonded(); 594 } 595 } 596 onBonded()597 private void onBonded() { 598 openConnection(); 599 } 600 openConnection()601 private void openConnection() { 602 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 603 BluetoothConnector btConnector = getBluetoothConnector(); 604 if (btConnector != null) { 605 setStatus(STATUS_CONNECTING); 606 btConnector.openConnection(adapter); 607 } else { 608 Log.w(TAG, "There was an error getting the BluetoothConnector."); 609 setStatus(STATUS_ERROR); 610 if (mLinkReceiverRegistered) { 611 unregisterLinkStatusReceiver(); 612 } 613 unpairDevice(mTarget); 614 } 615 } 616 onBondFailed()617 private void onBondFailed() { 618 Log.w(TAG, "There was an error bonding with the device."); 619 setStatus(STATUS_ERROR); 620 621 // remove bond, if existing 622 unpairDevice(mTarget); 623 624 // TODO do we need to check Bluetooth for the device and possible delete it? 625 mNextStageTimestamp = SystemClock.elapsedRealtime() + DELAY_RETRY; 626 mHandler.sendEmptyMessageDelayed(MSG_START, DELAY_RETRY); 627 } 628 registerLinkStatusReceiver()629 private void registerLinkStatusReceiver() { 630 mLinkReceiverRegistered = true; 631 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 632 mContext.registerReceiver(mLinkStatusReceiver, filter); 633 } 634 unregisterLinkStatusReceiver()635 private void unregisterLinkStatusReceiver() { 636 mLinkReceiverRegistered = false; 637 mContext.unregisterReceiver(mLinkStatusReceiver); 638 } 639 stopScanning()640 private void stopScanning() { 641 BluetoothScanner.stopListening(mBtListener); 642 BluetoothScanner.stopNow(); 643 } 644 unpairDevice(BluetoothDevice device)645 public boolean unpairDevice(BluetoothDevice device) { 646 if (device != null) { 647 int state = device.getBondState(); 648 649 if (state == BluetoothDevice.BOND_BONDING) { 650 device.cancelBondProcess(); 651 } 652 653 if (state != BluetoothDevice.BOND_NONE) { 654 final boolean successful = device.removeBond(); 655 if (successful) { 656 if (DEBUG) { 657 Log.d(TAG, "Bluetooth device successfully unpaired: " + device.getName()); 658 } 659 return true; 660 } else { 661 Log.e(TAG, "Failed to unpair Bluetooth Device: " + device.getName()); 662 } 663 } 664 } 665 return false; 666 } 667 getBluetoothConnector()668 private BluetoothConnector getBluetoothConnector() { 669 int majorDeviceClass = mTarget.getBluetoothClass().getMajorDeviceClass(); 670 switch (majorDeviceClass) { 671 case BluetoothClass.Device.Major.PERIPHERAL: 672 return new BluetoothInputDeviceConnector( 673 mContext, mTarget, mHandler, mOpenConnectionCallback); 674 case BluetoothClass.Device.Major.AUDIO_VIDEO: 675 return new BluetoothA2dpConnector(mContext, mTarget, mOpenConnectionCallback); 676 default: 677 Log.d(TAG, "Unhandle device class: " + majorDeviceClass); 678 break; 679 } 680 return null; 681 } 682 } 683