• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
onDialogNegativeClick(BluetoothPairingDialogFragment dialog)63         void onDialogNegativeClick(BluetoothPairingDialogFragment dialog);
64 
onDialogPositiveClick(BluetoothPairingDialogFragment dialog)65         void onDialogPositiveClick(BluetoothPairingDialogFragment dialog);
66     }
67 
68     @Override
onCreateDialog(Bundle savedInstanceState)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
beforeTextChanged(CharSequence s, int start, int count, int after)85     public void beforeTextChanged(CharSequence s, int start, int count, int after) {
86     }
87 
88     @Override
onTextChanged(CharSequence s, int start, int before, int count)89     public void onTextChanged(CharSequence s, int start, int before, int count) {
90     }
91 
92     @Override
afterTextChanged(Editable s)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
onClick(DialogInterface dialog, int which)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
getMetricsCategory()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      */
getmDialog()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      */
setPairingController(BluetoothPairingController pairingController)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      */
isPairingControllerSet()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      */
setPairingDialogActivity(BluetoothPairingDialog pairingDialogActivity)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      */
isPairingDialogActivitySet()164     boolean isPairingDialogActivitySet() {
165         return mPairingDialogActivity != null;
166     }
167 
168     /**
169      * Creates the appropriate type of dialog and returns it.
170      */
setupDialog()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
getPairingViewText()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      */
createUserEntryDialog()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      */
createPinEntryView()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      */
createConfirmationDialog()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      */
createConsentDialog()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      */
createDisplayPasskeyOrPinDialog()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      */
createView()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