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