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