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