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