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    /**
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