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