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
17package com.android.tv.settings.accessories;
18
19import android.bluetooth.BluetoothAdapter;
20import android.bluetooth.BluetoothDevice;
21import android.bluetooth.BluetoothInputDevice;
22import android.bluetooth.BluetoothProfile;
23import android.content.BroadcastReceiver;
24import android.content.Context;
25import android.content.Intent;
26import android.content.IntentFilter;
27import android.hardware.input.InputManager;
28import android.os.Handler;
29import android.os.Message;
30import android.os.SystemClock;
31import android.util.Log;
32import android.view.InputDevice;
33
34import com.android.tv.settings.util.bluetooth.BluetoothScanner;
35import com.android.tv.settings.R;
36
37import java.util.ArrayList;
38import java.util.List;
39
40/**
41 * Monitors available Bluetooth input devices and manages process of pairing
42 * and connecting to the device.
43 */
44public 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    public void clearDeviceList() {
356        doCancel();
357        mVisibleDevices.clear();
358    }
359
360    /**
361     * Stop any pairing request that is in progress.
362     */
363    public void cancelPairing() {
364        mAutoMode = false;
365        doCancel();
366    }
367
368    /**
369     * Stop doing anything we're doing, release any resources.
370     */
371    public void dispose() {
372        mHandler.removeCallbacksAndMessages(null);
373        if (mLinkReceiverRegistered) {
374            unregisterLinkStatusReceiver();
375        }
376        stopScanning();
377    }
378
379    /**
380     * Start pairing and connection to the specified device.
381     * @param device
382     */
383    public void startPairing(BluetoothDevice device) {
384        startPairing(device, DELAY_MANUAL_PAIRING);
385    }
386
387    /**
388     * See {@link #startPairing(BluetoothDevice)}.
389     * @param delay The delay before pairing starts. In this window, cancel may
390     * be called.
391     */
392    public void startPairing(BluetoothDevice device, int delay) {
393        startPairing(device, delay, true);
394    }
395
396    /**
397     * Return our state
398     * @return One of the STATE_ constants.
399     */
400    public int getStatus() {
401        return mStatus;
402    }
403
404    /**
405     * Get the device that we're currently targeting. This will be null if
406     * there is no device that is in the process of being connected to.
407     */
408    public BluetoothDevice getTargetDevice() {
409        return mTarget;
410    }
411
412    /**
413     * When the timer to start the next stage will expire, in {@link SystemClock#elapsedRealtime()}.
414     * Will only be valid while waiting to pair and after an error from which we are restarting.
415     */
416    public long getNextStageTime() {
417        return mNextStageTimestamp;
418    }
419
420    public List<BluetoothDevice> getAvailableDevices() {
421        ArrayList<BluetoothDevice> copy = new ArrayList<BluetoothDevice>(mVisibleDevices.size());
422        copy.addAll(mVisibleDevices);
423        return copy;
424    }
425
426    public void setListener(EventListener listener) {
427        mListener = listener;
428    }
429
430    public void invalidateDevice(BluetoothDevice device) {
431        onDeviceLost(device);
432    }
433
434    private void startPairing(BluetoothDevice device, int delay, boolean isManual) {
435        // TODO check if we're already paired/bonded to this device
436
437        // cancel auto-mode if applicable
438        mAutoMode = !isManual;
439
440        mTarget = device;
441
442        if (isInProgress()) {
443            throw new RuntimeException("Pairing already in progress, you must cancel the " +
444                    "previous request first");
445        }
446
447        mHandler.removeMessages(MSG_PAIR);
448        mHandler.removeMessages(MSG_START);
449
450        mNextStageTimestamp = SystemClock.elapsedRealtime() +
451                (mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING);
452        mHandler.sendEmptyMessageDelayed(MSG_PAIR,
453                mAutoMode ? DELAY_AUTO_PAIRING : DELAY_MANUAL_PAIRING);
454
455        setStatus(STATUS_WAITING_TO_PAIR);
456    }
457
458    /**
459     * Pairing is in progress and is no longer cancelable.
460     */
461    public boolean isInProgress() {
462        return mStatus != STATUS_NONE && mStatus != STATUS_ERROR && mStatus != STATUS_SCANNING &&
463                mStatus != STATUS_WAITING_TO_PAIR;
464    }
465
466    private void updateListener() {
467        if (mListener != null) {
468            mListener.statusChanged();
469        }
470    }
471
472    private void onDeviceFound(BluetoothDevice device) {
473        if (!mVisibleDevices.contains(device)) {
474            mVisibleDevices.add(device);
475            Log.d(TAG, "Added device to visible list. Name = " + device.getName() + " , class = " +
476                    device.getBluetoothClass().getDeviceClass());
477        } else {
478            return;
479        }
480
481        updatePairingState();
482        // update the listener because a new device is visible
483        updateListener();
484    }
485
486    private void onDeviceLost(BluetoothDevice device) {
487        // TODO validate removal works as expected
488        if (mVisibleDevices.remove(device)) {
489            updatePairingState();
490            // update the listener because a device disappeared
491            updateListener();
492        }
493    }
494
495    private void updatePairingState() {
496        if (mAutoMode) {
497            if (isReadyToAutoPair()) {
498                mTarget = mVisibleDevices.get(0);
499                startPairing(mTarget, DELAY_AUTO_PAIRING, false);
500            } else {
501                doCancel();
502            }
503        }
504    }
505
506    /**
507     * @return {@code true} If there is only one visible input device.
508     */
509    private boolean isReadyToAutoPair() {
510        // we imagine that the conditions under which we decide to pair or
511        // not may one day become more complicated, which is why this length
512        // check is wrapped in a method call.
513        return mVisibleDevices.size() == 1;
514    }
515
516    private void doCancel() {
517        // TODO allow cancel to be called from any state
518        if (isInProgress()) {
519            Log.d(TAG, "Pairing process has already begun, it can not be canceled.");
520            return;
521        }
522
523        // stop scanning, just in case we are
524        BluetoothScanner.stopListening(mBtListener);
525        BluetoothScanner.stopNow();
526
527        // remove any callbacks
528        mHandler.removeMessages(MSG_PAIR);
529
530        // remove bond, if existing
531        unpairDevice(mTarget);
532
533        mTarget = null;
534
535        setStatus(STATUS_NONE);
536
537        // resume scanning
538        start();
539    }
540
541    /**
542     * Set the status and update any listener.
543     */
544    private void setStatus(int status) {
545        mStatus = status;
546        updateListener();
547    }
548
549    private void startBonding() {
550        stopScanning();
551        setStatus(STATUS_PAIRING);
552        if (mTarget.getBondState() != BluetoothDevice.BOND_BONDED) {
553            registerLinkStatusReceiver();
554
555            // create bond (pair) to the device
556            mTarget.createBond();
557        } else {
558            onBonded();
559        }
560    }
561
562    private void onBonded() {
563        openConnection();
564    }
565
566    private void openConnection() {
567        setStatus(STATUS_CONNECTING);
568
569        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
570
571        // connect to the Bluetooth service, then registerInputListener
572        adapter.getProfileProxy(mContext, mServiceConnection, BluetoothProfile.INPUT_DEVICE);
573    }
574
575    private void onInputAdded() {
576        unregisterInputMethodMonitor();
577        BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
578        adapter.closeProfileProxy(BluetoothProfile.INPUT_DEVICE, mInputProxy);
579        setStatus(STATUS_NONE);
580    }
581
582    private void onBondFailed() {
583        Log.w(TAG, "There was an error bonding with the device.");
584        setStatus(STATUS_ERROR);
585
586        // remove bond, if existing
587        unpairDevice(mTarget);
588
589        // TODO do we need to check Bluetooth for the device and possible delete it?
590        mNextStageTimestamp = SystemClock.elapsedRealtime() + DELAY_RETRY;
591        mHandler.sendEmptyMessageDelayed(MSG_START, DELAY_RETRY);
592    }
593
594    private void registerInputMethodMonitor() {
595        InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE);
596        inputManager.registerInputDeviceListener(mInputListener, mHandler);
597
598        // TO DO: The line below is a workaround for an issue in InputManager.
599        // The manager doesn't actually registers itself with the InputService
600        // unless we query it for input devices. We should remove this once
601        // the problem is fixed in InputManager.
602        // Reference bug in Frameworks: b/10415556
603        int[] inputDevices = inputManager.getInputDeviceIds();
604    }
605
606    private void unregisterInputMethodMonitor() {
607        InputManager inputManager = (InputManager) mContext.getSystemService(Context.INPUT_SERVICE);
608        inputManager.unregisterInputDeviceListener(mInputListener);
609    }
610
611    private void registerLinkStatusReceiver() {
612        mLinkReceiverRegistered = true;
613        IntentFilter filter = new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED);
614        mContext.registerReceiver(mLinkStatusReceiver, filter);
615    }
616
617    private void unregisterLinkStatusReceiver() {
618        mLinkReceiverRegistered = false;
619        mContext.unregisterReceiver(mLinkStatusReceiver);
620    }
621
622    private void stopScanning() {
623        BluetoothScanner.stopListening(mBtListener);
624        BluetoothScanner.stopNow();
625    }
626
627    public boolean unpairDevice(BluetoothDevice device) {
628        if (device != null) {
629            int state = device.getBondState();
630
631            if (state == BluetoothDevice.BOND_BONDING) {
632                device.cancelBondProcess();
633            }
634
635            if (state != BluetoothDevice.BOND_NONE) {
636                final boolean successful = device.removeBond();
637                if (successful) {
638                    if (DEBUG) {
639                        Log.d(TAG, "Bluetooth device successfully unpaired: " + device.getName());
640                    }
641                    return true;
642                } else {
643                    Log.e(TAG, "Failed to unpair Bluetooth Device: " + device.getName());
644                }
645            }
646        }
647        return false;
648    }
649}
650