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