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