Home | History | Annotate | Download | only in keyboard
      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
    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         @Override
    612         public void onActiveDeviceChanged(CachedBluetoothDevice activeDevice,
    613                                           int bluetoothProfile) { }
    614 
    615         @Override
    616         public void onAudioModeChanged() { }
    617     }
    618 
    619     private final class BluetoothErrorListener implements Utils.ErrorListener {
    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 
    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