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