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