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