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