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