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