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