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