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