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