KeyboardUI.java revision c0d7058b14c24cd07912f5629c26b39b7b4673d5
1/* 2 * Copyright (C) 2015 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.systemui.keyboard; 18 19import android.bluetooth.BluetoothAdapter; 20import android.bluetooth.BluetoothDevice; 21import android.bluetooth.le.BluetoothLeScanner; 22import android.bluetooth.le.ScanCallback; 23import android.bluetooth.le.ScanFilter; 24import android.bluetooth.le.ScanRecord; 25import android.bluetooth.le.ScanResult; 26import android.bluetooth.le.ScanSettings; 27import android.content.ContentResolver; 28import android.content.Context; 29import android.content.DialogInterface; 30import android.content.res.Configuration; 31import android.hardware.input.InputManager; 32import android.os.Handler; 33import android.os.HandlerThread; 34import android.os.Looper; 35import android.os.Message; 36import android.os.Process; 37import android.os.SystemClock; 38import android.os.UserHandle; 39import android.provider.Settings.Secure; 40import android.text.TextUtils; 41import android.util.Slog; 42 43import com.android.settingslib.bluetooth.BluetoothCallback; 44import com.android.settingslib.bluetooth.CachedBluetoothDevice; 45import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 46import com.android.settingslib.bluetooth.LocalBluetoothAdapter; 47import com.android.settingslib.bluetooth.LocalBluetoothManager; 48import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 49import com.android.systemui.R; 50import com.android.systemui.SystemUI; 51 52import java.io.FileDescriptor; 53import java.io.PrintWriter; 54import java.util.Arrays; 55import java.util.Collection; 56import java.util.List; 57import java.util.Set; 58 59public class KeyboardUI extends SystemUI implements InputManager.OnTabletModeChangedListener { 60 private static final String TAG = "KeyboardUI"; 61 private static final boolean DEBUG = false; 62 63 // Give BT some time to start after SyUI comes up. This avoids flashing a dialog in the user's 64 // face because BT starts a little bit later in the boot process than SysUI and it takes some 65 // time for us to receive the signal that it's starting. 66 private static final long BLUETOOTH_START_DELAY_MILLIS = 10 * 1000; 67 68 // We will be scanning up to 30 seconds, after which we'll stop. 69 private static final long BLUETOOTH_SCAN_TIMEOUT_MILLIS = 30 * 1000; 70 71 private static final int STATE_NOT_ENABLED = -1; 72 private static final int STATE_UNKNOWN = 0; 73 private static final int STATE_WAITING_FOR_BOOT_COMPLETED = 1; 74 private static final int STATE_WAITING_FOR_TABLET_MODE_EXIT = 2; 75 private static final int STATE_WAITING_FOR_DEVICE_DISCOVERY = 3; 76 private static final int STATE_WAITING_FOR_BLUETOOTH = 4; 77 private static final int STATE_PAIRING = 5; 78 private static final int STATE_PAIRED = 6; 79 private static final int STATE_USER_CANCELLED = 7; 80 private static final int STATE_DEVICE_NOT_FOUND = 8; 81 82 private static final int MSG_INIT = 0; 83 private static final int MSG_ON_BOOT_COMPLETED = 1; 84 private static final int MSG_PROCESS_KEYBOARD_STATE = 2; 85 private static final int MSG_ENABLE_BLUETOOTH = 3; 86 private static final int MSG_ON_BLUETOOTH_STATE_CHANGED = 4; 87 private static final int MSG_ON_DEVICE_BOND_STATE_CHANGED = 5; 88 private static final int MSG_ON_BLUETOOTH_DEVICE_ADDED = 6; 89 private static final int MSG_ON_BLE_SCAN_FAILED = 7; 90 private static final int MSG_SHOW_BLUETOOTH_DIALOG = 8; 91 private static final int MSG_DISMISS_BLUETOOTH_DIALOG = 9; 92 private static final int MSG_BLE_ABORT_SCAN = 10; 93 94 private volatile KeyboardHandler mHandler; 95 private volatile KeyboardUIHandler mUIHandler; 96 97 protected volatile Context mContext; 98 99 private boolean mEnabled; 100 private String mKeyboardName; 101 private CachedBluetoothDeviceManager mCachedDeviceManager; 102 private LocalBluetoothAdapter mLocalBluetoothAdapter; 103 private LocalBluetoothProfileManager mProfileManager; 104 private boolean mBootCompleted; 105 private long mBootCompletedTime; 106 107 private int mInTabletMode = InputManager.SWITCH_STATE_UNKNOWN; 108 private int mScanAttempt = 0; 109 private ScanCallback mScanCallback; 110 private BluetoothDialog mDialog; 111 112 private int mState; 113 114 @Override 115 public void start() { 116 mContext = super.mContext; 117 HandlerThread thread = new HandlerThread("Keyboard", Process.THREAD_PRIORITY_BACKGROUND); 118 thread.start(); 119 mHandler = new KeyboardHandler(thread.getLooper()); 120 mHandler.sendEmptyMessage(MSG_INIT); 121 } 122 123 @Override 124 protected void onConfigurationChanged(Configuration newConfig) { 125 } 126 127 @Override 128 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 129 pw.println("KeyboardUI:"); 130 pw.println(" mEnabled=" + mEnabled); 131 pw.println(" mBootCompleted=" + mEnabled); 132 pw.println(" mBootCompletedTime=" + mBootCompletedTime); 133 pw.println(" mKeyboardName=" + mKeyboardName); 134 pw.println(" mInTabletMode=" + mInTabletMode); 135 pw.println(" mState=" + stateToString(mState)); 136 } 137 138 @Override 139 protected void onBootCompleted() { 140 mHandler.sendEmptyMessage(MSG_ON_BOOT_COMPLETED); 141 } 142 143 @Override 144 public void onTabletModeChanged(long whenNanos, boolean inTabletMode) { 145 if (DEBUG) { 146 Slog.d(TAG, "onTabletModeChanged(" + whenNanos + ", " + inTabletMode + ")"); 147 } 148 149 if (inTabletMode && mInTabletMode != InputManager.SWITCH_STATE_ON 150 || !inTabletMode && mInTabletMode != InputManager.SWITCH_STATE_OFF) { 151 mInTabletMode = inTabletMode ? 152 InputManager.SWITCH_STATE_ON : InputManager.SWITCH_STATE_OFF; 153 processKeyboardState(); 154 } 155 } 156 157 // Shoud only be called on the handler thread 158 private void init() { 159 Context context = mContext; 160 mKeyboardName = 161 context.getString(com.android.internal.R.string.config_packagedKeyboardName); 162 if (TextUtils.isEmpty(mKeyboardName)) { 163 if (DEBUG) { 164 Slog.d(TAG, "No packaged keyboard name given."); 165 } 166 return; 167 } 168 169 LocalBluetoothManager bluetoothManager = LocalBluetoothManager.getInstance(context, null); 170 if (bluetoothManager == null) { 171 if (DEBUG) { 172 Slog.e(TAG, "Failed to retrieve LocalBluetoothManager instance"); 173 } 174 return; 175 } 176 mEnabled = true; 177 mCachedDeviceManager = bluetoothManager.getCachedDeviceManager(); 178 mLocalBluetoothAdapter = bluetoothManager.getBluetoothAdapter(); 179 mProfileManager = bluetoothManager.getProfileManager(); 180 bluetoothManager.getEventManager().registerCallback(new BluetoothCallbackHandler()); 181 182 InputManager im = context.getSystemService(InputManager.class); 183 im.registerOnTabletModeChangedListener(this, mHandler); 184 mInTabletMode = im.isInTabletMode(); 185 186 processKeyboardState(); 187 mUIHandler = new KeyboardUIHandler(); 188 } 189 190 // Should only be called on the handler thread 191 private void processKeyboardState() { 192 mHandler.removeMessages(MSG_PROCESS_KEYBOARD_STATE); 193 194 if (!mEnabled) { 195 mState = STATE_NOT_ENABLED; 196 return; 197 } 198 199 if (!mBootCompleted) { 200 mState = STATE_WAITING_FOR_BOOT_COMPLETED; 201 return; 202 } 203 204 if (mInTabletMode != InputManager.SWITCH_STATE_OFF) { 205 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY) { 206 stopScanning(); 207 } 208 mState = STATE_WAITING_FOR_TABLET_MODE_EXIT; 209 return; 210 } 211 212 final int btState = mLocalBluetoothAdapter.getState(); 213 if (btState == BluetoothAdapter.STATE_TURNING_ON || btState == BluetoothAdapter.STATE_ON 214 && mState == STATE_WAITING_FOR_BLUETOOTH) { 215 // If we're waiting for bluetooth but it has come on in the meantime, or is coming 216 // on, just dismiss the dialog. This frequently happens during device startup. 217 mUIHandler.sendEmptyMessage(MSG_DISMISS_BLUETOOTH_DIALOG); 218 } 219 220 if (btState == BluetoothAdapter.STATE_TURNING_ON) { 221 mState = STATE_WAITING_FOR_BLUETOOTH; 222 // Wait for bluetooth to fully come on. 223 return; 224 } 225 226 if (btState != BluetoothAdapter.STATE_ON) { 227 mState = STATE_WAITING_FOR_BLUETOOTH; 228 showBluetoothDialog(); 229 return; 230 } 231 232 CachedBluetoothDevice device = getPairedKeyboard(); 233 if (mState == STATE_WAITING_FOR_TABLET_MODE_EXIT || mState == STATE_WAITING_FOR_BLUETOOTH) { 234 if (device != null) { 235 // If we're just coming out of tablet mode or BT just turned on, 236 // then we want to go ahead and automatically connect to the 237 // keyboard. We want to avoid this in other cases because we might 238 // be spuriously called after the user has manually disconnected 239 // the keyboard, meaning we shouldn't try to automtically connect 240 // it again. 241 mState = STATE_PAIRED; 242 device.connect(false); 243 return; 244 } 245 mCachedDeviceManager.clearNonBondedDevices(); 246 } 247 248 device = getDiscoveredKeyboard(); 249 if (device != null) { 250 mState = STATE_PAIRING; 251 device.startPairing(); 252 } else { 253 mState = STATE_WAITING_FOR_DEVICE_DISCOVERY; 254 startScanning(); 255 } 256 } 257 258 // Should only be called on the handler thread 259 public void onBootCompletedInternal() { 260 mBootCompleted = true; 261 mBootCompletedTime = SystemClock.uptimeMillis(); 262 if (mState == STATE_WAITING_FOR_BOOT_COMPLETED) { 263 processKeyboardState(); 264 } 265 } 266 267 // Should only be called on the handler thread 268 private void showBluetoothDialog() { 269 if (isUserSetupComplete()) { 270 long now = SystemClock.uptimeMillis(); 271 long earliestDialogTime = mBootCompletedTime + BLUETOOTH_START_DELAY_MILLIS; 272 if (earliestDialogTime < now) { 273 mUIHandler.sendEmptyMessage(MSG_SHOW_BLUETOOTH_DIALOG); 274 } else { 275 mHandler.sendEmptyMessageAtTime(MSG_PROCESS_KEYBOARD_STATE, earliestDialogTime); 276 } 277 } else { 278 // If we're in setup wizard and the keyboard is docked, just automatically enable BT. 279 mLocalBluetoothAdapter.enable(); 280 } 281 } 282 283 private boolean isUserSetupComplete() { 284 ContentResolver resolver = mContext.getContentResolver(); 285 return Secure.getIntForUser( 286 resolver, Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0; 287 } 288 289 private CachedBluetoothDevice getPairedKeyboard() { 290 Set<BluetoothDevice> devices = mLocalBluetoothAdapter.getBondedDevices(); 291 for (BluetoothDevice d : devices) { 292 if (mKeyboardName.equals(d.getName())) { 293 return getCachedBluetoothDevice(d); 294 } 295 } 296 return null; 297 } 298 299 private CachedBluetoothDevice getDiscoveredKeyboard() { 300 Collection<CachedBluetoothDevice> devices = mCachedDeviceManager.getCachedDevicesCopy(); 301 for (CachedBluetoothDevice d : devices) { 302 if (d.getName().equals(mKeyboardName)) { 303 return d; 304 } 305 } 306 return null; 307 } 308 309 310 private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice d) { 311 CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(d); 312 if (cachedDevice == null) { 313 cachedDevice = mCachedDeviceManager.addDevice( 314 mLocalBluetoothAdapter, mProfileManager, d); 315 } 316 return cachedDevice; 317 } 318 319 private void startScanning() { 320 BluetoothLeScanner scanner = mLocalBluetoothAdapter.getBluetoothLeScanner(); 321 ScanFilter filter = (new ScanFilter.Builder()).setDeviceName(mKeyboardName).build(); 322 ScanSettings settings = (new ScanSettings.Builder()) 323 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 324 .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) 325 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 326 .setReportDelay(0) 327 .build(); 328 mScanCallback = new KeyboardScanCallback(); 329 scanner.startScan(Arrays.asList(filter), settings, mScanCallback); 330 331 Message abortMsg = mHandler.obtainMessage(MSG_BLE_ABORT_SCAN, ++mScanAttempt, 0); 332 mHandler.sendMessageDelayed(abortMsg, BLUETOOTH_SCAN_TIMEOUT_MILLIS); 333 } 334 335 private void stopScanning() { 336 if (mScanCallback != null) { 337 mLocalBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback); 338 mScanCallback = null; 339 } 340 } 341 342 // Should only be called on the handler thread 343 private void bleAbortScanInternal(int scanAttempt) { 344 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && scanAttempt == mScanAttempt) { 345 if (DEBUG) { 346 Slog.d(TAG, "Bluetooth scan timed out"); 347 } 348 stopScanning(); 349 // FIXME: should we also try shutting off bluetooth if we enabled 350 // it in the first place? 351 mState = STATE_DEVICE_NOT_FOUND; 352 } 353 } 354 355 // Should only be called on the handler thread 356 private void onDeviceAddedInternal(CachedBluetoothDevice d) { 357 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && d.getName().equals(mKeyboardName)) { 358 stopScanning(); 359 d.startPairing(); 360 mState = STATE_PAIRING; 361 } 362 } 363 364 // Should only be called on the handler thread 365 private void onBluetoothStateChangedInternal(int bluetoothState) { 366 if (bluetoothState == BluetoothAdapter.STATE_ON && mState == STATE_WAITING_FOR_BLUETOOTH) { 367 processKeyboardState(); 368 } 369 } 370 371 // Should only be called on the handler thread 372 private void onDeviceBondStateChangedInternal(CachedBluetoothDevice d, int bondState) { 373 if (d.getName().equals(mKeyboardName) && bondState == BluetoothDevice.BOND_BONDED) { 374 // We don't need to manually connect to the device here because it will automatically 375 // try to connect after it has been paired. 376 mState = STATE_PAIRED; 377 } 378 } 379 380 // Should only be called on the handler thread 381 private void onBleScanFailedInternal() { 382 mScanCallback = null; 383 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY) { 384 mState = STATE_DEVICE_NOT_FOUND; 385 } 386 } 387 388 private final class KeyboardUIHandler extends Handler { 389 public KeyboardUIHandler() { 390 super(Looper.getMainLooper(), null, true /*async*/); 391 } 392 @Override 393 public void handleMessage(Message msg) { 394 switch(msg.what) { 395 case MSG_SHOW_BLUETOOTH_DIALOG: { 396 DialogInterface.OnClickListener listener = new BluetoothDialogClickListener(); 397 mDialog = new BluetoothDialog(mContext); 398 mDialog.setTitle(R.string.enable_bluetooth_title); 399 mDialog.setMessage(R.string.enable_bluetooth_message); 400 mDialog.setPositiveButton(R.string.enable_bluetooth_confirmation_ok, listener); 401 mDialog.setNegativeButton(android.R.string.cancel, listener); 402 mDialog.show(); 403 break; 404 } 405 case MSG_DISMISS_BLUETOOTH_DIALOG: { 406 if (mDialog != null) { 407 mDialog.dismiss(); 408 mDialog = null; 409 } 410 break; 411 } 412 } 413 } 414 } 415 416 private final class KeyboardHandler extends Handler { 417 public KeyboardHandler(Looper looper) { 418 super(looper, null, true /*async*/); 419 } 420 421 @Override 422 public void handleMessage(Message msg) { 423 switch(msg.what) { 424 case MSG_INIT: { 425 init(); 426 break; 427 } 428 case MSG_ON_BOOT_COMPLETED: { 429 onBootCompletedInternal(); 430 break; 431 } 432 case MSG_PROCESS_KEYBOARD_STATE: { 433 processKeyboardState(); 434 break; 435 } 436 case MSG_ENABLE_BLUETOOTH: { 437 boolean enable = msg.arg1 == 1; 438 if (enable) { 439 mLocalBluetoothAdapter.enable(); 440 } else { 441 mState = STATE_USER_CANCELLED; 442 } 443 break; 444 } 445 case MSG_BLE_ABORT_SCAN: { 446 int scanAttempt = msg.arg1; 447 bleAbortScanInternal(scanAttempt); 448 break; 449 } 450 case MSG_ON_BLUETOOTH_STATE_CHANGED: { 451 int bluetoothState = msg.arg1; 452 onBluetoothStateChangedInternal(bluetoothState); 453 break; 454 } 455 case MSG_ON_DEVICE_BOND_STATE_CHANGED: { 456 CachedBluetoothDevice d = (CachedBluetoothDevice)msg.obj; 457 int bondState = msg.arg1; 458 onDeviceBondStateChangedInternal(d, bondState); 459 break; 460 } 461 case MSG_ON_BLUETOOTH_DEVICE_ADDED: { 462 BluetoothDevice d = (BluetoothDevice)msg.obj; 463 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(d); 464 onDeviceAddedInternal(cachedDevice); 465 break; 466 467 } 468 case MSG_ON_BLE_SCAN_FAILED: { 469 onBleScanFailedInternal(); 470 break; 471 } 472 } 473 } 474 } 475 476 private final class BluetoothDialogClickListener implements DialogInterface.OnClickListener { 477 @Override 478 public void onClick(DialogInterface dialog, int which) { 479 int enable = DialogInterface.BUTTON_POSITIVE == which ? 1 : 0; 480 mHandler.obtainMessage(MSG_ENABLE_BLUETOOTH, enable, 0).sendToTarget(); 481 mDialog = null; 482 } 483 } 484 485 private final class KeyboardScanCallback extends ScanCallback { 486 487 private boolean isDeviceDiscoverable(ScanResult result) { 488 final ScanRecord scanRecord = result.getScanRecord(); 489 final int flags = scanRecord.getAdvertiseFlags(); 490 final int BT_DISCOVERABLE_MASK = 0x03; 491 492 return (flags & BT_DISCOVERABLE_MASK) != 0; 493 } 494 495 @Override 496 public void onBatchScanResults(List<ScanResult> results) { 497 if (DEBUG) { 498 Slog.d(TAG, "onBatchScanResults(" + results.size() + ")"); 499 } 500 501 BluetoothDevice bestDevice = null; 502 int bestRssi = Integer.MIN_VALUE; 503 504 for (ScanResult result : results) { 505 if (DEBUG) { 506 Slog.d(TAG, "onBatchScanResults: considering " + result); 507 } 508 509 if (isDeviceDiscoverable(result) && result.getRssi() > bestRssi) { 510 bestDevice = result.getDevice(); 511 bestRssi = result.getRssi(); 512 } 513 } 514 515 if (bestDevice != null) { 516 mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED, bestDevice).sendToTarget(); 517 } 518 } 519 520 @Override 521 public void onScanFailed(int errorCode) { 522 if (DEBUG) { 523 Slog.d(TAG, "onScanFailed(" + errorCode + ")"); 524 } 525 mHandler.obtainMessage(MSG_ON_BLE_SCAN_FAILED).sendToTarget(); 526 } 527 528 @Override 529 public void onScanResult(int callbackType, ScanResult result) { 530 if (DEBUG) { 531 Slog.d(TAG, "onScanResult(" + callbackType + ", " + result + ")"); 532 } 533 534 if (isDeviceDiscoverable(result)) { 535 mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED, 536 result.getDevice()).sendToTarget(); 537 } else if (DEBUG) { 538 Slog.d(TAG, "onScanResult: device " + result.getDevice() + 539 " is not discoverable, ignoring"); 540 } 541 } 542 } 543 544 private final class BluetoothCallbackHandler implements BluetoothCallback { 545 @Override 546 public void onBluetoothStateChanged(int bluetoothState) { 547 mHandler.obtainMessage(MSG_ON_BLUETOOTH_STATE_CHANGED, 548 bluetoothState, 0).sendToTarget(); 549 } 550 551 @Override 552 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 553 mHandler.obtainMessage(MSG_ON_DEVICE_BOND_STATE_CHANGED, 554 bondState, 0, cachedDevice).sendToTarget(); 555 } 556 557 @Override 558 public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { } 559 @Override 560 public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { } 561 @Override 562 public void onScanningStateChanged(boolean started) { } 563 @Override 564 public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { } 565 } 566 567 private static String stateToString(int state) { 568 switch (state) { 569 case STATE_NOT_ENABLED: 570 return "STATE_NOT_ENABLED"; 571 case STATE_WAITING_FOR_BOOT_COMPLETED: 572 return "STATE_WAITING_FOR_BOOT_COMPLETED"; 573 case STATE_WAITING_FOR_TABLET_MODE_EXIT: 574 return "STATE_WAITING_FOR_TABLET_MODE_EXIT"; 575 case STATE_WAITING_FOR_DEVICE_DISCOVERY: 576 return "STATE_WAITING_FOR_DEVICE_DISCOVERY"; 577 case STATE_WAITING_FOR_BLUETOOTH: 578 return "STATE_WAITING_FOR_BLUETOOTH"; 579 case STATE_PAIRING: 580 return "STATE_PAIRING"; 581 case STATE_PAIRED: 582 return "STATE_PAIRED"; 583 case STATE_USER_CANCELLED: 584 return "STATE_USER_CANCELLED"; 585 case STATE_DEVICE_NOT_FOUND: 586 return "STATE_DEVICE_NOT_FOUND"; 587 case STATE_UNKNOWN: 588 default: 589 return "STATE_UNKNOWN"; 590 } 591 } 592} 593