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