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