Home | History | Annotate | Download | only in accessories
      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