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