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