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