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         mPairingController.setContactSharingState();
    245         contactSharing.setOnCheckedChangeListener(mPairingController);
    246         contactSharing.setChecked(mPairingController.getContactSharingState());
    247 
    248         mPairingView = pairingView;
    249 
    250         pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
    251         pairingView.addTextChangedListener(this);
    252         alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> {
    253             // change input type for soft keyboard to numeric or alphanumeric
    254             if (isChecked) {
    255                 mPairingView.setInputType(InputType.TYPE_CLASS_TEXT);
    256             } else {
    257                 mPairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
    258             }
    259         });
    260 
    261         int messageId = mPairingController.getDeviceVariantMessageId();
    262         int messageIdHint = mPairingController.getDeviceVariantMessageHintId();
    263         int maxLength = mPairingController.getDeviceMaxPasskeyLength();
    264         alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric()
    265                 ? View.VISIBLE : View.GONE);
    266         if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
    267             messageView2.setText(messageId);
    268         } else {
    269             messageView2.setVisibility(View.GONE);
    270         }
    271         if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) {
    272             messageViewCaptionHint.setText(messageIdHint);
    273         } else {
    274             messageViewCaptionHint.setVisibility(View.GONE);
    275         }
    276         pairingView.setFilters(new InputFilter[]{
    277                 new LengthFilter(maxLength)});
    278 
    279         return view;
    280     }
    281 
    282     /**
    283      * Creates a dialog with UI elements that allow the user to confirm a pairing request.
    284      */
    285     private AlertDialog createConfirmationDialog() {
    286         mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
    287                 mPairingController.getDeviceName()));
    288         mBuilder.setView(createView());
    289         mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept), this);
    290         mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline), this);
    291         AlertDialog dialog = mBuilder.create();
    292         return dialog;
    293     }
    294 
    295     /**
    296      * Creates a dialog with UI elements that allow the user to consent to a pairing request.
    297      */
    298     private AlertDialog createConsentDialog() {
    299         return createConfirmationDialog();
    300     }
    301 
    302     /**
    303      * Creates a dialog that informs users of a pairing request and shows them the passkey/pin
    304      * of the device.
    305      */
    306     private AlertDialog createDisplayPasskeyOrPinDialog() {
    307         mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
    308                 mPairingController.getDeviceName()));
    309         mBuilder.setView(createView());
    310         mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
    311         AlertDialog dialog = mBuilder.create();
    312 
    313         // Tell the controller the dialog has been created.
    314         mPairingController.notifyDialogDisplayed();
    315 
    316         return dialog;
    317     }
    318 
    319     /**
    320      * Creates a custom view for dialogs which need to show users additional information but do
    321      * not require user input.
    322      */
    323     private View createView() {
    324         View view = getActivity().getLayoutInflater().inflate(R.layout.bluetooth_pin_confirm, null);
    325         TextView pairingViewCaption = (TextView) view.findViewById(R.id.pairing_caption);
    326         TextView pairingViewContent = (TextView) view.findViewById(R.id.pairing_subhead);
    327         TextView messagePairing = (TextView) view.findViewById(R.id.pairing_code_message);
    328         CheckBox contactSharing = (CheckBox) view.findViewById(
    329                 R.id.phonebook_sharing_message_confirm_pin);
    330         contactSharing.setText(getString(R.string.bluetooth_pairing_shares_phonebook,
    331                 mPairingController.getDeviceName()));
    332 
    333         contactSharing.setVisibility(
    334                 mPairingController.isProfileReady() ? View.GONE : View.VISIBLE);
    335         mPairingController.setContactSharingState();
    336         contactSharing.setChecked(mPairingController.getContactSharingState());
    337         contactSharing.setOnCheckedChangeListener(mPairingController);
    338 
    339         messagePairing.setVisibility(mPairingController.isDisplayPairingKeyVariant()
    340                 ? View.VISIBLE : View.GONE);
    341         if (mPairingController.hasPairingContent()) {
    342             pairingViewCaption.setVisibility(View.VISIBLE);
    343             pairingViewContent.setVisibility(View.VISIBLE);
    344             pairingViewContent.setText(mPairingController.getPairingContent());
    345         }
    346         return view;
    347     }
    348 
    349 }
    350