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