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
17package com.android.systemui.keyboard;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.le.BluetoothLeScanner;
22import android.bluetooth.le.ScanCallback;
23import android.bluetooth.le.ScanFilter;
24import android.bluetooth.le.ScanRecord;
25import android.bluetooth.le.ScanResult;
26import android.bluetooth.le.ScanSettings;
27import android.content.ContentResolver;
28import android.content.Context;
29import android.content.DialogInterface;
30import android.content.res.Configuration;
31import android.hardware.input.InputManager;
32import android.os.Handler;
33import android.os.HandlerThread;
34import android.os.Looper;
35import android.os.Message;
36import android.os.Process;
37import android.os.SystemClock;
38import android.os.UserHandle;
39import android.provider.Settings.Secure;
40import android.text.TextUtils;
41import android.util.Pair;
42import android.util.Slog;
43import android.widget.Toast;
44
45import com.android.settingslib.bluetooth.BluetoothCallback;
46import com.android.settingslib.bluetooth.CachedBluetoothDevice;
47import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
48import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
49import com.android.settingslib.bluetooth.LocalBluetoothManager;
50import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
51import com.android.settingslib.bluetooth.Utils;
52import com.android.systemui.R;
53import com.android.systemui.SystemUI;
54
55import java.io.FileDescriptor;
56import java.io.PrintWriter;
57import java.util.Arrays;
58import java.util.Collection;
59import java.util.List;
60import java.util.Set;
61
62public 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
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
129    protected void onConfigurationChanged(Configuration newConfig) {
130    }
131
132    @Override
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
144    protected void onBootCompleted() {
145        mHandler.sendEmptyMessage(MSG_ON_BOOT_COMPLETED);
146    }
147
148    @Override
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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.
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 {
415        public KeyboardUIHandler() {
416            super(Looper.getMainLooper(), null, true /*async*/);
417        }
418        @Override
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 {
451        public KeyboardHandler(Looper looper) {
452            super(looper, null, true /*async*/);
453        }
454
455        @Override
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
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
526        public void onDismiss(DialogInterface dialog) {
527            mDialog = null;
528        }
529    }
530
531    private final class KeyboardScanCallback extends ScanCallback {
532
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
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
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
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
592        public void onBluetoothStateChanged(int bluetoothState) {
593            mHandler.obtainMessage(MSG_ON_BLUETOOTH_STATE_CHANGED,
594                    bluetoothState, 0).sendToTarget();
595        }
596
597        @Override
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
604        public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { }
605        @Override
606        public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { }
607        @Override
608        public void onScanningStateChanged(boolean started) { }
609        @Override
610        public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { }
611    }
612
613    private final class BluetoothErrorListener implements Utils.ErrorListener {
614        public void onShowError(Context context, String name, int messageResId) {
615            mHandler.obtainMessage(MSG_SHOW_ERROR, messageResId, 0 /*unused*/,
616                    new Pair<>(context, name)).sendToTarget();
617        }
618    }
619
620    private static String stateToString(int state) {
621        switch (state) {
622            case STATE_NOT_ENABLED:
623                return "STATE_NOT_ENABLED";
624            case STATE_WAITING_FOR_BOOT_COMPLETED:
625                return "STATE_WAITING_FOR_BOOT_COMPLETED";
626            case STATE_WAITING_FOR_TABLET_MODE_EXIT:
627                return "STATE_WAITING_FOR_TABLET_MODE_EXIT";
628            case STATE_WAITING_FOR_DEVICE_DISCOVERY:
629                return "STATE_WAITING_FOR_DEVICE_DISCOVERY";
630            case STATE_WAITING_FOR_BLUETOOTH:
631                return "STATE_WAITING_FOR_BLUETOOTH";
632            case STATE_PAIRING:
633                return "STATE_PAIRING";
634            case STATE_PAIRED:
635                return "STATE_PAIRED";
636            case STATE_PAIRING_FAILED:
637                return "STATE_PAIRING_FAILED";
638            case STATE_USER_CANCELLED:
639                return "STATE_USER_CANCELLED";
640            case STATE_DEVICE_NOT_FOUND:
641                return "STATE_DEVICE_NOT_FOUND";
642            case STATE_UNKNOWN:
643            default:
644                return "STATE_UNKNOWN (" + state + ")";
645        }
646    }
647}
648