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