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