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 mDevice.setPin(mPasskeyFormatted); 273 } 274 } 275 276 /** 277 * A method for querying if this bluetooth device type has a key it would like displayed 278 * to the user. 279 * 280 * @return - A boolean indicating if a key exists which should be displayed to the user. 281 */ isDisplayPairingKeyVariant()282 public boolean isDisplayPairingKeyVariant() { 283 switch (mType) { 284 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 285 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 286 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 287 return true; 288 default: 289 return false; 290 } 291 } 292 293 /** 294 * A method for querying if this bluetooth device type has other content it would like displayed 295 * to the user. 296 * 297 * @return - A boolean indicating if content exists which should be displayed to the user. 298 */ hasPairingContent()299 public boolean hasPairingContent() { 300 switch (mType) { 301 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 302 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 303 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 304 return true; 305 306 default: 307 return false; 308 } 309 } 310 311 /** 312 * A method for obtaining any additional content this bluetooth device has for displaying to the 313 * user. 314 * 315 * @return - A string containing the additional content, null if none exists. 316 * @see {@link BluetoothPairingController#hasPairingContent()} 317 */ getPairingContent()318 public String getPairingContent() { 319 if (hasPairingContent()) { 320 return mPasskeyFormatted; 321 } else { 322 return null; 323 } 324 } 325 326 /** 327 * A method that exists to allow the fragment to update the controller with input the user has 328 * provided in the fragment. 329 * 330 * @param input - A string containing the user input. 331 */ updateUserInput(String input)332 protected void updateUserInput(String input) { 333 mUserInput = input; 334 } 335 336 /** 337 * Returns the provided passkey in a format that this device expects. Only works for numeric 338 * passkeys/pins. 339 * 340 * @param passkey - An integer containing the passkey to format. 341 * @return - A string containing the formatted passkey/pin 342 */ formatKey(int passkey)343 private String formatKey(int passkey) { 344 switch (mType) { 345 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 346 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 347 return String.format(Locale.US, "%06d", passkey); 348 349 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 350 return String.format("%04d", passkey); 351 352 default: 353 354 return null; 355 } 356 } 357 358 /** 359 * handles the necessary communication with the bluetooth device to establish a successful 360 * pairing 361 * 362 * @param passkey - The passkey we will attempt to pair to the device with. 363 */ onPair(String passkey)364 private void onPair(String passkey) { 365 LOG.d("Pairing dialog accepted"); 366 switch (mType) { 367 case BluetoothDevice.PAIRING_VARIANT_PIN: 368 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: 369 mDevice.setPin(passkey); 370 break; 371 372 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 373 int pass = Integer.parseInt(passkey); 374 break; 375 376 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 377 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 378 mDevice.setPairingConfirmation(true); 379 break; 380 381 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 382 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 383 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 384 // Do nothing. 385 break; 386 387 default: 388 LOG.e("Incorrect pairing type received"); 389 } 390 } 391 392 /** 393 * A method for properly ending communication with the bluetooth device. Will be called by the 394 * {@link BluetoothPairingDialogFragment} when it is dismissed. 395 */ onCancel()396 public void onCancel() { 397 LOG.d("Pairing dialog canceled"); 398 mDevice.cancelPairing(); 399 } 400 401 /** 402 * A method for checking if this device is equal to another device. 403 * 404 * @param device - The other device being compared to this device. 405 * @return - A boolean indicating if the devices were equal. 406 */ deviceEquals(BluetoothDevice device)407 public boolean deviceEquals(BluetoothDevice device) { 408 return mDevice == device; 409 } 410 } 411