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