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