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