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