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 public void clearDeviceList() { 356 doCancel(); 357 mVisibleDevices.clear(); 358 } 359 360 /** 361 * Stop any pairing request that is in progress. 362 */ 363 public void cancelPairing() { 364 mAutoMode = false; 365 doCancel(); 366 } 367 368 /** 369 * Stop doing anything we're doing, release any resources. 370 */ 371 public void dispose() { 372 mHandler.removeCallbacksAndMessages(null); 373 if (mLinkReceiverRegistered) { 374 unregisterLinkStatusReceiver(); 375 } 376 stopScanning(); 377 } 378 379 /** 380 * Start pairing and connection to the specified device. 381 * @param device 382 */ 383 public void startPairing(BluetoothDevice device) { 384 startPairing(device, DELAY_MANUAL_PAIRING); 385 } 386 387 /** 388 * See {@link #startPairing(BluetoothDevice)}. 389 * @param delay The delay before pairing starts. In this window, cancel may 390 * be called. 391 */ 392 public void startPairing(BluetoothDevice device, int delay) { 393 startPairing(device, delay, true); 394 } 395 396 /** 397 * Return our state 398 * @return One of the STATE_ constants. 399 */ 400 public int getStatus() { 401 return mStatus; 402 } 403 404 /** 405 * Get the device that we're currently targeting. This will be null if 406 * there is no device that is in the process of being connected to. 407 */ 408 public BluetoothDevice getTargetDevice() { 409 return mTarget; 410 } 411 412 /** 413 * When the timer to start the next stage will expire, in {@link SystemClock#elapsedRealtime()}. 414 * Will only be valid while waiting to pair and after an error from which we are restarting. 415 */ 416 public long getNextStageTime() { 417 return mNextStageTimestamp; 418 } 419 420 public List<BluetoothDevice> getAvailableDevices() { 421 ArrayList<BluetoothDevice> copy = new ArrayList<BluetoothDevice>(mVisibleDevices.size()); 422 copy.addAll(mVisibleDevices); 423 return copy; 424 } 425 426 public void setListener(EventListener listener) { 427 mListener = listener; 428 } 429 430 public void invalidateDevice(BluetoothDevice device) { 431 onDeviceLost(device); 432 } 433 434 private void startPairing(BluetoothDevice device, int delay, boolean isManual) { 435 // TODO check if we're already paired/bonded to this device 436 437 // cancel auto-mode if applicable 438 mAutoMode = !isManual; 439 440 mTarget = device; 441 442 if (isInProgress()) { 443 throw new RuntimeException("Pairing already in progress, you must cancel the " + 444 "previous request first"); 445 } 446 447 mHandler.removeMessages(MSG_PAIR); 448 mHandler.removeMessages(MSG_START); 449 450 mNextStageTimestamp = SystemClock.elapsedRealtime() + 451 (mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 452 mHandler.sendEmptyMessageDelayed(MSG_PAIR, 453 mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING); 454 455 setStatus(STATUS_WAITING_TO_PAIR); 456 } 457 458 /** 459 * Pairing is in progress and is no longer cancelable. 460 */ 461 public boolean isInProgress() { 462 return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING && 463 mStatus != STATUS_WAITING_TO_PAIR; 464 } 465 466 private void updateListener() { 467 if (mListener != null) { 468 mListener.statusChanged(); 469 } 470 } 471 472 private void onDeviceFound(BluetoothDevice device) { 473 if (!mVisibleDevices.contains(device)) { 474 mVisibleDevices.add(device); 475 Log.d(TAG, "Added device to visible list. Name = " + device.getName() + " , class = " + 476 device.getBluetoothClass().getDeviceClass()); 477 } else { 478 return; 479 } 480 481 updatePairingState(); 482 // update the listener because a new device is visible 483 updateListener(); 484 } 485 486 private void onDeviceLost(BluetoothDevice device) { 487 // TODO validate removal works as expected 488 if (mVisibleDevices.remove(device)) { 489 updatePairingState(); 490 // update the listener because a device disappeared 491 updateListener(); 492 } 493 } 494 495 private void updatePairingState() { 496 if (mAutoMode) { 497 if (isReadyToAutoPair()) { 498 mTarget = mVisibleDevices.get(0); 499 startPairing(mTarget, DELAY_AUTO_PAIRING, false); 500 } else { 501 doCancel(); 502 } 503 } 504 } 505 506 /** 507 * @return {@code true} If there is only one visible input device. 508 */ 509 private boolean isReadyToAutoPair() { 510 // we imagine that the conditions under which we decide to pair or 511 // not may one day become more complicated, which is why this length 512 // check is wrapped in a method call. 513 return mVisibleDevices.size() == 1; 514 } 515 516 private void doCancel() { 517 // TODO allow cancel to be called from any state 518 if (isInProgress()) { 519 Log.d(TAG, "Pairing process has already begun, it can not be canceled."); 520 return; 521 } 522 523 // stop scanning, just in case we are 524 BluetoothScanner.stopListening(mBtListener); 525 BluetoothScanner.stopNow(); 526 527 // remove any callbacks 528 mHandler.removeMessages(MSG_PAIR); 529 530 // remove bond, if existing 531 unpairDevice(mTarget); 532 533 mTarget = null; 534 535 setStatus(STATUS_NONE); 536 537 // resume scanning 538 start(); 539 } 540 541 /** 542 * Set the status and update any listener. 543 */ 544 private void setStatus(int status) { 545 mStatus = status; 546 updateListener(); 547 } 548 549 private void startBonding() { 550 stopScanning(); 551 setStatus(STATUS_PAIRING); 552 if (mTarget.getBondState() != BluetoothDevice.BOND_BONDED) { 553 registerLinkStatusReceiver(); 554 555 // create bond (pair) to the device 556 mTarget.createBond(); 557 } else { 558 onBonded(); 559 } 560 } 561 562 private void onBonded() { 563 openConnection(); 564 } 565 566 private void openConnection() { 567 setStatus(STATUS_CONNECTING); 568 569 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 570 571 // connect to the Bluetooth service, then registerInputListener 572 adapter.getProfileProxy(mContext, mServiceConnection, BluetoothProfile.INPUT_DEVICE); 573 } 574 575 private void onInputAdded() { 576 unregisterInputMethodMonitor(); 577 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); 578 adapter.closeProfileProxy(BluetoothProfile.INPUT_DEVICE, mInputProxy); 579 setStatus(STATUS_NONE); 580 } 581 582 private void onBondFailed() { 583 Log.w(TAG, "There was an error bonding with the device."); 584 setStatus(STATUS_ERROR); 585 586 // remove bond, if existing 587 unpairDevice(mTarget); 588 589 // TODO do we need to check Bluetooth for the device and possible delete it? 590 mNextStageTimestamp = SystemClock.elapsedRealtime() + DELAY_RETRY; 591 mHandler.sendEmptyMessageDelayed(MSG_START, DELAY_RETRY); 592 } 593 594 private void registerInputMethodMonitor() { 595 InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE); 596 inputManager.registerInputDeviceListener(mInputListener, mHandler); 597 598 // TO DO: The line below is a workaround for an issue in InputManager. 599 // The manager doesn't actually registers itself with the InputService 600 // unless we query it for input devices. We should remove this once 601 // the problem is fixed in InputManager. 602 // Reference bug in Frameworks: b/10415556 603 int[] inputDevices = inputManager.getInputDeviceIds(); 604 } 605 606 private void unregisterInputMethodMonitor() { 607 InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE); 608 inputManager.unregisterInputDeviceListener(mInputListener); 609 } 610 611 private void registerLinkStatusReceiver() { 612 mLinkReceiverRegistered = true; 613 IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 614 mContext.registerReceiver(mLinkStatusReceiver, filter); 615 } 616 617 private void unregisterLinkStatusReceiver() { 618 mLinkReceiverRegistered = false; 619 mContext.unregisterReceiver(mLinkStatusReceiver); 620 } 621 622 private void stopScanning() { 623 BluetoothScanner.stopListening(mBtListener); 624 BluetoothScanner.stopNow(); 625 } 626 627 public boolean unpairDevice(BluetoothDevice device) { 628 if (device != null) { 629 int state = device.getBondState(); 630 631 if (state == BluetoothDevice.BOND_BONDING) { 632 device.cancelBondProcess(); 633 } 634 635 if (state != BluetoothDevice.BOND_NONE) { 636 final boolean successful = device.removeBond(); 637 if (successful) { 638 if (DEBUG) { 639 Log.d(TAG, "Bluetooth device successfully unpaired: " + device.getName()); 640 } 641 return true; 642 } else { 643 Log.e(TAG, "Failed to unpair Bluetooth Device: " + device.getName()); 644 } 645 } 646 } 647 return false; 648 } 649 } 650