Home | History | Annotate | Download | only in accessories
      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          */
    107         void statusChanged();
    108     }
    109 
    110     public interface BluetoothConnector {
    111         void openConnection(BluetoothAdapter adapter);
    112     }
    113 
    114     public interface OpenConnectionCallback {
    115         /**
    116          * Call back when BT device connection is completed.
    117          */
    118         void succeeded();
    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 
    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 
    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      */
    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 
    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      */
    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 
    360     public void clearDeviceList() {
    361         doCancel();
    362         mVisibleDevices.clear();
    363     }
    364 
    365     /**
    366      * Stop any pairing request that is in progress.
    367      */
    368     public void cancelPairing() {
    369         mAutoMode = false;
    370         doCancel();
    371     }
    372 
    373 
    374     /**
    375      * Switch to manual pairing mode.
    376      */
    377     public void disableAutoPairing() {
    378         mAutoMode = false;
    379     }
    380 
    381     /**
    382      * Stop doing anything we're doing, release any resources.
    383      */
    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      */
    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      */
    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      */
    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      */
    423     public long getNextStageTime() {
    424         return mNextStageTimestamp;
    425     }
    426 
    427     public List<BluetoothDevice> getAvailableDevices() {
    428         ArrayList<BluetoothDevice> copy = new ArrayList<>(mVisibleDevices.size());
    429         copy.addAll(mVisibleDevices);
    430         return copy;
    431     }
    432 
    433     public void setListener(EventListener listener) {
    434         mListener = listener;
    435     }
    436 
    437     public void invalidateDevice(BluetoothDevice device) {
    438         onDeviceLost(device);
    439     }
    440 
    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      */
    467     public boolean isInProgress() {
    468         return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING &&
    469                 mStatus != STATUS_WAITING_TO_PAIR;
    470     }
    471 
    472     private void updateListener() {
    473         if (mListener != null) {
    474             mListener.statusChanged();
    475         }
    476     }
    477 
    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 
    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 
    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      */
    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 
    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      */
    558     private void setStatus(int status) {
    559         mStatus = status;
    560         updateListener();
    561     }
    562 
    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 
    576     private void onBonded() {
    577         openConnection();
    578     }
    579 
    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 
    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 
    608     private void registerLinkStatusReceiver() {
    609         mLinkReceiverRegistered = true;
    610         IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
    611         mContext.registerReceiver(mLinkStatusReceiver, filter);
    612     }
    613 
    614     private void unregisterLinkStatusReceiver() {
    615         mLinkReceiverRegistered = false;
    616         mContext.unregisterReceiver(mLinkStatusReceiver);
    617     }
    618 
    619     private void stopScanning() {
    620         BluetoothScanner.stopListening(mBtListener);
    621         BluetoothScanner.stopNow();
    622     }
    623 
    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 
    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