1 /* 2 * Copyright (C) 2014 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.tv.settings.accessories; 18 19 import android.app.Fragment; 20 import android.bluetooth.BluetoothDevice; 21 import android.content.BroadcastReceiver; 22 import android.content.Context; 23 import android.content.Intent; 24 import android.content.IntentFilter; 25 import android.graphics.drawable.ColorDrawable; 26 import android.os.Bundle; 27 import android.support.annotation.NonNull; 28 import android.support.annotation.Nullable; 29 import android.text.Html; 30 import android.text.InputFilter; 31 import android.text.InputFilter.LengthFilter; 32 import android.text.InputType; 33 import android.util.Log; 34 import android.view.KeyEvent; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.WindowManager; 39 import android.view.inputmethod.EditorInfo; 40 import android.widget.EditText; 41 import android.widget.TextView; 42 import android.widget.TextView.OnEditorActionListener; 43 44 import com.android.internal.logging.nano.MetricsProto; 45 import com.android.tv.settings.R; 46 import com.android.tv.settings.dialog.old.Action; 47 import com.android.tv.settings.dialog.old.ActionFragment; 48 import com.android.tv.settings.dialog.old.DialogActivity; 49 import com.android.tv.settings.util.AccessibilityHelper; 50 51 import java.util.ArrayList; 52 import java.util.Locale; 53 54 /** 55 * BluetoothPairingDialog asks the user to enter a PIN / Passkey / simple 56 * confirmation for pairing with a remote Bluetooth device. 57 */ 58 public class BluetoothPairingDialog extends DialogActivity { 59 60 private static final String KEY_PAIR = "action_pair"; 61 private static final String KEY_CANCEL = "action_cancel"; 62 63 private static final String TAG = "BluetoothPairingDialog"; 64 private static final boolean DEBUG = false; 65 66 private static final int BLUETOOTH_PIN_MAX_LENGTH = 16; 67 private static final int BLUETOOTH_PASSKEY_MAX_LENGTH = 6; 68 69 private BluetoothDevice mDevice; 70 private int mType; 71 private String mPairingKey; 72 private boolean mPairingInProgress = false; 73 74 /** 75 * Dismiss the dialog if the bond state changes to bonded or none, or if 76 * pairing was canceled for {@link #mDevice}. 77 */ 78 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 79 @Override 80 public void onReceive(Context context, Intent intent) { 81 String action = intent.getAction(); 82 if (DEBUG) { 83 Log.d(TAG, "onReceive. Broadcast Intent = " + intent.toString()); 84 } 85 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 86 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 87 BluetoothDevice.ERROR); 88 if (bondState == BluetoothDevice.BOND_BONDED || 89 bondState == BluetoothDevice.BOND_NONE) { 90 dismiss(); 91 } 92 } else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) { 93 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 94 if (device == null || device.equals(mDevice)) { 95 dismiss(); 96 } 97 } 98 } 99 }; 100 101 @Override 102 protected void onCreate(Bundle savedInstanceState) { 103 super.onCreate(savedInstanceState); 104 105 final Intent intent = getIntent(); 106 if (!BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) { 107 Log.e(TAG, "Error: this activity may be started only with intent " + 108 BluetoothDevice.ACTION_PAIRING_REQUEST); 109 finish(); 110 return; 111 } 112 113 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 114 mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); 115 116 if (DEBUG) { 117 Log.d(TAG, "Requested pairing Type = " + mType + " , Device = " + mDevice); 118 } 119 120 switch (mType) { 121 case BluetoothDevice.PAIRING_VARIANT_PIN: 122 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 123 createUserEntryDialog(); 124 break; 125 126 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 127 int passkey = 128 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 129 if (passkey == BluetoothDevice.ERROR) { 130 Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog"); 131 finish(); 132 return; 133 } 134 mPairingKey = String.format(Locale.US, "%06d", passkey); 135 createConfirmationDialog(); 136 break; 137 138 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 139 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 140 createConfirmationDialog(); 141 break; 142 143 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 144 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 145 int pairingKey = 146 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 147 if (pairingKey == BluetoothDevice.ERROR) { 148 Log.e(TAG, 149 "Invalid Confirmation Passkey or PIN received, not showing any dialog"); 150 finish(); 151 return; 152 } 153 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 154 mPairingKey = String.format("%06d", pairingKey); 155 } else { 156 mPairingKey = String.format("%04d", pairingKey); 157 } 158 createConfirmationDialog(); 159 break; 160 161 default: 162 Log.e(TAG, "Incorrect pairing type received, not showing any dialog"); 163 finish(); 164 return; 165 } 166 167 // Fade out the old activity, and fade in the new activity. 168 overridePendingTransition(R.anim.fade_in, R.anim.fade_out); 169 170 // TODO: don't do this 171 final ViewGroup contentView = (ViewGroup) findViewById(android.R.id.content); 172 final View topLayout = contentView.getChildAt(0); 173 174 // Set the activity background 175 final ColorDrawable bgDrawable = 176 new ColorDrawable(getColor(R.color.dialog_activity_background)); 177 bgDrawable.setAlpha(255); 178 topLayout.setBackground(bgDrawable); 179 180 // Make sure pairing wakes up day dream 181 getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | 182 WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | 183 WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | 184 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 185 } 186 187 @Override 188 protected void onResume() { 189 super.onResume(); 190 191 IntentFilter filter = new IntentFilter(); 192 filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL); 193 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 194 registerReceiver(mReceiver, filter); 195 } 196 197 @Override 198 protected void onPause() { 199 unregisterReceiver(mReceiver); 200 201 // Finish the activity if we get placed in the background and cancel pairing 202 if (!mPairingInProgress) { 203 cancelPairing(); 204 } 205 dismiss(); 206 207 super.onPause(); 208 } 209 210 @Override 211 public void onActionClicked(Action action) { 212 String key = action.getKey(); 213 if (KEY_PAIR.equals(key)) { 214 onPair(null); 215 dismiss(); 216 } else if (KEY_CANCEL.equals(key)) { 217 cancelPairing(); 218 } 219 } 220 221 @Override 222 public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) { 223 if (keyCode == KeyEvent.KEYCODE_BACK) { 224 cancelPairing(); 225 } 226 return super.onKeyDown(keyCode, event); 227 } 228 229 private ArrayList<Action> getActions() { 230 ArrayList<Action> actions = new ArrayList<>(); 231 232 switch (mType) { 233 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 234 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 235 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 236 actions.add(new Action.Builder() 237 .key(KEY_PAIR) 238 .title(getString(R.string.bluetooth_pair)) 239 .build()); 240 241 actions.add(new Action.Builder() 242 .key(KEY_CANCEL) 243 .title(getString(R.string.bluetooth_cancel)) 244 .build()); 245 break; 246 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 247 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 248 actions.add(new Action.Builder() 249 .key(KEY_CANCEL) 250 .title(getString(R.string.bluetooth_cancel)) 251 .build()); 252 break; 253 } 254 255 return actions; 256 } 257 258 private void dismiss() { 259 finish(); 260 } 261 262 private void cancelPairing() { 263 if (DEBUG) { 264 Log.d(TAG, "cancelPairing"); 265 } 266 mDevice.cancelPairingUserInput(); 267 } 268 269 private void createUserEntryDialog() { 270 getFragmentManager().beginTransaction() 271 .replace(android.R.id.content, EntryDialogFragment.newInstance(mDevice, mType)) 272 .commit(); 273 } 274 275 private void createConfirmationDialog() { 276 // Build a Dialog activity view, with Action Fragment 277 278 final ArrayList<Action> actions = getActions(); 279 280 final Fragment actionFragment = ActionFragment.newInstance(actions); 281 final Fragment contentFragment = 282 ConfirmationDialogFragment.newInstance(mDevice, mPairingKey, mType); 283 284 setContentAndActionFragments(contentFragment, actionFragment); 285 } 286 287 private void onPair(String value) { 288 if (DEBUG) { 289 Log.d(TAG, "onPair: " + value); 290 } 291 switch (mType) { 292 case BluetoothDevice.PAIRING_VARIANT_PIN: 293 byte[] pinBytes = BluetoothDevice.convertPinToBytes(value); 294 if (pinBytes == null) { 295 return; 296 } 297 mDevice.setPin(pinBytes); 298 mPairingInProgress = true; 299 break; 300 301 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 302 try { 303 int passkey = Integer.parseInt(value); 304 mDevice.setPasskey(passkey); 305 mPairingInProgress = true; 306 } catch (NumberFormatException e) { 307 Log.d(TAG, "pass key " + value + " is not an integer"); 308 } 309 break; 310 311 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 312 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 313 mDevice.setPairingConfirmation(true); 314 mPairingInProgress = true; 315 break; 316 317 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 318 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 319 // Do nothing. 320 break; 321 322 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 323 mDevice.setRemoteOutOfBandData(); 324 mPairingInProgress = true; 325 break; 326 327 default: 328 Log.e(TAG, "Incorrect pairing type received"); 329 } 330 } 331 332 @Override 333 public int getMetricsCategory() { 334 return MetricsProto.MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT; 335 } 336 337 public static class EntryDialogFragment extends Fragment { 338 339 private static final String ARG_DEVICE = "ConfirmationDialogFragment.DEVICE"; 340 private static final String ARG_TYPE = "ConfirmationDialogFragment.TYPE"; 341 342 private BluetoothDevice mDevice; 343 private int mType; 344 345 public static EntryDialogFragment newInstance(BluetoothDevice device, int type) { 346 final EntryDialogFragment fragment = new EntryDialogFragment(); 347 final Bundle b = new Bundle(2); 348 fragment.setArguments(b); 349 b.putParcelable(ARG_DEVICE, device); 350 b.putInt(ARG_TYPE, type); 351 return fragment; 352 } 353 354 @Override 355 public void onCreate(@Nullable Bundle savedInstanceState) { 356 super.onCreate(savedInstanceState); 357 final Bundle args = getArguments(); 358 mDevice = args.getParcelable(ARG_DEVICE); 359 mType = args.getInt(ARG_TYPE); 360 } 361 362 @Override 363 public @Nullable View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 364 Bundle savedInstanceState) { 365 final View v = inflater.inflate(R.layout.bt_pairing_passkey_entry, container, false); 366 367 final TextView titleText = (TextView) v.findViewById(R.id.title_text); 368 final EditText textInput = (EditText) v.findViewById(R.id.text_input); 369 370 textInput.setOnEditorActionListener(new OnEditorActionListener() { 371 @Override 372 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 373 String value = textInput.getText().toString(); 374 if (actionId == EditorInfo.IME_ACTION_NEXT || 375 (actionId == EditorInfo.IME_NULL && 376 event.getAction() == KeyEvent.ACTION_DOWN)) { 377 ((BluetoothPairingDialog)getActivity()).onPair(value); 378 } 379 return true; 380 } 381 }); 382 383 final String instructions; 384 final int maxLength; 385 switch (mType) { 386 case BluetoothDevice.PAIRING_VARIANT_PIN: 387 instructions = getString(R.string.bluetooth_enter_pin_msg, mDevice.getName()); 388 final TextView instructionText = (TextView) v.findViewById(R.id.hint_text); 389 instructionText.setText(getString(R.string.bluetooth_pin_values_hint)); 390 // Maximum of 16 characters in a PIN 391 maxLength = BLUETOOTH_PIN_MAX_LENGTH; 392 textInput.setInputType(InputType.TYPE_CLASS_NUMBER); 393 break; 394 395 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 396 instructions = getString(R.string.bluetooth_enter_passkey_msg, 397 mDevice.getName()); 398 // Maximum of 6 digits for passkey 399 maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH; 400 textInput.setInputType(InputType.TYPE_CLASS_TEXT); 401 break; 402 403 default: 404 throw new IllegalStateException("Incorrect pairing type for" + 405 " createPinEntryView: " + mType); 406 } 407 408 titleText.setText(Html.fromHtml(instructions)); 409 410 textInput.setFilters(new InputFilter[]{new LengthFilter(maxLength)}); 411 412 return v; 413 } 414 } 415 416 public static class ConfirmationDialogFragment extends Fragment { 417 418 private static final String ARG_DEVICE = "ConfirmationDialogFragment.DEVICE"; 419 private static final String ARG_PAIRING_KEY = "ConfirmationDialogFragment.PAIRING_KEY"; 420 private static final String ARG_TYPE = "ConfirmationDialogFragment.TYPE"; 421 422 private BluetoothDevice mDevice; 423 private String mPairingKey; 424 private int mType; 425 426 public static ConfirmationDialogFragment newInstance(BluetoothDevice device, 427 String pairingKey, int type) { 428 final ConfirmationDialogFragment fragment = new ConfirmationDialogFragment(); 429 final Bundle b = new Bundle(3); 430 b.putParcelable(ARG_DEVICE, device); 431 b.putString(ARG_PAIRING_KEY, pairingKey); 432 b.putInt(ARG_TYPE, type); 433 fragment.setArguments(b); 434 return fragment; 435 } 436 437 @Override 438 public void onCreate(@Nullable Bundle savedInstanceState) { 439 super.onCreate(savedInstanceState); 440 441 final Bundle args = getArguments(); 442 443 mDevice = args.getParcelable(ARG_DEVICE); 444 mPairingKey = args.getString(ARG_PAIRING_KEY); 445 mType = args.getInt(ARG_TYPE); 446 } 447 448 @Override 449 public View onCreateView(LayoutInflater inflater, ViewGroup container, 450 Bundle savedInstanceState) { 451 final View v = inflater.inflate(R.layout.bt_pairing_passkey_display, container, false); 452 453 final TextView titleText = (TextView) v.findViewById(R.id.title); 454 final TextView instructionText = (TextView) v.findViewById(R.id.pairing_instructions); 455 456 titleText.setText(getString(R.string.bluetooth_pairing_request)); 457 458 if (AccessibilityHelper.forceFocusableViews(getActivity())) { 459 titleText.setFocusable(true); 460 titleText.setFocusableInTouchMode(true); 461 instructionText.setFocusable(true); 462 instructionText.setFocusableInTouchMode(true); 463 } 464 465 final String instructions; 466 467 switch (mType) { 468 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 469 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 470 instructions = getString(R.string.bluetooth_display_passkey_pin_msg, 471 mDevice.getName(), mPairingKey); 472 473 // Since its only a notification, send an OK to the framework, 474 // indicating that the dialog has been displayed. 475 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 476 mDevice.setPairingConfirmation(true); 477 } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { 478 byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey); 479 mDevice.setPin(pinBytes); 480 } 481 break; 482 483 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 484 instructions = getString(R.string.bluetooth_confirm_passkey_msg, 485 mDevice.getName(), mPairingKey); 486 break; 487 488 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 489 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 490 instructions = getString(R.string.bluetooth_incoming_pairing_msg, 491 mDevice.getName()); 492 493 break; 494 default: 495 instructions = ""; 496 } 497 498 instructionText.setText(Html.fromHtml(instructions)); 499 500 return v; 501 } 502 } 503 } 504