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