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