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