Home | History | Annotate | Download | only in bluetooth
      1 /*
      2  * Copyright (C) 2016 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 package com.android.settings.bluetooth;
     17 
     18 import android.app.AlertDialog;
     19 import android.app.Dialog;
     20 import android.content.Context;
     21 import android.content.DialogInterface;
     22 import android.content.DialogInterface.OnClickListener;
     23 import android.os.Bundle;
     24 import android.text.Editable;
     25 import android.text.InputFilter;
     26 import android.text.InputFilter.LengthFilter;
     27 import android.text.InputType;
     28 import android.text.TextUtils;
     29 import android.text.TextWatcher;
     30 import android.util.Log;
     31 import android.view.View;
     32 import android.view.inputmethod.InputMethodManager;
     33 import android.widget.Button;
     34 import android.widget.CheckBox;
     35 import android.widget.EditText;
     36 import android.widget.TextView;
     37 
     38 import com.android.internal.annotations.VisibleForTesting;
     39 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     40 import com.android.settings.R;
     41 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
     42 
     43 /**
     44  * A dialogFragment used by {@link BluetoothPairingDialog} to create an appropriately styled dialog
     45  * for the bluetooth device.
     46  */
     47 public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment implements
     48         TextWatcher, OnClickListener {
     49 
     50     private static final String TAG = "BTPairingDialogFragment";
     51 
     52     private AlertDialog.Builder mBuilder;
     53     private AlertDialog mDialog;
     54     private BluetoothPairingController mPairingController;
     55     private BluetoothPairingDialog mPairingDialogActivity;
     56     private EditText mPairingView;
     57     /**
     58      * The interface we expect a listener to implement. Typically this should be done by
     59      * the controller.
     60      */
     61     public interface BluetoothPairingDialogListener {
     62 
     63         void onDialogNegativeClick(BluetoothPairingDialogFragment dialog);
     64 
     65         void onDialogPositiveClick(BluetoothPairingDialogFragment dialog);
     66     }
     67 
     68     @Override
     69     public Dialog onCreateDialog(Bundle savedInstanceState) {
     70         if (!isPairingControllerSet()) {
     71             throw new IllegalStateException(
     72                 "Must call setPairingController() before showing dialog");
     73         }
     74         if (!isPairingDialogActivitySet()) {
     75             throw new IllegalStateException(
     76                 "Must call setPairingDialogActivity() before showing dialog");
     77         }
     78         mBuilder = new AlertDialog.Builder(getActivity());
     79         mDialog = setupDialog();
     80         mDialog.setCanceledOnTouchOutside(false);
     81         return mDialog;
     82     }
     83 
     84     @Override
     85     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
     86     }
     87 
     88     @Override
     89     public void onTextChanged(CharSequence s, int start, int before, int count) {
     90     }
     91 
     92     @Override
     93     public void afterTextChanged(Editable s) {
     94         // enable the positive button when we detect potentially valid input
     95         Button positiveButton = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
     96         if (positiveButton != null) {
     97             positiveButton.setEnabled(mPairingController.isPasskeyValid(s));
     98         }
     99         // notify the controller about user input
    100         mPairingController.updateUserInput(s.toString());
    101     }
    102 
    103     @Override
    104     public void onClick(DialogInterface dialog, int which) {
    105         if (which == DialogInterface.BUTTON_POSITIVE) {
    106             mPairingController.onDialogPositiveClick(this);
    107         } else if (which == DialogInterface.BUTTON_NEGATIVE) {
    108             mPairingController.onDialogNegativeClick(this);
    109         }
    110         mPairingDialogActivity.dismiss();
    111     }
    112 
    113     @Override
    114     public int getMetricsCategory() {
    115         return MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT;
    116     }
    117 
    118     /**
    119      * Used in testing to get a reference to the dialog.
    120      * @return - The fragments current dialog
    121      */
    122     protected AlertDialog getmDialog() {
    123         return mDialog;
    124     }
    125 
    126     /**
    127      * Sets the controller that the fragment should use. this method MUST be called
    128      * before you try to show the dialog or an error will be thrown. An implementation
    129      * of a pairing controller can be found at {@link BluetoothPairingController}. A
    130      * controller may not be substituted once it is assigned. Forcibly switching a
    131      * controller for a new one will lead to undefined behavior.
    132      */
    133     void setPairingController(BluetoothPairingController pairingController) {
    134         if (isPairingControllerSet()) {
    135             throw new IllegalStateException("The controller can only be set once. "
    136                     + "Forcibly replacing it will lead to undefined behavior");
    137         }
    138         mPairingController = pairingController;
    139     }
    140 
    141     /**
    142      * Checks whether mPairingController is set
    143      * @return True when mPairingController is set, False otherwise
    144      */
    145     boolean isPairingControllerSet() {
    146         return mPairingController != null;
    147     }
    148 
    149     /**
    150      * Sets the BluetoothPairingDialog activity that started this fragment
    151      * @param pairingDialogActivity The pairing dialog activty that started this fragment
    152      */
    153     void setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity) {
    154         if (isPairingDialogActivitySet()) {
    155             throw new IllegalStateException("The pairing dialog activity can only be set once");
    156         }
    157         mPairingDialogActivity = pairingDialogActivity;
    158     }
    159 
    160     /**
    161      * Checks whether mPairingDialogActivity is set
    162      * @return True when mPairingDialogActivity is set, False otherwise
    163      */
    164     boolean isPairingDialogActivitySet() {
    165         return mPairingDialogActivity != null;
    166     }
    167 
    168     /**
    169      * Creates the appropriate type of dialog and returns it.
    170      */
    171     private AlertDialog setupDialog() {
    172         AlertDialog dialog;
    173         switch (mPairingController.getDialogType()) {
    174             case BluetoothPairingController.USER_ENTRY_DIALOG:
    175                 dialog = createUserEntryDialog();
    176                 break;
    177             case BluetoothPairingController.CONFIRMATION_DIALOG:
    178                 dialog = createConsentDialog();
    179                 break;
    180             case BluetoothPairingController.DISPLAY_PASSKEY_DIALOG:
    181                 dialog = createDisplayPasskeyOrPinDialog();
    182                 break;
    183             default:
    184                 dialog = null;
    185                 Log.e(TAG, "Incorrect pairing type received, not showing any dialog");
    186         }
    187         return dialog;
    188     }
    189 
    190     /**
    191      * Helper method to return the text of the pin entry field - this exists primarily to help us
    192      * simulate having existing text when the dialog is recreated, for example after a screen
    193      * rotation.
    194      */
    195     @VisibleForTesting
    196     CharSequence getPairingViewText() {
    197         if (mPairingView != null) {
    198             return mPairingView.getText();
    199         }
    200         return null;
    201     }
    202 
    203     /**
    204      * Returns a dialog with UI elements that allow a user to provide input.
    205      */
    206     private AlertDialog createUserEntryDialog() {
    207         mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
    208                 mPairingController.getDeviceName()));
    209         mBuilder.setView(createPinEntryView());
    210         mBuilder.setPositiveButton(getString(android.R.string.ok), this);
    211         mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
    212         AlertDialog dialog = mBuilder.create();
    213         dialog.setOnShowListener(d -> {
    214             if (TextUtils.isEmpty(getPairingViewText())) {
    215                 mDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false);
    216             }
    217             if (mPairingView != null && mPairingView.requestFocus()) {
    218                 InputMethodManager imm = (InputMethodManager)
    219                         getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
    220                 if (imm != null) {
    221                     imm.showSoftInput(mPairingView, InputMethodManager.SHOW_IMPLICIT);
    222                 }
    223             }
    224         });
    225         return dialog;
    226     }
    227 
    228     /**
    229      * Creates the custom view with UI elements for user input.
    230      */
    231     private View createPinEntryView() {
    232         View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_entry, null);
    233         TextView messageViewCaptionHint = (TextView) view.findViewById(R.id.pin_values_hint);
    234         TextView messageView2 = (TextView) view.findViewById(R.id.message_below_pin);
    235         CheckBox alphanumericPin = (CheckBox) view.findViewById(R.id.alphanumeric_pin);
    236         CheckBox contactSharing = (CheckBox) view.findViewById(
    237                 R.id.phonebook_sharing_message_entry_pin);
    238         contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook,
    239                 mPairingController.getDeviceName()));
    240         EditText pairingView = (EditText) view.findViewById(R.id.text);
    241 
    242         contactSharing.setVisibility(mPairingController.isProfileReady()
    243                 ? View.GONE : View.VISIBLE);
    244         contactSharing.setOnCheckedChangeListener(mPairingController);
    245         contactSharing.setChecked(mPairingController.getContactSharingState());
    246 
    247         mPairingView = pairingView;
    248 
    249         pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
    250         pairingView.addTextChangedListener(this);
    251         alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> {
    252             // change input type for soft keyboard to numeric or alphanumeric
    253             if (isChecked) {
    254                 mPairingView.setInputType(InputType.TYPE_CLASS_TEXT);
    255             } else {
    256                 mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
    257             }
    258         });
    259 
    260         int messageId = mPairingController.getDeviceVariantMessageId();
    261         int messageIdHint = mPairingController.getDeviceVariantMessageHintId();
    262         int maxLength = mPairingController.getDeviceMaxPasskeyLength();
    263         alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric()
    264                 ? View.VISIBLE : View.GONE);
    265         if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
    266             messageView2.setText(messageId);
    267         } else {
    268             messageView2.setVisibility(View.GONE);
    269         }
    270         if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) {
    271             messageViewCaptionHint.setText(messageIdHint);
    272         } else {
    273             messageViewCaptionHint.setVisibility(View.GONE);
    274         }
    275         pairingView.setFilters(new InputFilter[]{
    276                 new LengthFilter(maxLength)});
    277 
    278         return view;
    279     }
    280 
    281     /**
    282      * Creates a dialog with UI elements that allow the user to confirm a pairing request.
    283      */
    284     private AlertDialog createConfirmationDialog() {
    285         mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
    286                 mPairingController.getDeviceName()));
    287         mBuilder.setView(createView());
    288         mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept), this);
    289         mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline), this);
    290         AlertDialog dialog = mBuilder.create();
    291         return dialog;
    292     }
    293 
    294     /**
    295      * Creates a dialog with UI elements that allow the user to consent to a pairing request.
    296      */
    297     private AlertDialog createConsentDialog() {
    298         return createConfirmationDialog();
    299     }
    300 
    301     /**
    302      * Creates a dialog that informs users of a pairing request and shows them the passkey/pin
    303      * of the device.
    304      */
    305     private AlertDialog createDisplayPasskeyOrPinDialog() {
    306         mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
    307                 mPairingController.getDeviceName()));
    308         mBuilder.setView(createView());
    309         mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
    310         AlertDialog dialog = mBuilder.create();
    311 
    312         // Tell the controller the dialog has been created.
    313         mPairingController.notifyDialogDisplayed();
    314 
    315         return dialog;
    316     }
    317 
    318     /**
    319      * Creates a custom view for dialogs which need to show users additional information but do
    320      * not require user input.
    321      */
    322     private View createView() {
    323         View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null);
    324         TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption);
    325         TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead);
    326         TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message);
    327         CheckBox contactSharing = (CheckBox) view.findViewById(
    328                 R.id.phonebook_sharing_message_confirm_pin);
    329         contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook,
    330                 mPairingController.getDeviceName()));
    331 
    332         contactSharing.setVisibility(
    333                 mPairingController.isProfileReady() ? View.GONE : View.VISIBLE);
    334         contactSharing.setChecked(mPairingController.getContactSharingState());
    335         contactSharing.setOnCheckedChangeListener(mPairingController);
    336 
    337         messagePairing.setVisibility(mPairingController.isDisplayPairingKeyVariant()
    338                 ? View.VISIBLE : View.GONE);
    339         if (mPairingController.hasPairingContent()) {
    340             pairingViewCaption.setVisibility(View.VISIBLE);
    341             pairingViewContent.setVisibility(View.VISIBLE);
    342             pairingViewContent.setText(mPairingController.getPairingContent());
    343         }
    344         return view;
    345     }
    346 
    347 }
    348