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.view.WindowManager; 20 import com.android.tv.settings.R; 21 import com.android.tv.settings.dialog.old.Action; 22 import com.android.tv.settings.dialog.old.ActionAdapter; 23 import com.android.tv.settings.dialog.old.ActionFragment; 24 import com.android.tv.settings.dialog.old.DialogActivity; 25 26 import android.app.Fragment; 27 import android.bluetooth.BluetoothDevice; 28 import android.content.BroadcastReceiver; 29 import android.content.Context; 30 import android.content.Intent; 31 import android.content.IntentFilter; 32 import android.graphics.drawable.ColorDrawable; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.Message; 36 import android.text.Html; 37 import android.text.InputFilter; 38 import android.text.InputType; 39 import android.text.InputFilter.LengthFilter; 40 import android.util.Log; 41 import android.view.KeyEvent; 42 import android.view.LayoutInflater; 43 import android.view.View; 44 import android.view.ViewGroup; 45 import android.widget.EditText; 46 import android.widget.RelativeLayout; 47 import android.widget.TextView; 48 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 = "aah.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 73 private ActionFragment mActionFragment; 74 private Fragment mContentFragment; 75 private ArrayList<Action> mActions; 76 77 private RelativeLayout mTopLayout; 78 protected ColorDrawable mBgDrawable = new ColorDrawable(); 79 private TextView mTitleText; 80 private TextView mInstructionText; 81 private EditText mTextInput; 82 83 84 /** 85 * Dismiss the dialog if the bond state changes to bonded or none, or if 86 * pairing was canceled for {@link #mDevice}. 87 */ 88 private final BroadcastReceiver mReceiver = new BroadcastReceiver() { 89 @Override 90 public void onReceive(Context context, Intent intent) { 91 String action = intent.getAction(); 92 if (DEBUG) { 93 Log.d(TAG, "onReceive. Broadcast Intent = " + intent.toString()); 94 } 95 if (BluetoothDevice.ACTION_BOND_STATE_CHANGED.equals(action)) { 96 int bondState = intent.getIntExtra(BluetoothDevice.EXTRA_BOND_STATE, 97 BluetoothDevice.ERROR); 98 if (bondState == BluetoothDevice.BOND_BONDED || 99 bondState == BluetoothDevice.BOND_NONE) { 100 dismiss(); 101 } 102 } else if (BluetoothDevice.ACTION_PAIRING_CANCEL.equals(action)) { 103 BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 104 if (device == null || device.equals(mDevice)) { 105 dismiss(); 106 } 107 } 108 } 109 }; 110 111 @Override 112 protected void onCreate(Bundle savedInstanceState) { 113 super.onCreate(savedInstanceState); 114 115 Intent intent = getIntent(); 116 if (!BluetoothDevice.ACTION_PAIRING_REQUEST.equals(intent.getAction())) { 117 Log.e(TAG, "Error: this activity may be started only with intent " + 118 BluetoothDevice.ACTION_PAIRING_REQUEST); 119 finish(); 120 return; 121 } 122 123 mDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); 124 mType = intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_VARIANT, BluetoothDevice.ERROR); 125 126 if (DEBUG) { 127 Log.d(TAG, "Requested pairing Type = " + mType + " , Device = " + mDevice); 128 } 129 130 mActions = new ArrayList<Action>(); 131 132 switch (mType) { 133 case BluetoothDevice.PAIRING_VARIANT_PIN: 134 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 135 createUserEntryDialog(); 136 break; 137 138 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 139 int passkey = 140 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 141 if (passkey == BluetoothDevice.ERROR) { 142 Log.e(TAG, "Invalid Confirmation Passkey received, not showing any dialog"); 143 finish(); 144 return; 145 } 146 mPairingKey = String.format(Locale.US, "%06d", passkey); 147 createConfirmationDialog(); 148 break; 149 150 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 151 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 152 createConfirmationDialog(); 153 break; 154 155 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 156 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 157 int pairingKey = 158 intent.getIntExtra(BluetoothDevice.EXTRA_PAIRING_KEY, BluetoothDevice.ERROR); 159 if (pairingKey == BluetoothDevice.ERROR) { 160 Log.e(TAG, 161 "Invalid Confirmation Passkey or PIN received, not showing any dialog"); 162 finish(); 163 return; 164 } 165 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 166 mPairingKey = String.format("%06d", pairingKey); 167 } else { 168 mPairingKey = String.format("%04d", pairingKey); 169 } 170 createConfirmationDialog(); 171 break; 172 173 default: 174 Log.e(TAG, "Incorrect pairing type received, not showing any dialog"); 175 finish(); 176 return; 177 } 178 179 ViewGroup contentView = (ViewGroup) findViewById(android.R.id.content); 180 mTopLayout = (RelativeLayout) contentView.getChildAt(0); 181 182 // Fade out the old activity, and fade in the new activity. 183 overridePendingTransition(R.anim.fade_in, R.anim.fade_out); 184 185 // Set the activity background 186 int bgColor = getResources().getColor(R.color.dialog_activity_background); 187 mBgDrawable.setColor(bgColor); 188 mBgDrawable.setAlpha(255); 189 mTopLayout.setBackground(mBgDrawable); 190 191 // Make sure pairing wakes up day dream 192 getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD | 193 WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED | 194 WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON | 195 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); 196 } 197 198 @Override 199 protected void onResume() { 200 super.onResume(); 201 202 IntentFilter filter = new IntentFilter(); 203 filter.addAction(BluetoothDevice.ACTION_PAIRING_CANCEL); 204 filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); 205 registerReceiver(mReceiver, filter); 206 } 207 208 @Override 209 protected void onPause() { 210 unregisterReceiver(mReceiver); 211 212 // Finish the activity if we get placed in the background and cancel pairing 213 cancelPairing(); 214 dismiss(); 215 216 super.onPause(); 217 } 218 219 @Override 220 public void onActionClicked(Action action) { 221 String key = action.getKey(); 222 if (KEY_PAIR.equals(key)) { 223 onPair(null); 224 dismiss(); 225 } else if (KEY_CANCEL.equals(key)) { 226 cancelPairing(); 227 } 228 } 229 230 @Override 231 public boolean onKeyDown(int keyCode, KeyEvent event) { 232 if (keyCode == KeyEvent.KEYCODE_BACK) { 233 cancelPairing(); 234 } 235 return super.onKeyDown(keyCode, event); 236 } 237 238 private ArrayList<Action> getActions() { 239 ArrayList<Action> actions = new ArrayList<Action>(); 240 241 switch (mType) { 242 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 243 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 244 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 245 actions.add(new Action.Builder() 246 .key(KEY_PAIR) 247 .title(getString(R.string.bluetooth_pair)) 248 .build()); 249 250 actions.add(new Action.Builder() 251 .key(KEY_CANCEL) 252 .title(getString(R.string.bluetooth_cancel)) 253 .build()); 254 break; 255 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 256 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 257 actions.add(new Action.Builder() 258 .key(KEY_CANCEL) 259 .title(getString(R.string.bluetooth_cancel)) 260 .build()); 261 break; 262 } 263 264 return actions; 265 } 266 267 private void dismiss() { 268 finish(); 269 } 270 271 private void cancelPairing() { 272 if (DEBUG) { 273 Log.d(TAG, "cancelPairing"); 274 } 275 mDevice.cancelPairingUserInput(); 276 } 277 278 private void createUserEntryDialog() { 279 setContentView(R.layout.bt_pairing_passkey_entry); 280 281 mTitleText = (TextView) findViewById(R.id.title_text); 282 mTextInput = (EditText) findViewById(R.id.text_input); 283 284 String instructions = getString(R.string.bluetooth_confirm_passkey_msg, 285 mDevice.getName(), mPairingKey); 286 int maxLength; 287 switch (mType) { 288 case BluetoothDevice.PAIRING_VARIANT_PIN: 289 instructions = getString(R.string.bluetooth_enter_pin_msg, mDevice.getName()); 290 mInstructionText = (TextView) findViewById(R.id.hint_text); 291 mInstructionText.setText(getString(R.string.bluetooth_pin_values_hint)); 292 // Maximum of 16 characters in a PIN 293 maxLength = BLUETOOTH_PIN_MAX_LENGTH; 294 mTextInput.setInputType(InputType.TYPE_CLASS_NUMBER); 295 break; 296 297 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 298 instructions = getString(R.string.bluetooth_enter_passkey_msg, mDevice.getName()); 299 // Maximum of 6 digits for passkey 300 maxLength = BLUETOOTH_PASSKEY_MAX_LENGTH; 301 mTextInput.setInputType(InputType.TYPE_CLASS_TEXT); 302 break; 303 304 default: 305 Log.e(TAG, "Incorrect pairing type for createPinEntryView: " + mType); 306 dismiss(); 307 return; 308 } 309 310 mTitleText.setText(Html.fromHtml(instructions)); 311 312 mTextInput.setFilters(new InputFilter[] { new LengthFilter(maxLength) }); 313 } 314 315 private void createConfirmationDialog() { 316 // Build a Dialog activity view, with Action Fragment 317 318 mActions = getActions(); 319 320 mActionFragment = ActionFragment.newInstance(mActions); 321 mContentFragment = new Fragment() { 322 @Override 323 public View onCreateView(LayoutInflater inflater, ViewGroup container, 324 Bundle savedInstanceState) { 325 View v = inflater.inflate(R.layout.bt_pairing_passkey_display, container, false); 326 327 mTitleText = (TextView) v.findViewById(R.id.title); 328 mInstructionText = (TextView) v.findViewById(R.id.pairing_instructions); 329 330 mTitleText.setText(getString(R.string.bluetooth_pairing_request)); 331 332 if (AccessibilityHelper.forceFocusableViews(getActivity())) { 333 mTitleText.setFocusable(true); 334 mTitleText.setFocusableInTouchMode(true); 335 mInstructionText.setFocusable(true); 336 mInstructionText.setFocusableInTouchMode(true); 337 } 338 339 String instructions; 340 341 switch (mType) { 342 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 343 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 344 instructions = getString(R.string.bluetooth_display_passkey_pin_msg, 345 mDevice.getName(), mPairingKey); 346 347 // Since its only a notification, send an OK to the framework, 348 // indicating that the dialog has been displayed. 349 if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY) { 350 mDevice.setPairingConfirmation(true); 351 } else if (mType == BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN) { 352 byte[] pinBytes = BluetoothDevice.convertPinToBytes(mPairingKey); 353 mDevice.setPin(pinBytes); 354 } 355 break; 356 357 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 358 instructions = getString(R.string.bluetooth_confirm_passkey_msg, 359 mDevice.getName(), mPairingKey); 360 break; 361 362 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 363 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 364 instructions = getString(R.string.bluetooth_incoming_pairing_msg, 365 mDevice.getName()); 366 367 break; 368 default: 369 instructions = new String(); 370 } 371 372 mInstructionText.setText(Html.fromHtml(instructions)); 373 374 return v; 375 } 376 }; 377 378 setContentAndActionFragments(mContentFragment, mActionFragment); 379 } 380 381 private void onPair(String value) { 382 if (DEBUG) { 383 Log.d(TAG, "onPair: " + value); 384 } 385 switch (mType) { 386 case BluetoothDevice.PAIRING_VARIANT_PIN: 387 byte[] pinBytes = BluetoothDevice.convertPinToBytes(value); 388 if (pinBytes == null) { 389 return; 390 } 391 mDevice.setPin(pinBytes); 392 break; 393 394 case BluetoothDevice.PAIRING_VARIANT_PASSKEY: 395 int passkey = Integer.parseInt(value); 396 mDevice.setPasskey(passkey); 397 break; 398 399 case BluetoothDevice.PAIRING_VARIANT_PASSKEY_CONFIRMATION: 400 case BluetoothDevice.PAIRING_VARIANT_CONSENT: 401 mDevice.setPairingConfirmation(true); 402 break; 403 404 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PASSKEY: 405 case BluetoothDevice.PAIRING_VARIANT_DISPLAY_PIN: 406 // Do nothing. 407 break; 408 409 case BluetoothDevice.PAIRING_VARIANT_OOB_CONSENT: 410 mDevice.setRemoteOutOfBandData(); 411 break; 412 413 default: 414 Log.e(TAG, "Incorrect pairing type received"); 415 } 416 } 417 418 } 419