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  */
16 package com.android.settings.bluetooth;
17 
18 import android.bluetooth.BluetoothClass;
19 import android.bluetooth.BluetoothDevice;
20 import android.content.Context;
21 import android.content.Intent;
22 import android.text.Editable;
23 import android.util.Log;
24 import android.widget.CompoundButton;
25 import android.widget.CompoundButton.OnCheckedChangeListener;
26 import com.android.settings.R;
27 import com.android.settings.bluetooth.BluetoothPairingDialogFragment.BluetoothPairingDialogListener;
28 import com.android.settingslib.bluetooth.LocalBluetoothManager;
29 import com.android.settingslib.bluetooth.LocalBluetoothProfile;
30 import 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  */
37 public 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      */
BluetoothPairingController(Intent intent, Context context)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
onCheckedChanged(CompoundButton buttonView, boolean isChecked)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
onDialogPositiveClick(BluetoothPairingDialogFragment dialog)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
onDialogNegativeClick(BluetoothPairingDialogFragment dialog)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      */
getDialogType()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      */
getDeviceName()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      */
isProfileReady()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      */
getContactSharingState()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      */
isPasskeyValid(Editable s)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      */
getDeviceVariantMessageId()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      */
getDeviceVariantMessageHintId()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      */
getDeviceMaxPasskeyLength()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      */
pairingCodeIsAlphanumeric()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      */
notifyDialogDisplayed()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      */
isDisplayPairingKeyVariant()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      */
hasPairingContent()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      */
getPairingContent()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      */
updateUserInput(String input)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      */
formatKey(int passkey)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      */
onPair(String passkey)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      */
onCancel()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      */
deviceEquals(BluetoothDevice device)412     public boolean deviceEquals(BluetoothDevice device) {
413         return mDevice == device;
414     }
415 }
416