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