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