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