KeyboardUI.java revision 9209c9cd9a6f779d0d9d86f9b2e368df564fa6bb
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.ScanResult; 27import android.bluetooth.le.ScanSettings; 28import android.content.ContentResolver; 29import android.content.Context; 30import android.content.DialogInterface; 31import android.content.Intent; 32import android.content.res.Configuration; 33import android.hardware.input.InputManager; 34import android.os.Handler; 35import android.os.HandlerThread; 36import android.os.Looper; 37import android.os.Message; 38import android.os.Process; 39import android.os.SystemClock; 40import android.os.UserHandle; 41import android.provider.Settings.Secure; 42import android.text.TextUtils; 43import android.util.Slog; 44import android.view.WindowManager; 45 46import com.android.settingslib.bluetooth.BluetoothCallback; 47import com.android.settingslib.bluetooth.CachedBluetoothDevice; 48import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 49import com.android.settingslib.bluetooth.LocalBluetoothAdapter; 50import com.android.settingslib.bluetooth.LocalBluetoothManager; 51import com.android.settingslib.bluetooth.LocalBluetoothProfileManager; 52import com.android.systemui.R; 53import com.android.systemui.SystemUI; 54 55import java.io.FileDescriptor; 56import java.io.PrintWriter; 57import java.util.Arrays; 58import java.util.Collection; 59import java.util.List; 60import java.util.Map; 61import java.util.Set; 62 63public class KeyboardUI extends SystemUI implements InputManager.OnTabletModeChangedListener { 64 private static final String TAG = "KeyboardUI"; 65 private static final boolean DEBUG = false; 66 67 // Give BT some time to start after SyUI comes up. This avoids flashing a dialog in the user's 68 // face because BT starts a little bit later in the boot process than SysUI and it takes some 69 // time for us to receive the signal that it's starting. 70 private static final long BLUETOOTH_START_DELAY_MILLIS = 10 * 1000; 71 72 private static final int STATE_NOT_ENABLED = -1; 73 private static final int STATE_UNKNOWN = 0; 74 private static final int STATE_WAITING_FOR_BOOT_COMPLETED = 1; 75 private static final int STATE_WAITING_FOR_TABLET_MODE_EXIT = 2; 76 private static final int STATE_WAITING_FOR_DEVICE_DISCOVERY = 3; 77 private static final int STATE_WAITING_FOR_BLUETOOTH = 4; 78 private static final int STATE_WAITING_FOR_STATE_PAIRED = 5; 79 private static final int STATE_PAIRING = 6; 80 private static final int STATE_PAIRED = 7; 81 private static final int STATE_USER_CANCELLED = 8; 82 private static final int STATE_DEVICE_NOT_FOUND = 9; 83 84 private static final int MSG_INIT = 0; 85 private static final int MSG_ON_BOOT_COMPLETED = 1; 86 private static final int MSG_PROCESS_KEYBOARD_STATE = 2; 87 private static final int MSG_ENABLE_BLUETOOTH = 3; 88 private static final int MSG_ON_BLUETOOTH_STATE_CHANGED = 4; 89 private static final int MSG_ON_DEVICE_BOND_STATE_CHANGED = 5; 90 private static final int MSG_ON_BLUETOOTH_DEVICE_ADDED = 6; 91 private static final int MSG_ON_BLE_SCAN_FAILED = 7; 92 private static final int MSG_SHOW_BLUETOOTH_DIALOG = 8; 93 private static final int MSG_DISMISS_BLUETOOTH_DIALOG = 9; 94 95 private volatile KeyboardHandler mHandler; 96 private volatile KeyboardUIHandler mUIHandler; 97 98 protected volatile Context mContext; 99 100 private boolean mEnabled; 101 private String mKeyboardName; 102 private CachedBluetoothDeviceManager mCachedDeviceManager; 103 private LocalBluetoothAdapter mLocalBluetoothAdapter; 104 private LocalBluetoothProfileManager mProfileManager; 105 private boolean mBootCompleted; 106 private long mBootCompletedTime; 107 108 private int mInTabletMode = InputManager.SWITCH_STATE_UNKNOWN; 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 = (InputManager) context.getSystemService(Context.INPUT_SERVICE); 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 && 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 246 device = getDiscoveredKeyboard(); 247 if (device != null) { 248 mState = STATE_PAIRING; 249 device.startPairing(); 250 } else { 251 mState = STATE_WAITING_FOR_DEVICE_DISCOVERY; 252 startScanning(); 253 } 254 } 255 256 // Should only be called on the handler thread 257 public void onBootCompletedInternal() { 258 mBootCompleted = true; 259 mBootCompletedTime = SystemClock.uptimeMillis(); 260 if (mState == STATE_WAITING_FOR_BOOT_COMPLETED) { 261 processKeyboardState(); 262 } 263 } 264 265 // Should only be called on the handler thread 266 private void showBluetoothDialog() { 267 if (isUserSetupComplete()) { 268 long now = SystemClock.uptimeMillis(); 269 long earliestDialogTime = mBootCompletedTime + BLUETOOTH_START_DELAY_MILLIS; 270 if (earliestDialogTime < now) { 271 mUIHandler.sendEmptyMessage(MSG_SHOW_BLUETOOTH_DIALOG); 272 } else { 273 mHandler.sendEmptyMessageAtTime(MSG_PROCESS_KEYBOARD_STATE, earliestDialogTime); 274 } 275 } else { 276 // If we're in setup wizard and the keyboard is docked, just automatically enable BT. 277 mLocalBluetoothAdapter.enable(); 278 } 279 } 280 281 private boolean isUserSetupComplete() { 282 ContentResolver resolver = mContext.getContentResolver(); 283 return Secure.getIntForUser( 284 resolver, Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0; 285 } 286 287 private CachedBluetoothDevice getPairedKeyboard() { 288 Set<BluetoothDevice> devices = mLocalBluetoothAdapter.getBondedDevices(); 289 for (BluetoothDevice d : devices) { 290 if (mKeyboardName.equals(d.getName())) { 291 return getCachedBluetoothDevice(d); 292 } 293 } 294 return null; 295 } 296 297 private CachedBluetoothDevice getDiscoveredKeyboard() { 298 Collection<CachedBluetoothDevice> devices = mCachedDeviceManager.getCachedDevicesCopy(); 299 for (CachedBluetoothDevice d : devices) { 300 if (d.getName().equals(mKeyboardName)) { 301 return d; 302 } 303 } 304 return null; 305 } 306 307 308 private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice d) { 309 CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(d); 310 if (cachedDevice == null) { 311 cachedDevice = mCachedDeviceManager.addDevice( 312 mLocalBluetoothAdapter, mProfileManager, d); 313 } 314 return cachedDevice; 315 } 316 317 private void startScanning() { 318 BluetoothLeScanner scanner = mLocalBluetoothAdapter.getBluetoothLeScanner(); 319 ScanFilter filter = (new ScanFilter.Builder()).setDeviceName(mKeyboardName).build(); 320 ScanSettings settings = (new ScanSettings.Builder()) 321 .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES) 322 .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT) 323 .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY) 324 .setReportDelay(0) 325 .build(); 326 mScanCallback = new KeyboardScanCallback(); 327 scanner.startScan(Arrays.asList(filter), settings, mScanCallback); 328 } 329 330 private void stopScanning() { 331 if (mScanCallback != null) { 332 mLocalBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback); 333 mScanCallback = null; 334 } 335 } 336 337 // Should only be called on the handler thread 338 private void onDeviceAddedInternal(CachedBluetoothDevice d) { 339 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && d.getName().equals(mKeyboardName)) { 340 stopScanning(); 341 d.startPairing(); 342 mState = STATE_PAIRING; 343 } 344 } 345 346 // Should only be called on the handler thread 347 private void onBluetoothStateChangedInternal(int bluetoothState) { 348 if (bluetoothState == BluetoothAdapter.STATE_ON && mState == STATE_WAITING_FOR_BLUETOOTH) { 349 processKeyboardState(); 350 } 351 } 352 353 // Should only be called on the handler thread 354 private void onDeviceBondStateChangedInternal(CachedBluetoothDevice d, int bondState) { 355 if (d.getName().equals(mKeyboardName) && bondState == BluetoothDevice.BOND_BONDED) { 356 // We don't need to manually connect to the device here because it will automatically 357 // try to connect after it has been paired. 358 mState = STATE_PAIRED; 359 } 360 } 361 362 // Should only be called on the handler thread 363 private void onBleScanFailedInternal() { 364 mScanCallback = null; 365 if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY) { 366 mState = STATE_DEVICE_NOT_FOUND; 367 } 368 } 369 370 private final class KeyboardUIHandler extends Handler { 371 public KeyboardUIHandler() { 372 super(Looper.getMainLooper(), null, true /*async*/); 373 } 374 @Override 375 public void handleMessage(Message msg) { 376 switch(msg.what) { 377 case MSG_SHOW_BLUETOOTH_DIALOG: { 378 DialogInterface.OnClickListener listener = new BluetoothDialogClickListener(); 379 mDialog = new BluetoothDialog(mContext); 380 mDialog.setTitle(R.string.enable_bluetooth_title); 381 mDialog.setMessage(R.string.enable_bluetooth_message); 382 mDialog.setPositiveButton(R.string.enable_bluetooth_confirmation_ok, listener); 383 mDialog.setNegativeButton(android.R.string.cancel, listener); 384 mDialog.show(); 385 break; 386 } 387 case MSG_DISMISS_BLUETOOTH_DIALOG: { 388 if (mDialog != null) { 389 mDialog.dismiss(); 390 mDialog = null; 391 } 392 break; 393 } 394 } 395 } 396 } 397 398 private final class KeyboardHandler extends Handler { 399 public KeyboardHandler(Looper looper) { 400 super(looper, null, true /*async*/); 401 } 402 403 @Override 404 public void handleMessage(Message msg) { 405 switch(msg.what) { 406 case MSG_INIT: { 407 init(); 408 break; 409 } 410 case MSG_ON_BOOT_COMPLETED: { 411 onBootCompletedInternal(); 412 break; 413 } 414 case MSG_PROCESS_KEYBOARD_STATE: { 415 processKeyboardState(); 416 break; 417 } 418 case MSG_ENABLE_BLUETOOTH: { 419 boolean enable = msg.arg1 == 1; 420 if (enable) { 421 mLocalBluetoothAdapter.enable(); 422 } else { 423 mState = STATE_USER_CANCELLED; 424 } 425 } 426 case MSG_ON_BLUETOOTH_STATE_CHANGED: { 427 int bluetoothState = msg.arg1; 428 onBluetoothStateChangedInternal(bluetoothState); 429 break; 430 } 431 case MSG_ON_DEVICE_BOND_STATE_CHANGED: { 432 CachedBluetoothDevice d = (CachedBluetoothDevice)msg.obj; 433 int bondState = msg.arg1; 434 onDeviceBondStateChangedInternal(d, bondState); 435 break; 436 } 437 case MSG_ON_BLUETOOTH_DEVICE_ADDED: { 438 BluetoothDevice d = (BluetoothDevice)msg.obj; 439 CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(d); 440 onDeviceAddedInternal(cachedDevice); 441 break; 442 443 } 444 case MSG_ON_BLE_SCAN_FAILED: { 445 onBleScanFailedInternal(); 446 break; 447 } 448 } 449 } 450 } 451 452 private final class BluetoothDialogClickListener implements DialogInterface.OnClickListener { 453 @Override 454 public void onClick(DialogInterface dialog, int which) { 455 int enable = DialogInterface.BUTTON_POSITIVE == which ? 1 : 0; 456 mHandler.obtainMessage(MSG_ENABLE_BLUETOOTH, enable, 0).sendToTarget(); 457 mDialog = null; 458 } 459 } 460 461 private final class KeyboardScanCallback extends ScanCallback { 462 @Override 463 public void onBatchScanResults(List<ScanResult> results) { 464 if (DEBUG) { 465 Slog.d(TAG, "onBatchScanResults(" + results.size() + ")"); 466 } 467 if (!results.isEmpty()) { 468 BluetoothDevice bestDevice = results.get(0).getDevice(); 469 int bestRssi = results.get(0).getRssi(); 470 final int N = results.size(); 471 for (int i = 0; i < N; i++) { 472 ScanResult r = results.get(i); 473 if (r.getRssi() > bestRssi) { 474 bestDevice = r.getDevice(); 475 } 476 } 477 mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED, bestDevice).sendToTarget(); 478 } 479 } 480 481 @Override 482 public void onScanFailed(int errorCode) { 483 if (DEBUG) { 484 Slog.d(TAG, "onScanFailed(" + errorCode + ")"); 485 } 486 mHandler.obtainMessage(MSG_ON_BLE_SCAN_FAILED).sendToTarget(); 487 } 488 489 @Override 490 public void onScanResult(int callbackType, ScanResult result) { 491 if (DEBUG) { 492 Slog.d(TAG, "onScanResult(" + callbackType + ", " + result + ")"); 493 } 494 mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED, 495 result.getDevice()).sendToTarget(); 496 } 497 } 498 499 private final class BluetoothCallbackHandler implements BluetoothCallback { 500 @Override 501 public void onBluetoothStateChanged(int bluetoothState) { 502 mHandler.obtainMessage(MSG_ON_BLUETOOTH_STATE_CHANGED, 503 bluetoothState, 0).sendToTarget(); 504 } 505 506 @Override 507 public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) { 508 mHandler.obtainMessage(MSG_ON_DEVICE_BOND_STATE_CHANGED, 509 bondState, 0, cachedDevice).sendToTarget(); 510 } 511 512 @Override 513 public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { } 514 @Override 515 public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { } 516 @Override 517 public void onScanningStateChanged(boolean started) { } 518 @Override 519 public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { } 520 } 521 522 private static String stateToString(int state) { 523 switch (state) { 524 case STATE_NOT_ENABLED: 525 return "STATE_NOT_ENABLED"; 526 case STATE_WAITING_FOR_BOOT_COMPLETED: 527 return "STATE_WAITING_FOR_BOOT_COMPLETED"; 528 case STATE_WAITING_FOR_TABLET_MODE_EXIT: 529 return "STATE_WAITING_FOR_TABLET_MODE_EXIT"; 530 case STATE_WAITING_FOR_DEVICE_DISCOVERY: 531 return "STATE_WAITING_FOR_DEVICE_DISCOVERY"; 532 case STATE_WAITING_FOR_BLUETOOTH: 533 return "STATE_WAITING_FOR_BLUETOOTH"; 534 case STATE_WAITING_FOR_STATE_PAIRED: 535 return "STATE_WAITING_FOR_STATE_PAIRED"; 536 case STATE_PAIRING: 537 return "STATE_PAIRING"; 538 case STATE_PAIRED: 539 return "STATE_PAIRED"; 540 case STATE_USER_CANCELLED: 541 return "STATE_USER_CANCELLED"; 542 case STATE_DEVICE_NOT_FOUND: 543 return "STATE_DEVICE_NOT_FOUND"; 544 case STATE_UNKNOWN: 545 default: 546 return "STATE_UNKNOWN"; 547 } 548 } 549} 550