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