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 
onDialogNegativeClick(BluetoothPairingDialogFragment dialog)58         void onDialogNegativeClick(BluetoothPairingDialogFragment dialog);
59 
onDialogPositiveClick(BluetoothPairingDialogFragment dialog)60         void onDialogPositiveClick(BluetoothPairingDialogFragment dialog);
61     }
62 
63     @Override
onCreateDialog(Bundle savedInstanceState)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
beforeTextChanged(CharSequence s, int start, int count, int after)80     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
81     }
82 
83     @Override
onTextChanged(CharSequence s, int start, int before, int count)84     public void onTextChanged(CharSequence s, int start, int before, int count) {
85     }
86 
87     @Override
afterTextChanged(Editable s)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
onClick(DialogInterface dialog, int which)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
getMetricsCategory()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      */
getmDialog()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      */
setPairingController(BluetoothPairingController pairingController)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      */
isPairingControllerSet()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      */
setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity)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      */
isPairingDialogActivitySet()159     boolean isPairingDialogActivitySet() {
160         return mPairingDialogActivity != null;
161     }
162 
163     /**
164      * Creates the appropriate type of dialog and returns it.
165      */
setupDialog()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      */
createUserEntryDialog()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      */
createPinEntryView()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      */
createConfirmationDialog()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      */
createConsentDialog()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      */
createDisplayPasskeyOrPinDialog()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      */
createView()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