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