blob: d931856a822b1f161b21f4e276c896622a398c1d [file] [log] [blame]
Michael Wright9209c9c2015-09-03 17:57:01 +01001/*
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;
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -070026import android.bluetooth.le.ScanRecord;
Michael Wright9209c9c2015-09-03 17:57:01 +010027import 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
Yohei Yukawa8ce2a532015-11-25 20:35:04 -0800183 InputManager im = context.getSystemService(InputManager.class);
Michael Wright9209c9c2015-09-03 17:57:01 +0100184 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();
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -0700234 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();
Michael Wright9209c9c2015-09-03 17:57:01 +0100247 }
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 {
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -0700465
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
Michael Wright9209c9c2015-09-03 17:57:01 +0100474 @Override
475 public void onBatchScanResults(List<ScanResult> results) {
476 if (DEBUG) {
477 Slog.d(TAG, "onBatchScanResults(" + results.size() + ")");
478 }
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -0700479
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);
Michael Wright9209c9c2015-09-03 17:57:01 +0100486 }
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -0700487
488 if (isDeviceDiscoverable(result) && result.getRssi() > bestRssi) {
489 bestDevice = result.getDevice();
490 bestRssi = result.getRssi();
491 }
492 }
493
494 if (bestDevice != null) {
Michael Wright9209c9c2015-09-03 17:57:01 +0100495 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 }
Dmitry Torokhov79f00cf2015-10-22 10:07:53 -0700512
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 }
Michael Wright9209c9c2015-09-03 17:57:01 +0100520 }
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}