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