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