1 /* 2 * Copyright (C) 2008 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 17 package com.android.settings.bluetooth; 18 19 import android.bluetooth.BluetoothClass; 20 import android.bluetooth.BluetoothDevice; 21 import android.bluetooth.BluetoothUuid; 22 import android.content.BroadcastReceiver; 23 import android.content.Context; 24 import android.content.DialogInterface; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.os.Bundle; 28 import android.text.Editable; 29 import android.text.InputFilter; 30 import android.text.InputFilter.LengthFilter; 31 import android.text.InputType; 32 import android.text.TextWatcher; 33 import android.util.Log; 34 import android.view.KeyEvent; 35 import android.view.View; 36 import android.widget.Button; 37 import android.widget.CheckBox; 38 import android.widget.CompoundButton; 39 import android.widget.EditText; 40 import android.widget.TextView; 41 42 import com.android.internal.app.AlertActivity; 43 import com.android.internal.app.AlertController; 44 import com.android.settings.R; 45 import com.android.settingslib.bluetooth.CachedBluetoothDeviceManager; 46 import com.android.settingslib.bluetooth.LocalBluetoothManager; 47 import com.android.settingslib.bluetooth.LocalBluetoothProfile; 48 49 import java.util.Locale; 50 51 /** 52 * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple confirmation 53 * for pairing with a remote Bluetooth device. It is an activity that appears as a dialog. 54 */ 55 public final class BluetoothPairingDialog extends AlertActivity implements 56 CompoundButton.OnCheckedChangeListener, DialogInterface.OnClickListener, TextWatcher { 57 private static final String TAG = "BluetoothPairingDialog"; 58 59 private static final int BLUETOOTH_PIN_MAX_LENGTH = 16; 60 private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6; 61 62 private LocalBluetoothManager mBluetoothManager; 63 private CachedBluetoothDeviceManager mCachedDeviceManager; 64 private BluetoothDevice mDevice; 65 private int mType; 66 private String mPairingKey; 67 private EditText mPairingView; 68 private Button mOkButton; 69 private LocalBluetoothProfile mPbapClientProfile; 70 71 72 /** 73 * Dismiss the dialog if the bond state changes to bonded or none, 74 * or if pairing was canceled for {@link #mDevice}. 75 */ 76 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 77 @Override 78 public void onReceive(Context context, Intent intent) { 79 String action = intent.getAction(); 80 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 81 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 82 BluetoothDevice.ERROR); 83 if (bondState == BluetoothDevice.BOND_BONDED || 84 bondState == BluetoothDevice.BOND_NONE) { 85 dismiss(); 86 } 87 } else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) { 88 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 89 if (device == null || device.equals(mDevice)) { 90 dismiss(); 91 } 92 } 93 } 94 }; 95 96 @Override onCreate(Bundle savedInstanceState)97 protected void onCreate(Bundle savedInstanceState) { 98 super.onCreate(savedInstanceState); 99 100 Intent intent = getIntent(); 101 if (!intent.getAction().equals(BluetoothDevice.ACTION_PAIRING_REQUEST)) 102 { 103 Log.e(TAG, "Error: this activity may be started only with intent " + 104 BluetoothDevice.ACTION_PAIRING_REQUEST); 105 finish(); 106 return; 107 } 108 109 mBluetoothManager = Utils.getLocalBtManager(this); 110 if (mBluetoothManager == null) { 111 Log.e(TAG, "Error: BluetoothAdapter not supported by system"); 112 finish(); 113 return; 114 } 115 mCachedDeviceManager = mBluetoothManager.getCachedDeviceManager(); 116 mPbapClientProfile = mBluetoothManager.getProfileManager().getPbapClientProfile(); 117 118 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 119 mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); 120 121 switch (mType) { 122 case BluetoothDevice.PAIRING_VARIANT_PIN: 123 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: 124 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 125 createUserEntryDialog(); 126 break; 127 128 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 129 int passkey = 130 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 131 if (passkey == BluetoothDevice.ERROR) { 132 Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog"); 133 return; 134 } 135 mPairingKey = String.format(Locale.US, "%06d", passkey); 136 createConfirmationDialog(); 137 break; 138 139 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 140 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 141 createConsentDialog(); 142 break; 143 144 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 145 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 146 int pairingKey = 147 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 148 if (pairingKey == BluetoothDevice.ERROR) { 149 Log.e(TAG, "Invalid Confirmation Passkey or PIN received, not showing any dialog"); 150 return; 151 } 152 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 153 mPairingKey = String.format("%06d", pairingKey); 154 } else { 155 mPairingKey = String.format("%04d", pairingKey); 156 } 157 createDisplayPasskeyOrPinDialog(); 158 break; 159 160 default: 161 Log.e(TAG, "Incorrect pairing type received, not showing any dialog"); 162 } 163 164 /* 165 * Leave this registered through pause/resume since we still want to 166 * finish the activity in the background if pairing is canceled. 167 */ 168 registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_PAIRING_CANCEL)); 169 registerReceiver(mReceiver, new IntentFilter(BluetoothDevice.ACTION_BOND_STATE_CHANGED)); 170 } 171 createUserEntryDialog()172 private void createUserEntryDialog() { 173 final AlertController.AlertParams p = mAlertParams; 174 p.mTitle = getString(R.string.bluetooth_pairing_request, 175 mCachedDeviceManager.getName(mDevice)); 176 p.mView = createPinEntryView(); 177 p.mPositiveButtonText = getString(android.R.string.ok); 178 p.mPositiveButtonListener = this; 179 p.mNegativeButtonText = getString(android.R.string.cancel); 180 p.mNegativeButtonListener = this; 181 setupAlert(); 182 183 mOkButton = mAlert.getButton(BUTTON_POSITIVE); 184 mOkButton.setEnabled(false); 185 } 186 createPinEntryView()187 private View createPinEntryView() { 188 View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null); 189 TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint); 190 TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin); 191 CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin); 192 CheckBox contactSharing = (CheckBox) view.findViewById( 193 R.id.phonebook_sharing_message_entry_pin); 194 contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook, 195 mCachedDeviceManager.getName(mDevice))); 196 if (mPbapClientProfile != null && mPbapClientProfile.isProfileReady()) { 197 contactSharing.setVisibility(View.GONE); 198 } 199 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_ALLOWED) { 200 contactSharing.setChecked(true); 201 } else if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_REJECTED){ 202 contactSharing.setChecked(false); 203 } else { 204 if (mDevice.getBluetoothClass().getDeviceClass() 205 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) { 206 contactSharing.setChecked(true); 207 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 208 } else { 209 contactSharing.setChecked(false); 210 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 211 } 212 } 213 214 contactSharing.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 215 @Override 216 public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { 217 if (isChecked) { 218 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 219 } else { 220 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 221 } 222 } 223 }); 224 225 mPairingView = (EditText) view.findViewById(R.id.text); 226 mPairingView.addTextChangedListener(this); 227 alphanumericPin.setOnCheckedChangeListener(this); 228 229 int messageId; 230 int messageIdHint = R.string.bluetooth_pin_values_hint; 231 int maxLength; 232 switch (mType) { 233 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: 234 messageIdHint = R.string.bluetooth_pin_values_hint_16_digits; 235 // FALLTHROUGH 236 case BluetoothDevice.PAIRING_VARIANT_PIN: 237 messageId = R.string.bluetooth_enter_pin_other_device; 238 // Maximum of 16 characters in a PIN 239 maxLength = BLUETOOTH_PIN_MAX_LENGTH; 240 break; 241 242 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 243 messageId = R.string.bluetooth_enter_passkey_other_device; 244 // Maximum of 6 digits for passkey 245 maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH; 246 alphanumericPin.setVisibility(View.GONE); 247 break; 248 249 default: 250 Log.e(TAG, "Incorrect pairing type for createPinEntryView: " + mType); 251 return null; 252 } 253 254 messageViewCaptionHint.setText(messageIdHint); 255 messageView2.setText(messageId); 256 mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); 257 mPairingView.setFilters(new InputFilter[] { 258 new LengthFilter(maxLength) }); 259 260 return view; 261 } 262 createView()263 private View createView() { 264 View view = getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null); 265 TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption); 266 TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead); 267 TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message); 268 CheckBox contactSharing = (CheckBox) view.findViewById( 269 R.id.phonebook_sharing_message_confirm_pin); 270 contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook, 271 mCachedDeviceManager.getName(mDevice))); 272 if (mPbapClientProfile != null && mPbapClientProfile.isProfileReady()) { 273 contactSharing.setVisibility(View.GONE); 274 } 275 if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_ALLOWED) { 276 contactSharing.setChecked(true); 277 } else if (mDevice.getPhonebookAccessPermission() == BluetoothDevice.ACCESS_REJECTED){ 278 contactSharing.setChecked(false); 279 } else { 280 if (mDevice.getBluetoothClass().getDeviceClass() 281 == BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE) { 282 contactSharing.setChecked(true); 283 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 284 } else { 285 contactSharing.setChecked(false); 286 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 287 } 288 } 289 290 contactSharing.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { 291 @Override 292 public void onCheckedChanged(CompoundButton compoundButton, boolean isChecked) { 293 if (isChecked) { 294 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_ALLOWED); 295 } else { 296 mDevice.setPhonebookAccessPermission(BluetoothDevice.ACCESS_REJECTED); 297 } 298 } 299 }); 300 301 String messageCaption = null; 302 String pairingContent = null; 303 switch (mType) { 304 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 305 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 306 messagePairing.setVisibility(View.VISIBLE); 307 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 308 pairingContent = mPairingKey; 309 break; 310 311 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 312 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 313 messagePairing.setVisibility(View.VISIBLE); 314 break; 315 316 default: 317 Log.e(TAG, "Incorrect pairing type received, not creating view"); 318 return null; 319 } 320 321 if (pairingContent != null) { 322 pairingViewCaption.setVisibility(View.VISIBLE); 323 pairingViewContent.setVisibility(View.VISIBLE); 324 pairingViewContent.setText(pairingContent); 325 } 326 327 return view; 328 } 329 createConfirmationDialog()330 private void createConfirmationDialog() { 331 final AlertController.AlertParams p = mAlertParams; 332 p.mTitle = getString(R.string.bluetooth_pairing_request, 333 mCachedDeviceManager.getName(mDevice)); 334 p.mView = createView(); 335 p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept); 336 p.mPositiveButtonListener = this; 337 p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline); 338 p.mNegativeButtonListener = this; 339 setupAlert(); 340 } 341 createConsentDialog()342 private void createConsentDialog() { 343 final AlertController.AlertParams p = mAlertParams; 344 p.mTitle = getString(R.string.bluetooth_pairing_request, 345 mCachedDeviceManager.getName(mDevice)); 346 p.mView = createView(); 347 p.mPositiveButtonText = getString(R.string.bluetooth_pairing_accept); 348 p.mPositiveButtonListener = this; 349 p.mNegativeButtonText = getString(R.string.bluetooth_pairing_decline); 350 p.mNegativeButtonListener = this; 351 setupAlert(); 352 } 353 createDisplayPasskeyOrPinDialog()354 private void createDisplayPasskeyOrPinDialog() { 355 final AlertController.AlertParams p = mAlertParams; 356 p.mTitle = getString(R.string.bluetooth_pairing_request, 357 mCachedDeviceManager.getName(mDevice)); 358 p.mView = createView(); 359 p.mNegativeButtonText = getString(android.R.string.cancel); 360 p.mNegativeButtonListener = this; 361 setupAlert(); 362 363 // Since its only a notification, send an OK to the framework, 364 // indicating that the dialog has been displayed. 365 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 366 mDevice.setPairingConfirmation(true); 367 } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { 368 byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey); 369 mDevice.setPin(pinBytes); 370 } 371 } 372 373 @Override onDestroy()374 protected void onDestroy() { 375 super.onDestroy(); 376 unregisterReceiver(mReceiver); 377 } 378 afterTextChanged(Editable s)379 public void afterTextChanged(Editable s) { 380 if (mOkButton != null) { 381 if (mType == BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS) { 382 mOkButton.setEnabled(s.length() >= 16); 383 } else { 384 mOkButton.setEnabled(s.length() > 0); 385 } 386 } 387 } 388 onPair(String value)389 private void onPair(String value) { 390 switch (mType) { 391 case BluetoothDevice.PAIRING_VARIANT_PIN: 392 case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS: 393 byte[] pinBytes = BluetoothDevice.convertPinToBytes(value); 394 if (pinBytes == null) { 395 return; 396 } 397 mDevice.setPin(pinBytes); 398 break; 399 400 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 401 int passkey = Integer.parseInt(value); 402 mDevice.setPasskey(passkey); 403 break; 404 405 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 406 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 407 mDevice.setPairingConfirmation(true); 408 break; 409 410 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 411 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 412 // Do nothing. 413 break; 414 415 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 416 mDevice.setRemoteOutOfBandData(); 417 break; 418 419 default: 420 Log.e(TAG, "Incorrect pairing type received"); 421 } 422 } 423 onCancel()424 private void onCancel() { 425 mDevice.cancelPairingUserInput(); 426 } 427 onKeyDown(int keyCode, KeyEvent event)428 public boolean onKeyDown(int keyCode, KeyEvent event) { 429 if (keyCode == KeyEvent.KEYCODE_BACK) { 430 onCancel(); 431 } 432 return super.onKeyDown(keyCode,event); 433 } 434 onClick(DialogInterface dialog, int which)435 public void onClick(DialogInterface dialog, int which) { 436 switch (which) { 437 case BUTTON_POSITIVE: 438 if (mPairingView != null) { 439 onPair(mPairingView.getText().toString()); 440 } else { 441 onPair(null); 442 } 443 break; 444 445 case BUTTON_NEGATIVE: 446 default: 447 onCancel(); 448 break; 449 } 450 } 451 452 /* Not used */ beforeTextChanged(CharSequence s, int start, int count, int after)453 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 454 } 455 456 /* Not used */ onTextChanged(CharSequence s, int start, int before, int count)457 public void onTextChanged(CharSequence s, int start, int before, int count) { 458 } 459 onCheckedChanged(CompoundButton buttonView, boolean isChecked)460 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { 461 // change input type for soft keyboard to numeric or alphanumeric 462 if (isChecked) { 463 mPairingView.setInputType(InputType.TYPE_CLASS_TEXT); 464 } else { 465 mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER); 466 } 467 } 468 } 469