1/*
2 * Copyright (C) 2016 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 */
16package com.android.settings.bluetooth;
17
18import android.bluetooth.BluetoothClass;
19import android.bluetooth.BluetoothDevice;
20import android.content.Context;
21import android.content.Intent;
22import android.text.Editable;
23import android.util.Log;
24import android.widget.CompoundButton;
25import android.widget.CompoundButton.OnCheckedChangeListener;
26import com.android.settings.R;
27import com.android.settings.bluetooth.BluetoothPairingDialogFragment.BluetoothPairingDialogListener;
28import com.android.settingslib.bluetooth.LocalBluetoothManager;
29import com.android.settingslib.bluetooth.LocalBluetoothProfile;
30import java.util.Locale;
31
32/**
33 * A controller used by {@link BluetoothPairingDialog} to manage connection state while we try to
34 * pair with a bluetooth device. It includes methods that allow the
35 * {@link BluetoothPairingDialogFragment} to interrogate the current state as well.
36 */
37public class BluetoothPairingController implements OnCheckedChangeListener,
38        BluetoothPairingDialogListener {
39
40    private static final String TAG = "BTPairingController";
41
42    // Different types of dialogs we can map to
43    public static final int INVALID_DIALOG_TYPE = -1;
44    public static final int USER_ENTRY_DIALOG = 0;
45    public static final int CONFIRMATION_DIALOG = 1;
46    public static final int DISPLAY_PASSKEY_DIALOG = 2;
47
48    private static final int BLUETOOTH_PIN_MAX_LENGTH = 16;
49    private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6;
50
51    // Bluetooth dependencies for the connection we are trying to establish
52    private LocalBluetoothManager mBluetoothManager;
53    private BluetoothDevice mDevice;
54    private int mType;
55    private String mUserInput;
56    private String mPasskeyFormatted;
57    private int mPasskey;
58    private String mDeviceName;
59    private LocalBluetoothProfile mPbapClientProfile;
60
61    /**
62     * Creates an instance of a BluetoothPairingController.
63     *
64     * @param intent - must contain {@link BluetoothDevice#EXTRA_PAIRING_VARIANT}, {@link
65     * BluetoothDevice#EXTRA_PAIRING_KEY}, and {@link BluetoothDevice#EXTRA_DEVICE}. Missing extra
66     * will lead to undefined behavior.
67     */
68    public BluetoothPairingController(Intent intent, Context context) {
69        mBluetoothManager = Utils.getLocalBtManager(context);
70        mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE);
71
72        String message = "";
73        if (mBluetoothManager == null) {
74            throw new IllegalStateException("Could not obtain LocalBluetoothManager");
75        } else if (mDevice == null) {
76            throw new IllegalStateException("Could not find BluetoothDevice");
77        }
78
79        mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR);
80        mPasskey = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR);
81        mDeviceName = mBluetoothManager.getCachedDeviceManager().getName(mDevice);
82        mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile();
83        mPasskeyFormatted = formatKey(mPasskey);
84
85    }
86
87    @Override
88    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
89        if (isChecked) {
90            mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED);
91        } else {
92            mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED);
93        }
94    }
95
96    @Override
97    public void onDialogPositiveClick(BluetoothPairingDialogFragment dialog) {
98        if (getDialogType() == USER_ENTRY_DIALOG) {
99            onPair(mUserInput);
100        } else {
101            onPair(null);
102        }
103    }
104
105    @Override
106    public void onDialogNegativeClick(BluetoothPairingDialogFragment dialog) {
107        onCancel();
108    }
109
110    /**
111     * A method for querying which bluetooth pairing dialog fragment variant this device requires.
112     *
113     * @return - The dialog view variant needed for this device.
114     */
115    public int getDialogType() {
116        switch (mType) {
117            case BluetoothDevice.PAIRING_VARIANT_PIN:
118            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
119            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
120                return USER_ENTRY_DIALOG;
121
122            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
123            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
124            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
125                return CONFIRMATION_DIALOG;
126
127            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
128            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
129                return DISPLAY_PASSKEY_DIALOG;
130
131            default:
132                return INVALID_DIALOG_TYPE;
133        }
134    }
135
136    /**
137     * @return - A string containing the name provided by the device.
138     */
139    public String getDeviceName() {
140        return mDeviceName;
141    }
142
143    /**
144     * A method for querying if the bluetooth device has a profile already set up on this device.
145     *
146     * @return - A boolean indicating if the device has previous knowledge of a profile for this
147     * device.
148     */
149    public boolean isProfileReady() {
150        return mPbapClientProfile != null && mPbapClientProfile.isProfileReady();
151    }
152
153    /**
154     * A method for querying if the bluetooth device has access to contacts on the device.
155     *
156     * @return - A boolean indicating if the bluetooth device has permission to access the device
157     * contacts
158     */
159    public boolean getContactSharingState() {
160        switch (mDevice.getPhonebookAccessPermission()) {
161            case BluetoothDevice.ACCESS_ALLOWED:
162                return true;
163            case BluetoothDevice.ACCESS_REJECTED:
164                return false;
165            default:
166                if (mDevice.getBluetoothClass().getDeviceClass()
167                        == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) {
168                    return true;
169                }
170                return false;
171        }
172    }
173
174    /**
175     * A method for querying if the provided editable is a valid passkey/pin format for this device.
176     *
177     * @param s - The passkey/pin
178     * @return - A boolean indicating if the passkey/pin is of the correct format.
179     */
180    public boolean isPasskeyValid(Editable s) {
181        boolean requires16Digits = mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS;
182        return s.length() >= 16 && requires16Digits || s.length() > 0 && !requires16Digits;
183    }
184
185    /**
186     * A method for querying what message should be shown to the user as additional text in the
187     * dialog for this device. Returns -1 to indicate a device type that does not use this message.
188     *
189     * @return - The message ID to show the user.
190     */
191    public int getDeviceVariantMessageId() {
192        switch (mType) {
193            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
194            case BluetoothDevice.PAIRING_VARIANT_PIN:
195                return R.string.bluetooth_enter_pin_other_device;
196
197            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
198                return R.string.bluetooth_enter_passkey_other_device;
199
200            default:
201                return INVALID_DIALOG_TYPE;
202        }
203    }
204
205    /**
206     * A method for querying what message hint should be shown to the user as additional text in the
207     * dialog for this device. Returns -1 to indicate a device type that does not use this message.
208     *
209     * @return - The message ID to show the user.
210     */
211    public int getDeviceVariantMessageHintId() {
212        switch (mType) {
213            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
214                return R.string.bluetooth_pin_values_hint_16_digits;
215
216            case BluetoothDevice.PAIRING_VARIANT_PIN:
217            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
218                return R.string.bluetooth_pin_values_hint;
219
220            default:
221                return INVALID_DIALOG_TYPE;
222        }
223    }
224
225    /**
226     * A method for querying the maximum passkey/pin length for this device.
227     *
228     * @return - An int indicating the maximum length
229     */
230    public int getDeviceMaxPasskeyLength() {
231        switch (mType) {
232            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
233            case BluetoothDevice.PAIRING_VARIANT_PIN:
234                return BLUETOOTH_PIN_MAX_LENGTH;
235
236            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
237                return BLUETOOTH_PASSKEY_MAX_LENGTH;
238
239            default:
240                return 0;
241        }
242
243    }
244
245    /**
246     * A method for querying if the device uses an alphanumeric passkey.
247     *
248     * @return - a boolean indicating if the passkey can be alphanumeric.
249     */
250    public boolean pairingCodeIsAlphanumeric() {
251        switch (mType) {
252            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
253                return false;
254
255            default:
256                return true;
257        }
258    }
259
260    /**
261     * A method used by the dialogfragment to notify the controller that the dialog has been
262     * displayed for bluetooth device types that just care about it being displayed.
263     */
264    protected void notifyDialogDisplayed() {
265        // send an OK to the framework, indicating that the dialog has been displayed.
266        if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) {
267            mDevice.setPairingConfirmation(true);
268        } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) {
269            byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPasskeyFormatted);
270            mDevice.setPin(pinBytes);
271        }
272    }
273
274    /**
275     * A method for querying if this bluetooth device type has a key it would like displayed
276     * to the user.
277     *
278     * @return - A boolean indicating if a key exists which should be displayed to the user.
279     */
280    public boolean isDisplayPairingKeyVariant() {
281        switch (mType) {
282            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
283            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
284            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
285                return true;
286            default:
287                return false;
288        }
289    }
290
291    /**
292     * A method for querying if this bluetooth device type has other content it would like displayed
293     * to the user.
294     *
295     * @return - A boolean indicating if content exists which should be displayed to the user.
296     */
297    public boolean hasPairingContent() {
298        switch (mType) {
299            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
300            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
301            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
302                return true;
303
304            default:
305                return false;
306        }
307    }
308
309    /**
310     * A method for obtaining any additional content this bluetooth device has for displaying to the
311     * user.
312     *
313     * @return - A string containing the additional content, null if none exists.
314     * @see {@link BluetoothPairingController#hasPairingContent()}
315     */
316    public String getPairingContent() {
317        if (hasPairingContent()) {
318            return mPasskeyFormatted;
319        } else {
320            return null;
321        }
322    }
323
324    /**
325     * A method that exists to allow the fragment to update the controller with input the user has
326     * provided in the fragment.
327     *
328     * @param input - A string containing the user input.
329     */
330    protected void updateUserInput(String input) {
331        mUserInput = input;
332    }
333
334    /**
335     * Returns the provided passkey in a format that this device expects. Only works for numeric
336     * passkeys/pins.
337     *
338     * @param passkey - An integer containing the passkey to format.
339     * @return - A string containing the formatted passkey/pin
340     */
341    private String formatKey(int passkey) {
342        switch (mType) {
343            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
344            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
345                return String.format(Locale.US, "%06d", passkey);
346
347            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
348                return String.format("%04d", passkey);
349
350            default:
351                return null;
352        }
353    }
354
355    /**
356     * handles the necessary communication with the bluetooth device to establish a successful
357     * pairing
358     *
359     * @param passkey - The passkey we will attempt to pair to the device with.
360     */
361    private void onPair(String passkey) {
362        Log.d(TAG, "Pairing dialog accepted");
363        switch (mType) {
364            case BluetoothDevice.PAIRING_VARIANT_PIN:
365            case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
366                byte[] pinBytes = BluetoothDevice.convertPinToBytes(passkey);
367                if (pinBytes == null) {
368                    return;
369                }
370                mDevice.setPin(pinBytes);
371                break;
372
373            case BluetoothDevice.PAIRING_VARIANT_PASSKEY:
374                int pass = Integer.parseInt(passkey);
375                mDevice.setPasskey(pass);
376                break;
377
378            case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION:
379            case BluetoothDevice.PAIRING_VARIANT_CONSENT:
380                mDevice.setPairingConfirmation(true);
381                break;
382
383            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY:
384            case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN:
385                // Do nothing.
386                break;
387
388            case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT:
389                mDevice.setRemoteOutOfBandData();
390                break;
391
392            default:
393                Log.e(TAG, "Incorrect pairing type received");
394        }
395    }
396
397    /**
398     * A method for properly ending communication with the bluetooth device. Will be called by the
399     * {@link BluetoothPairingDialogFragment} when it is dismissed.
400     */
401    public void onCancel() {
402        Log.d(TAG, "Pairing dialog canceled");
403        mDevice.cancelPairingUserInput();
404    }
405
406    /**
407     * A method for checking if this device is equal to another device.
408     *
409     * @param device - The other device being compared to this device.
410     * @return - A boolean indicating if the devices were equal.
411     */
412    public boolean deviceEquals(BluetoothDevice device) {
413        return mDevice == device;
414    }
415}
416