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