1 /*
2  * Copyright (C) 2014 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.tv.settings.accessories;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothClass;
21 import android.bluetooth.BluetoothDevice;
22 import android.bluetooth.IBluetoothA2dp;
23 import android.content.BroadcastReceiver;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.Intent;
27 import android.content.IntentFilter;
28 import android.content.pm.PackageManager;
29 import android.hardware.input.InputManager;
30 import android.os.Handler;
31 import android.os.Message;
32 import android.os.SystemClock;
33 import android.util.Log;
34 import android.view.InputDevice;
35 
36 import com.android.tv.settings.util.bluetooth.BluetoothDeviceCriteria;
37 import com.android.tv.settings.util.bluetooth.BluetoothScanner;
38 
39 import java.util.ArrayList;
40 import java.util.List;
41 
42 /**
43  * Monitors available Bluetooth devices and manages process of pairing
44  * and connecting to the device.
45  */
46 public class BluetoothDevicePairer {
47 
48     /**
49      * This class operates in two modes, automatic and manual.
50      *
51      * AUTO MODE
52      * In auto mode we listen for an input device that looks like it can
53      * generate DPAD events. When one is found we wait
54      * {@link #DELAY_AUTO_PAIRING} milliseconds before starting the process of
55      * connecting to the device. The idea is that a UI making use of this class
56      * would give the user a chance to cancel pairing during this window. Once
57      * the connection process starts, it is considered uninterruptible.
58      *
59      * Connection is accomplished in two phases, bonding and socket connection.
60      * First we try to create a bond to the device and listen for bond status
61      * change broadcasts. Once the bond is made, we connect to the device.
62      * Connecting to the device actually opens a socket and hooks the device up
63      * to the input system.
64      *
65      * In auto mode if we see more than one compatible input device before
66      * bonding with a candidate device, we stop the process. We don't want to
67      * connect to the wrong device and it is up to the user of this class to
68      * tell us what to connect to.
69      *
70      * MANUAL MODE
71      * Manual mode is where a user of this class explicitly tells us which
72      * device to connect to. To switch to manual mode you can call
73      * {@link #cancelPairing()}. It is safe to call this method even if no
74      * device connection process is underway. You would then call
75      * {@link #start()} to resume scanning for devices. Once one is found
76      * that you want to connect to, call {@link #startPairing(BluetoothDevice)}
77      * to start the connection process. At this point the same process is
78      * followed as when we start connection in auto mode.
79      *
80      * Even in manual mode there is a timeout before we actually start
81      * connecting, but it is {@link #DELAY_MANUAL_PAIRING}.
82      */
83 
84     public static final String TAG = "BluetoothDevicePairer";
85     public static final int STATUS_ERROR = -1;
86     public static final int STATUS_NONE = 0;
87     public static final int STATUS_SCANNING = 1;
88     /**
89      * A device to pair with has been identified, we're currently in the
90      * timeout period where the process can be cancelled.
91      */
92     public static final int STATUS_WAITING_TO_PAIR = 2;
93     /**
94      * Pairing is in progress.
95      */
96     public static final int STATUS_PAIRING = 3;
97     /**
98      * Device has been paired with, we are opening a connection to the device.
99      */
100     public static final int STATUS_CONNECTING = 4;
101 
102 
103     public interface EventListener {
104         /**
105          * The status of the {@link BluetoothDevicePairer} changed.
106          */
statusChanged()107         void statusChanged();
108     }
109 
110     public interface BluetoothConnector {
openConnection(BluetoothAdapter adapter)111         void openConnection(BluetoothAdapter adapter);
112     }
113 
114     public interface OpenConnectionCallback {
115         /**
116          * Call back when BT device connection is completed.
117          */
succeeded()118         void succeeded();
failed()119         void failed();
120     }
121 
122     /**
123      * Time between when a single input device is found and pairing begins. If
124      * one or more other input devices are found before this timeout or
125      * {@link #cancelPairing()} is called then pairing will not proceed.
126      */
127     public static final int DELAY_AUTO_PAIRING = 15 * 1000;
128     /**
129      * Time between when the call to {@link #startPairing(BluetoothDevice)} is
130      * called and when we actually start pairing. This gives the caller a
131      * chance to change their mind.
132      */
133     public static final int DELAY_MANUAL_PAIRING = 5 * 1000;
134     /**
135      * If there was an error in pairing, we will wait this long before trying
136      * again.
137      */
138     public static final int DELAY_RETRY = 5 * 1000;
139 
140     private static final int MSG_PAIR = 1;
141     private static final int MSG_START = 2;
142 
143     private static final boolean DEBUG = true;
144 
145     private static final String[] INVALID_INPUT_KEYBOARD_DEVICE_NAMES = {
146         "gpio-keypad", "cec_keyboard", "Virtual", "athome_remote"
147     };
148 
149     private final BluetoothScanner.Listener mBtListener = new BluetoothScanner.Listener() {
150         @Override
151         public void onDeviceAdded(BluetoothScanner.Device device) {
152             if (DEBUG) {
153                 Log.d(TAG, "Adding device: " + device.btDevice.getAddress());
154             }
155             onDeviceFound(device.btDevice);
156         }
157 
158         @Override
159         public void onDeviceRemoved(BluetoothScanner.Device device) {
160             if (DEBUG) {
161                 Log.d(TAG, "Device lost: " + device.btDevice.getAddress());
162             }
163             onDeviceLost(device.btDevice);
164         }
165     };
166 
hasValidInputDevice(Context context, int[] deviceIds)167     public static boolean hasValidInputDevice(Context context, int[] deviceIds) {
168         InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
169 
170         for (int ptr = deviceIds.length - 1; ptr > -1; ptr--) {
171             InputDevice device = inMan.getInputDevice(deviceIds[ptr]);
172             int sources = device.getSources();
173 
174             boolean isCompatible = false;
175 
176             if ((sources & InputDevice.SOURCE_DPAD) == InputDevice.SOURCE_DPAD) {
177                 isCompatible = true;
178             }
179 
180             if ((sources & InputDevice.SOURCE_GAMEPAD) == InputDevice.SOURCE_GAMEPAD) {
181                 isCompatible = true;
182             }
183 
184             if ((sources & InputDevice.SOURCE_KEYBOARD) == InputDevice.SOURCE_KEYBOARD) {
185                 boolean isValidKeyboard = true;
186                 String keyboardName = device.getName();
187                 for (int index = 0; index < INVALID_INPUT_KEYBOARD_DEVICE_NAMES.length; ++index) {
188                     if (keyboardName.equals(INVALID_INPUT_KEYBOARD_DEVICE_NAMES[index])) {
189                         isValidKeyboard = false;
190                         break;
191                     }
192                 }
193 
194                 if (isValidKeyboard) {
195                     isCompatible = true;
196                 }
197             }
198 
199             if (!device.isVirtual() && isCompatible) {
200                 return true;
201             }
202         }
203         return false;
204     }
205 
hasValidInputDevice(Context context)206     public static boolean hasValidInputDevice(Context context) {
207         InputManager inMan = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
208         int[] inputDevices = inMan.getInputDeviceIds();
209 
210         return hasValidInputDevice(context, inputDevices);
211     }
212 
213     private final BroadcastReceiver mLinkStatusReceiver = new BroadcastReceiver() {
214         @Override
215         public void onReceive(Context context, Intent intent) {
216             BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
217             if (DEBUG) {
218                 Log.d(TAG, "There was a link status change for: " + device.getAddress());
219             }
220 
221             if (device.equals(mTarget)) {
222                 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE,
223                         BluetoothDevice.BOND_NONE);
224                 int previousBondState = intent.getIntExtra(
225                         BluetoothDevice.EXTRA_PREVIOUS_BOND_STATE, BluetoothDevice.BOND_NONE);
226 
227                 if (DEBUG) {
228                     Log.d(TAG, "Bond states: old = " + previousBondState + ", new = " +
229                         bondState);
230                 }
231 
232                 if (bondState == BluetoothDevice.BOND_NONE &&
233                         previousBondState == BluetoothDevice.BOND_BONDING) {
234                     // we seem to have reverted, this is an error
235                     // TODO inform user, start scanning again
236                     unregisterLinkStatusReceiver();
237                     onBondFailed();
238                 } else if (bondState == BluetoothDevice.BOND_BONDED) {
239                     unregisterLinkStatusReceiver();
240                     onBonded();
241                 }
242             }
243         }
244     };
245 
246     private BroadcastReceiver mBluetoothStateReceiver;
247 
248     private final OpenConnectionCallback mOpenConnectionCallback = new OpenConnectionCallback() {
249         public void succeeded() {
250             setStatus(STATUS_NONE);
251         }
252         public void failed() {
253             setStatus(STATUS_ERROR);
254         }
255     };
256 
257     private final Context mContext;
258     private EventListener mListener;
259     private int mStatus = STATUS_NONE;
260     /**
261      * Set to {@code false} when {@link #cancelPairing()} or
262      * {@link #startPairing(BluetoothDevice)}. This instance
263      * will now no longer automatically start pairing.
264      */
265     private boolean mAutoMode = true;
266     private final ArrayList<BluetoothDevice> mVisibleDevices = new ArrayList<>();
267     private BluetoothDevice mTarget;
268     private final Handler mHandler;
269     private long mNextStageTimestamp = -1;
270     private boolean mLinkReceiverRegistered = false;
271     private final ArrayList<BluetoothDeviceCriteria> mBluetoothDeviceCriteria = new ArrayList<>();
272     private InputDeviceCriteria mInputDeviceCriteria;
273 
274     /**
275      * Should be instantiated on a thread with a Looper, perhaps the main thread!
276      */
BluetoothDevicePairer(Context context, EventListener listener)277     public BluetoothDevicePairer(Context context, EventListener listener) {
278         mContext = context.getApplicationContext();
279         mListener = listener;
280 
281         addBluetoothDeviceCriteria();
282 
283         mHandler = new Handler() {
284             @Override
285             public void handleMessage(Message msg) {
286                 switch (msg.what) {
287                     case MSG_PAIR:
288                         startBonding();
289                         break;
290                     case MSG_START:
291                         start();
292                         break;
293                     default:
294                         Log.d(TAG, "No handler case available for message: " + msg.what);
295                 }
296             }
297         };
298     }
299 
addBluetoothDeviceCriteria()300     private void addBluetoothDeviceCriteria() {
301         // Input is supported by all devices.
302         mInputDeviceCriteria = new InputDeviceCriteria();
303         mBluetoothDeviceCriteria.add(mInputDeviceCriteria);
304 
305         // Add Bluetooth a2dp on if the service is running and the
306         // setting profile_supported_a2dp is set to true.
307         Intent intent = new Intent(IBluetoothA2dp.class.getName());
308         ComponentName comp = intent.resolveSystemService(mContext.getPackageManager(), 0);
309         if (comp != null) {
310             int enabledState = mContext.getPackageManager().getComponentEnabledSetting(comp);
311             if (enabledState != PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
312                 Log.d(TAG, "Adding A2dp device criteria for pairing");
313                 mBluetoothDeviceCriteria.add(new A2dpDeviceCriteria());
314             }
315         }
316     }
317 
318     /**
319      * Start listening for devices and begin the pairing process when
320      * criteria is met.
321      */
start()322     public void start() {
323         final BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
324         if (!bluetoothAdapter.isEnabled()) {
325             Log.d(TAG, "Bluetooth not enabled, delaying startup.");
326             if (mBluetoothStateReceiver == null) {
327                 mBluetoothStateReceiver = new BroadcastReceiver() {
328                     @Override
329                     public void onReceive(Context context, Intent intent) {
330                         if (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
331                                 BluetoothAdapter.STATE_OFF) == BluetoothAdapter.STATE_ON) {
332                             Log.d(TAG, "Bluetooth now enabled, starting.");
333                             start();
334                         } else {
335                             Log.d(TAG, "Bluetooth not yet started, got broadcast: " + intent);
336                         }
337                     }
338                 };
339                 mContext.registerReceiver(mBluetoothStateReceiver,
340                         new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED));
341             }
342 
343             bluetoothAdapter.enable();
344             return;
345         } else {
346             if (mBluetoothStateReceiver != null) {
347                 mContext.unregisterReceiver(mBluetoothStateReceiver);
348                 mBluetoothStateReceiver = null;
349             }
350         }
351 
352         // set status to scanning before we start listening since
353         // startListening may result in a transition to STATUS_WAITING_TO_PAIR
354         // which might seem odd from a client perspective
355         setStatus(STATUS_SCANNING);
356 
357         BluetoothScanner.startListening(mContext, mBtListener, mBluetoothDeviceCriteria);
358     }
359 
clearDeviceList()360     public void clearDeviceList() {
361         doCancel();
362         mVisibleDevices.clear();
363     }
364 
365     /**
366      * Stop any pairing request that is in progress.
367      */
cancelPairing()368     public void cancelPairing() {
369         mAutoMode = false;
370         doCancel();
371     }
372 
373 
374     /**
375      * Switch to manual pairing mode.
376      */
disableAutoPairing()377     public void disableAutoPairing() {
378         mAutoMode = false;
379     }
380 
381     /**
382      * Stop doing anything we're doing, release any resources.
383      */
dispose()384     public void dispose() {
385         mHandler.removeCallbacksAndMessages(null);
386         if (mLinkReceiverRegistered) {
387             unregisterLinkStatusReceiver();
388         }
389         if (mBluetoothStateReceiver != null) {
390             mContext.unregisterReceiver(mBluetoothStateReceiver);
391         }
392         stopScanning();
393     }
394 
395     /**
396      * Start pairing and connection to the specified device.
397      * @param device device
398      */
startPairing(BluetoothDevice device)399     public void startPairing(BluetoothDevice device) {
400         startPairing(device, true);
401     }
402 
403     /**
404      * Return our state
405      * @return One of the STATE_ constants.
406      */
getStatus()407     public int getStatus() {
408         return mStatus;
409     }
410 
411     /**
412      * Get the device that we're currently targeting. This will be null if
413      * there is no device that is in the process of being connected to.
414      */
getTargetDevice()415     public BluetoothDevice getTargetDevice() {
416         return mTarget;
417     }
418 
419     /**
420      * When the timer to start the next stage will expire, in {@link SystemClock#elapsedRealtime()}.
421      * Will only be valid while waiting to pair and after an error from which we are restarting.
422      */
getNextStageTime()423     public long getNextStageTime() {
424         return mNextStageTimestamp;
425     }
426 
getAvailableDevices()427     public List<BluetoothDevice> getAvailableDevices() {
428         ArrayList<BluetoothDevice> copy = new ArrayList<>(mVisibleDevices.size());
429         copy.addAll(mVisibleDevices);
430         return copy;
431     }
432 
setListener(EventListener listener)433     public void setListener(EventListener listener) {
434         mListener = listener;
435     }
436 
invalidateDevice(BluetoothDevice device)437     public void invalidateDevice(BluetoothDevice device) {
438         onDeviceLost(device);
439     }
440 
startPairing(BluetoothDevice device, boolean isManual)441     private void startPairing(BluetoothDevice device, boolean isManual) {
442         // TODO check if we're already paired/bonded to this device
443 
444         // cancel auto-mode if applicable
445         mAutoMode = !isManual;
446 
447         mTarget = device;
448 
449         if (isInProgress()) {
450             throw new RuntimeException("Pairing already in progress, you must cancel the " +
451                     "previous request first");
452         }
453 
454         mHandler.removeCallbacksAndMessages(null);
455 
456         mNextStageTimestamp = SystemClock.elapsedRealtime() +
457                 (mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING);
458         mHandler.sendEmptyMessageDelayed(MSG_PAIR,
459                 mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING);
460 
461         setStatus(STATUS_WAITING_TO_PAIR);
462     }
463 
464     /**
465      * Pairing is in progress and is no longer cancelable.
466      */
isInProgress()467     public boolean isInProgress() {
468         return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING &&
469                 mStatus != STATUS_WAITING_TO_PAIR;
470     }
471 
updateListener()472     private void updateListener() {
473         if (mListener != null) {
474             mListener.statusChanged();
475         }
476     }
477 
onDeviceFound(BluetoothDevice device)478     private void onDeviceFound(BluetoothDevice device) {
479         if (!mVisibleDevices.contains(device)) {
480             mVisibleDevices.add(device);
481             Log.d(TAG, "Added device to visible list. Name = " + device.getName() + " , class = " +
482                     device.getBluetoothClass().getDeviceClass());
483         } else {
484             return;
485         }
486 
487         updatePairingState();
488         // update the listener because a new device is visible
489         updateListener();
490     }
491 
onDeviceLost(BluetoothDevice device)492     private void onDeviceLost(BluetoothDevice device) {
493         // TODO validate removal works as expected
494         if (mVisibleDevices.remove(device)) {
495             updatePairingState();
496             // update the listener because a device disappeared
497             updateListener();
498         }
499     }
500 
updatePairingState()501     private void updatePairingState() {
502         if (mAutoMode) {
503             BluetoothDevice candidate = getAutoPairDevice();
504             if (null != candidate) {
505                 mTarget = candidate;
506                 startPairing(mTarget, false);
507             } else {
508                 doCancel();
509             }
510         }
511     }
512 
513     /**
514      * @return returns the only visible input device if there is only one
515      */
getAutoPairDevice()516     private BluetoothDevice getAutoPairDevice() {
517         List<BluetoothDevice> inputDevices = new ArrayList<>();
518         for (BluetoothDevice device : mVisibleDevices) {
519             if (mInputDeviceCriteria.isInputDevice(device.getBluetoothClass())) {
520                 inputDevices.add(device);
521             }
522         }
523         if (inputDevices.size() == 1) {
524             return inputDevices.get(0);
525         }
526         return null;
527     }
528 
doCancel()529     private void doCancel() {
530         // TODO allow cancel to be called from any state
531         if (isInProgress()) {
532             Log.d(TAG, "Pairing process has already begun, it can not be canceled.");
533             return;
534         }
535 
536         // stop scanning, just in case we are
537         final boolean wasListening = BluetoothScanner.stopListening(mBtListener);
538         BluetoothScanner.stopNow();
539 
540         mHandler.removeCallbacksAndMessages(null);
541 
542         // remove bond, if existing
543         unpairDevice(mTarget);
544 
545         mTarget = null;
546 
547         setStatus(STATUS_NONE);
548 
549         // resume scanning
550         if (wasListening) {
551             start();
552         }
553     }
554 
555     /**
556      * Set the status and update any listener.
557      */
setStatus(int status)558     private void setStatus(int status) {
559         mStatus = status;
560         updateListener();
561     }
562 
startBonding()563     private void startBonding() {
564         stopScanning();
565         setStatus(STATUS_PAIRING);
566         if (mTarget.getBondState() != BluetoothDevice.BOND_BONDED) {
567             registerLinkStatusReceiver();
568 
569             // create bond (pair) to the device
570             mTarget.createBond();
571         } else {
572             onBonded();
573         }
574     }
575 
onBonded()576     private void onBonded() {
577         openConnection();
578     }
579 
openConnection()580     private void openConnection() {
581         BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
582         BluetoothConnector btConnector = getBluetoothConnector();
583         if (btConnector != null) {
584             setStatus(STATUS_CONNECTING);
585             btConnector.openConnection(adapter);
586         } else {
587             Log.w(TAG, "There was an error getting the BluetoothConnector.");
588             setStatus(STATUS_ERROR);
589             if (mLinkReceiverRegistered) {
590                 unregisterLinkStatusReceiver();
591             }
592             unpairDevice(mTarget);
593         }
594     }
595 
onBondFailed()596     private void onBondFailed() {
597         Log.w(TAG, "There was an error bonding with the device.");
598         setStatus(STATUS_ERROR);
599 
600         // remove bond, if existing
601         unpairDevice(mTarget);
602 
603         // TODO do we need to check Bluetooth for the device and possible delete it?
604         mNextStageTimestamp = SystemClock.elapsedRealtime() + DELAY_RETRY;
605         mHandler.sendEmptyMessageDelayed(MSG_START, DELAY_RETRY);
606     }
607 
registerLinkStatusReceiver()608     private void registerLinkStatusReceiver() {
609         mLinkReceiverRegistered = true;
610         IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
611         mContext.registerReceiver(mLinkStatusReceiver, filter);
612     }
613 
unregisterLinkStatusReceiver()614     private void unregisterLinkStatusReceiver() {
615         mLinkReceiverRegistered = false;
616         mContext.unregisterReceiver(mLinkStatusReceiver);
617     }
618 
stopScanning()619     private void stopScanning() {
620         BluetoothScanner.stopListening(mBtListener);
621         BluetoothScanner.stopNow();
622     }
623 
unpairDevice(BluetoothDevice device)624     public boolean unpairDevice(BluetoothDevice device) {
625         if (device != null) {
626             int state = device.getBondState();
627 
628             if (state == BluetoothDevice.BOND_BONDING) {
629                 device.cancelBondProcess();
630             }
631 
632             if (state != BluetoothDevice.BOND_NONE) {
633                 final boolean successful = device.removeBond();
634                 if (successful) {
635                     if (DEBUG) {
636                         Log.d(TAG, "Bluetooth device successfully unpaired: " + device.getName());
637                     }
638                     return true;
639                 } else {
640                     Log.e(TAG, "Failed to unpair Bluetooth Device: " + device.getName());
641                 }
642             }
643         }
644         return false;
645     }
646 
getBluetoothConnector()647     private BluetoothConnector getBluetoothConnector() {
648         int majorDeviceClass = mTarget.getBluetoothClass().getMajorDeviceClass();
649         switch (majorDeviceClass) {
650             case BluetoothClass.Device.Major.PERIPHERAL:
651                 return new BluetoothInputDeviceConnector(
652                     mContext, mTarget, mHandler, mOpenConnectionCallback);
653             case BluetoothClass.Device.Major.AUDIO_VIDEO:
654                 return new BluetoothA2dpConnector(mContext, mTarget, mOpenConnectionCallback);
655             default:
656                 Log.d(TAG, "Unhandle device class: " + majorDeviceClass);
657                 break;
658         }
659         return null;
660     }
661 }
662