KeyboardUI.java revision 9209c9cd9a6f779d0d9d86f9b2e368df564fa6bb
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.ScanResult;
27import android.bluetooth.le.ScanSettings;
28import android.content.ContentResolver;
29import android.content.Context;
30import android.content.DialogInterface;
31import android.content.Intent;
32import android.content.res.Configuration;
33import android.hardware.input.InputManager;
34import android.os.Handler;
35import android.os.HandlerThread;
36import android.os.Looper;
37import android.os.Message;
38import android.os.Process;
39import android.os.SystemClock;
40import android.os.UserHandle;
41import android.provider.Settings.Secure;
42import android.text.TextUtils;
43import android.util.Slog;
44import android.view.WindowManager;
45
46import com.android.settingslib.bluetooth.BluetoothCallback;
47import com.android.settingslib.bluetooth.CachedBluetoothDevice;
48import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager;
49import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
50import com.android.settingslib.bluetooth.LocalBluetoothManager;
51import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
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.Map;
61import java.util.Set;
62
63public class KeyboardUI extends SystemUI implements InputManager.OnTabletModeChangedListener {
64    private static final String TAG = "KeyboardUI";
65    private static final boolean DEBUG = false;
66
67    // Give BT some time to start after SyUI comes up. This avoids flashing a dialog in the user's
68    // face because BT starts a little bit later in the boot process than SysUI and it takes some
69    // time for us to receive the signal that it's starting.
70    private static final long BLUETOOTH_START_DELAY_MILLIS = 10 * 1000;
71
72    private static final int STATE_NOT_ENABLED = -1;
73    private static final int STATE_UNKNOWN = 0;
74    private static final int STATE_WAITING_FOR_BOOT_COMPLETED = 1;
75    private static final int STATE_WAITING_FOR_TABLET_MODE_EXIT = 2;
76    private static final int STATE_WAITING_FOR_DEVICE_DISCOVERY = 3;
77    private static final int STATE_WAITING_FOR_BLUETOOTH = 4;
78    private static final int STATE_WAITING_FOR_STATE_PAIRED = 5;
79    private static final int STATE_PAIRING = 6;
80    private static final int STATE_PAIRED = 7;
81    private static final int STATE_USER_CANCELLED = 8;
82    private static final int STATE_DEVICE_NOT_FOUND = 9;
83
84    private static final int MSG_INIT = 0;
85    private static final int MSG_ON_BOOT_COMPLETED = 1;
86    private static final int MSG_PROCESS_KEYBOARD_STATE = 2;
87    private static final int MSG_ENABLE_BLUETOOTH = 3;
88    private static final int MSG_ON_BLUETOOTH_STATE_CHANGED = 4;
89    private static final int MSG_ON_DEVICE_BOND_STATE_CHANGED = 5;
90    private static final int MSG_ON_BLUETOOTH_DEVICE_ADDED = 6;
91    private static final int MSG_ON_BLE_SCAN_FAILED = 7;
92    private static final int MSG_SHOW_BLUETOOTH_DIALOG = 8;
93    private static final int MSG_DISMISS_BLUETOOTH_DIALOG = 9;
94
95    private volatile KeyboardHandler mHandler;
96    private volatile KeyboardUIHandler mUIHandler;
97
98    protected volatile Context mContext;
99
100    private boolean mEnabled;
101    private String mKeyboardName;
102    private CachedBluetoothDeviceManager mCachedDeviceManager;
103    private LocalBluetoothAdapter mLocalBluetoothAdapter;
104    private LocalBluetoothProfileManager mProfileManager;
105    private boolean mBootCompleted;
106    private long mBootCompletedTime;
107
108    private int mInTabletMode = InputManager.SWITCH_STATE_UNKNOWN;
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 = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
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                && 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
246        device = getDiscoveredKeyboard();
247        if (device != null) {
248            mState = STATE_PAIRING;
249            device.startPairing();
250        } else {
251            mState = STATE_WAITING_FOR_DEVICE_DISCOVERY;
252            startScanning();
253        }
254    }
255
256    // Should only be called on the handler thread
257    public void onBootCompletedInternal() {
258        mBootCompleted = true;
259        mBootCompletedTime = SystemClock.uptimeMillis();
260        if (mState == STATE_WAITING_FOR_BOOT_COMPLETED) {
261            processKeyboardState();
262        }
263    }
264
265    // Should only be called on the handler thread
266    private void showBluetoothDialog() {
267        if (isUserSetupComplete()) {
268            long now = SystemClock.uptimeMillis();
269            long earliestDialogTime = mBootCompletedTime + BLUETOOTH_START_DELAY_MILLIS;
270            if (earliestDialogTime < now) {
271                mUIHandler.sendEmptyMessage(MSG_SHOW_BLUETOOTH_DIALOG);
272            } else {
273                mHandler.sendEmptyMessageAtTime(MSG_PROCESS_KEYBOARD_STATE, earliestDialogTime);
274            }
275        } else {
276            // If we're in setup wizard and the keyboard is docked, just automatically enable BT.
277            mLocalBluetoothAdapter.enable();
278        }
279    }
280
281    private boolean isUserSetupComplete() {
282        ContentResolver resolver = mContext.getContentResolver();
283        return Secure.getIntForUser(
284                resolver, Secure.USER_SETUP_COMPLETE, 0, UserHandle.USER_CURRENT) != 0;
285    }
286
287    private CachedBluetoothDevice getPairedKeyboard() {
288        Set<BluetoothDevice> devices = mLocalBluetoothAdapter.getBondedDevices();
289        for (BluetoothDevice d : devices) {
290            if (mKeyboardName.equals(d.getName())) {
291                return getCachedBluetoothDevice(d);
292            }
293        }
294        return null;
295    }
296
297    private CachedBluetoothDevice getDiscoveredKeyboard() {
298        Collection<CachedBluetoothDevice> devices = mCachedDeviceManager.getCachedDevicesCopy();
299        for (CachedBluetoothDevice d : devices) {
300            if (d.getName().equals(mKeyboardName)) {
301                return d;
302            }
303        }
304        return null;
305    }
306
307
308    private CachedBluetoothDevice getCachedBluetoothDevice(BluetoothDevice d) {
309        CachedBluetoothDevice cachedDevice = mCachedDeviceManager.findDevice(d);
310        if (cachedDevice == null) {
311            cachedDevice = mCachedDeviceManager.addDevice(
312                    mLocalBluetoothAdapter, mProfileManager, d);
313        }
314        return cachedDevice;
315    }
316
317    private void startScanning() {
318        BluetoothLeScanner scanner = mLocalBluetoothAdapter.getBluetoothLeScanner();
319        ScanFilter filter = (new ScanFilter.Builder()).setDeviceName(mKeyboardName).build();
320        ScanSettings settings = (new ScanSettings.Builder())
321            .setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
322            .setNumOfMatches(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT)
323            .setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
324            .setReportDelay(0)
325            .build();
326        mScanCallback = new KeyboardScanCallback();
327        scanner.startScan(Arrays.asList(filter), settings, mScanCallback);
328    }
329
330    private void stopScanning() {
331        if (mScanCallback != null) {
332            mLocalBluetoothAdapter.getBluetoothLeScanner().stopScan(mScanCallback);
333            mScanCallback = null;
334        }
335    }
336
337    // Should only be called on the handler thread
338    private void onDeviceAddedInternal(CachedBluetoothDevice d) {
339        if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY && d.getName().equals(mKeyboardName)) {
340            stopScanning();
341            d.startPairing();
342            mState = STATE_PAIRING;
343        }
344    }
345
346    // Should only be called on the handler thread
347    private void onBluetoothStateChangedInternal(int bluetoothState) {
348        if (bluetoothState == BluetoothAdapter.STATE_ON && mState == STATE_WAITING_FOR_BLUETOOTH) {
349            processKeyboardState();
350        }
351    }
352
353    // Should only be called on the handler thread
354    private void onDeviceBondStateChangedInternal(CachedBluetoothDevice d, int bondState) {
355        if (d.getName().equals(mKeyboardName) && bondState == BluetoothDevice.BOND_BONDED) {
356            // We don't need to manually connect to the device here because it will automatically
357            // try to connect after it has been paired.
358            mState = STATE_PAIRED;
359        }
360    }
361
362    // Should only be called on the handler thread
363    private void onBleScanFailedInternal() {
364        mScanCallback = null;
365        if (mState == STATE_WAITING_FOR_DEVICE_DISCOVERY) {
366            mState = STATE_DEVICE_NOT_FOUND;
367        }
368    }
369
370    private final class KeyboardUIHandler extends Handler {
371        public KeyboardUIHandler() {
372            super(Looper.getMainLooper(), null, true /*async*/);
373        }
374        @Override
375        public void handleMessage(Message msg) {
376            switch(msg.what) {
377                case MSG_SHOW_BLUETOOTH_DIALOG: {
378                    DialogInterface.OnClickListener listener = new BluetoothDialogClickListener();
379                    mDialog = new BluetoothDialog(mContext);
380                    mDialog.setTitle(R.string.enable_bluetooth_title);
381                    mDialog.setMessage(R.string.enable_bluetooth_message);
382                    mDialog.setPositiveButton(R.string.enable_bluetooth_confirmation_ok, listener);
383                    mDialog.setNegativeButton(android.R.string.cancel, listener);
384                    mDialog.show();
385                    break;
386                }
387                case MSG_DISMISS_BLUETOOTH_DIALOG: {
388                    if (mDialog != null) {
389                        mDialog.dismiss();
390                        mDialog = null;
391                    }
392                    break;
393                }
394            }
395        }
396    }
397
398    private final class KeyboardHandler extends Handler {
399        public KeyboardHandler(Looper looper) {
400            super(looper, null, true /*async*/);
401        }
402
403        @Override
404        public void handleMessage(Message msg) {
405            switch(msg.what) {
406                case MSG_INIT: {
407                    init();
408                    break;
409                }
410                case MSG_ON_BOOT_COMPLETED: {
411                    onBootCompletedInternal();
412                    break;
413                }
414                case MSG_PROCESS_KEYBOARD_STATE: {
415                    processKeyboardState();
416                    break;
417                }
418                case MSG_ENABLE_BLUETOOTH: {
419                    boolean enable = msg.arg1 == 1;
420                    if (enable) {
421                        mLocalBluetoothAdapter.enable();
422                    } else {
423                        mState = STATE_USER_CANCELLED;
424                    }
425                }
426                case MSG_ON_BLUETOOTH_STATE_CHANGED: {
427                    int bluetoothState = msg.arg1;
428                    onBluetoothStateChangedInternal(bluetoothState);
429                    break;
430                }
431                case MSG_ON_DEVICE_BOND_STATE_CHANGED: {
432                    CachedBluetoothDevice d = (CachedBluetoothDevice)msg.obj;
433                    int bondState = msg.arg1;
434                    onDeviceBondStateChangedInternal(d, bondState);
435                    break;
436                }
437                case MSG_ON_BLUETOOTH_DEVICE_ADDED: {
438                    BluetoothDevice d = (BluetoothDevice)msg.obj;
439                    CachedBluetoothDevice cachedDevice = getCachedBluetoothDevice(d);
440                    onDeviceAddedInternal(cachedDevice);
441                    break;
442
443                }
444                case MSG_ON_BLE_SCAN_FAILED: {
445                    onBleScanFailedInternal();
446                    break;
447                }
448            }
449        }
450    }
451
452    private final class BluetoothDialogClickListener implements DialogInterface.OnClickListener {
453        @Override
454        public void onClick(DialogInterface dialog, int which) {
455            int enable = DialogInterface.BUTTON_POSITIVE == which ? 1 : 0;
456            mHandler.obtainMessage(MSG_ENABLE_BLUETOOTH, enable, 0).sendToTarget();
457            mDialog = null;
458        }
459    }
460
461    private final class KeyboardScanCallback extends ScanCallback {
462        @Override
463        public void onBatchScanResults(List<ScanResult> results) {
464            if (DEBUG) {
465                Slog.d(TAG, "onBatchScanResults(" + results.size() + ")");
466            }
467            if (!results.isEmpty()) {
468                BluetoothDevice bestDevice = results.get(0).getDevice();
469                int bestRssi = results.get(0).getRssi();
470                final int N = results.size();
471                for (int i = 0; i < N; i++) {
472                    ScanResult r = results.get(i);
473                    if (r.getRssi() > bestRssi) {
474                        bestDevice = r.getDevice();
475                    }
476                }
477                mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED, bestDevice).sendToTarget();
478            }
479        }
480
481        @Override
482        public void onScanFailed(int errorCode) {
483            if (DEBUG) {
484                Slog.d(TAG, "onScanFailed(" + errorCode + ")");
485            }
486            mHandler.obtainMessage(MSG_ON_BLE_SCAN_FAILED).sendToTarget();
487        }
488
489        @Override
490        public void onScanResult(int callbackType, ScanResult result) {
491            if (DEBUG) {
492                Slog.d(TAG, "onScanResult(" + callbackType + ", " + result + ")");
493            }
494            mHandler.obtainMessage(MSG_ON_BLUETOOTH_DEVICE_ADDED,
495                    result.getDevice()).sendToTarget();
496        }
497    }
498
499    private final class BluetoothCallbackHandler implements BluetoothCallback {
500        @Override
501        public void onBluetoothStateChanged(int bluetoothState) {
502            mHandler.obtainMessage(MSG_ON_BLUETOOTH_STATE_CHANGED,
503                    bluetoothState, 0).sendToTarget();
504        }
505
506        @Override
507        public void onDeviceBondStateChanged(CachedBluetoothDevice cachedDevice, int bondState) {
508            mHandler.obtainMessage(MSG_ON_DEVICE_BOND_STATE_CHANGED,
509                    bondState, 0, cachedDevice).sendToTarget();
510        }
511
512        @Override
513        public void onDeviceAdded(CachedBluetoothDevice cachedDevice) { }
514        @Override
515        public void onDeviceDeleted(CachedBluetoothDevice cachedDevice) { }
516        @Override
517        public void onScanningStateChanged(boolean started) { }
518        @Override
519        public void onConnectionStateChanged(CachedBluetoothDevice cachedDevice, int state) { }
520    }
521
522    private static String stateToString(int state) {
523        switch (state) {
524            case STATE_NOT_ENABLED:
525                return "STATE_NOT_ENABLED";
526            case STATE_WAITING_FOR_BOOT_COMPLETED:
527                return "STATE_WAITING_FOR_BOOT_COMPLETED";
528            case STATE_WAITING_FOR_TABLET_MODE_EXIT:
529                return "STATE_WAITING_FOR_TABLET_MODE_EXIT";
530            case STATE_WAITING_FOR_DEVICE_DISCOVERY:
531                return "STATE_WAITING_FOR_DEVICE_DISCOVERY";
532            case STATE_WAITING_FOR_BLUETOOTH:
533                return "STATE_WAITING_FOR_BLUETOOTH";
534            case STATE_WAITING_FOR_STATE_PAIRED:
535                return "STATE_WAITING_FOR_STATE_PAIRED";
536            case STATE_PAIRING:
537                return "STATE_PAIRING";
538            case STATE_PAIRED:
539                return "STATE_PAIRED";
540            case STATE_USER_CANCELLED:
541                return "STATE_USER_CANCELLED";
542            case STATE_DEVICE_NOT_FOUND:
543                return "STATE_DEVICE_NOT_FOUND";
544            case STATE_UNKNOWN:
545            default:
546                return "STATE_UNKNOWN";
547        }
548    }
549}
550