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 static com.google.common.truth.Truth.assertThat;
19 
20 import static org.junit.Assert.fail;
21 import static org.mockito.ArgumentMatchers.any;
22 import static org.mockito.Mockito.doNothing;
23 import static org.mockito.Mockito.doReturn;
24 import static org.mockito.Mockito.mock;
25 import static org.mockito.Mockito.spy;
26 import static org.mockito.Mockito.times;
27 import static org.mockito.Mockito.verify;
28 import static org.mockito.Mockito.when;
29 
30 import android.app.Dialog;
31 import android.content.Context;
32 import android.text.SpannableStringBuilder;
33 import android.text.TextUtils;
34 import android.view.View;
35 import android.view.inputmethod.InputMethodManager;
36 import android.widget.TextView;
37 
38 import androidx.appcompat.app.AlertDialog;
39 import androidx.fragment.app.FragmentActivity;
40 
41 import com.android.settings.R;
42 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
43 
44 import org.junit.Before;
45 import org.junit.Ignore;
46 import org.junit.Test;
47 import org.junit.runner.RunWith;
48 import org.mockito.Mock;
49 import org.mockito.MockitoAnnotations;
50 import org.robolectric.RobolectricTestRunner;
51 import org.robolectric.RuntimeEnvironment;
52 import org.robolectric.annotation.Config;
53 import org.robolectric.shadows.androidx.fragment.FragmentController;
54 
55 @RunWith(RobolectricTestRunner.class)
56 @Config(shadows = ShadowAlertDialogCompat.class)
57 public class BluetoothPairingDialogTest {
58 
59     private static final String FILLER = "text that goes in a view";
60     private static final String FAKE_DEVICE_NAME = "Fake Bluetooth Device";
61 
62     @Mock
63     private BluetoothPairingController controller;
64     @Mock
65     private BluetoothPairingDialog dialogActivity;
66 
67     @Before
setUp()68     public void setUp() {
69         MockitoAnnotations.initMocks(this);
70         doNothing().when(dialogActivity).dismiss();
71     }
72 
73     @Test
dialogUpdatesControllerWithUserInput()74     public void dialogUpdatesControllerWithUserInput() {
75         // set the correct dialog type
76         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
77 
78         // we don't care about these for this test
79         when(controller.getDeviceVariantMessageId())
80                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
81         when(controller.getDeviceVariantMessageHintId())
82                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
83 
84         // build fragment
85         BluetoothPairingDialogFragment frag = makeFragment();
86 
87         // test that controller is updated on text change
88         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
89         verify(controller, times(1)).updateUserInput(any());
90     }
91 
92     @Test
dialogEnablesSubmitButtonOnValidationFromController()93     public void dialogEnablesSubmitButtonOnValidationFromController() {
94         // set the correct dialog type
95         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
96 
97         // we don't care about these for this test
98         when(controller.getDeviceVariantMessageId())
99                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
100         when(controller.getDeviceVariantMessageHintId())
101                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
102 
103         // force the controller to say that any passkey is valid
104         when(controller.isPasskeyValid(any())).thenReturn(true);
105 
106         // build fragment
107         BluetoothPairingDialogFragment frag = makeFragment();
108 
109         // test that the positive button is enabled when passkey is valid
110         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
111         View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
112         assertThat(button).isNotNull();
113         assertThat(button.getVisibility()).isEqualTo(View.VISIBLE);
114     }
115 
116     @Test
dialogDoesNotAskForPairCodeOnConsentVariant()117     public void dialogDoesNotAskForPairCodeOnConsentVariant() {
118         // set the dialog variant to confirmation/consent
119         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
120 
121         // build the fragment
122         BluetoothPairingDialogFragment frag = makeFragment();
123 
124         // check that the input field used by the entry dialog fragment does not exist
125         View view = frag.getmDialog().findViewById(R.id.text);
126         assertThat(view).isNull();
127     }
128 
129     @Ignore
130     @Test
dialogAsksForPairCodeOnUserEntryVariant()131     public void dialogAsksForPairCodeOnUserEntryVariant() {
132         // set the dialog variant to user entry
133         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
134 
135         // we don't care about these for this test
136         when(controller.getDeviceVariantMessageId())
137                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
138         when(controller.getDeviceVariantMessageHintId())
139                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
140 
141         Context context = spy(RuntimeEnvironment.application);
142         InputMethodManager imm = mock(InputMethodManager.class);
143         doReturn(imm).when(context).getSystemService(Context.INPUT_METHOD_SERVICE);
144 
145         // build the fragment
146         BluetoothPairingDialogFragment frag = spy(new BluetoothPairingDialogFragment());
147         when(frag.getContext()).thenReturn(context);
148         setupFragment(frag);
149         AlertDialog alertDialog = frag.getmDialog();
150 
151         // check that the pin/passkey input field is visible to the user
152         View view = alertDialog.findViewById(R.id.text);
153         assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
154 
155         // check that showSoftInput was called to make input method appear when the dialog was shown
156         assertThat(view.isFocused()).isTrue();
157         // TODO(b/73892004): Figure out why this is failing.
158         // assertThat(imm.isActive()).isTrue();
159         verify(imm).showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
160     }
161 
162     @Test
dialogDisplaysPairCodeOnDisplayPasskeyVariant()163     public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() {
164         // set the dialog variant to display passkey
165         when(controller.getDialogType())
166                 .thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG);
167 
168         // ensure that the controller returns good values to indicate a passkey needs to be shown
169         when(controller.isDisplayPairingKeyVariant()).thenReturn(true);
170         when(controller.hasPairingContent()).thenReturn(true);
171         when(controller.getPairingContent()).thenReturn(FILLER);
172 
173         // build the fragment
174         BluetoothPairingDialogFragment frag = makeFragment();
175 
176         // get the relevant views
177         View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message);
178         TextView pairingViewContent = frag.getmDialog().findViewById(R.id.pairing_subhead);
179         View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption);
180 
181         // check that the relevant views are visible and that the passkey is shown
182         assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE);
183         assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE);
184         assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE);
185         assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue();
186     }
187 
188     @Test(expected = IllegalStateException.class)
dialogThrowsExceptionIfNoControllerSet()189     public void dialogThrowsExceptionIfNoControllerSet() {
190         // instantiate a fragment
191         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
192 
193         // this should throw an error
194         FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */,
195                 null /* bundle */);
196         fail("Starting the fragment with no controller set should have thrown an exception.");
197     }
198 
199     @Test
dialogCallsHookOnPositiveButtonPress()200     public void dialogCallsHookOnPositiveButtonPress() {
201         // set the dialog variant to confirmation/consent
202         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
203 
204         // we don't care what this does, just that it is called
205         doNothing().when(controller).onDialogPositiveClick(any());
206 
207         // build the fragment
208         BluetoothPairingDialogFragment frag = makeFragment();
209 
210         // click the button and verify that the controller hook was called
211         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
212         verify(controller, times(1)).onDialogPositiveClick(any());
213     }
214 
215     @Test
dialogCallsHookOnNegativeButtonPress()216     public void dialogCallsHookOnNegativeButtonPress() {
217         // set the dialog variant to confirmation/consent
218         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
219 
220         // we don't care what this does, just that it is called
221         doNothing().when(controller).onDialogNegativeClick(any());
222 
223         // build the fragment
224         BluetoothPairingDialogFragment frag = makeFragment();
225 
226         // click the button and verify that the controller hook was called
227         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
228         verify(controller, times(1)).onDialogNegativeClick(any());
229     }
230 
231     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingController()232     public void dialogDoesNotAllowSwappingController() {
233         // instantiate a fragment
234         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
235         frag.setPairingController(controller);
236 
237         // this should throw an error
238         frag.setPairingController(controller);
239         fail("Setting the controller multiple times should throw an exception.");
240     }
241 
242     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingActivity()243     public void dialogDoesNotAllowSwappingActivity() {
244         // instantiate a fragment
245         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
246         frag.setPairingDialogActivity(dialogActivity);
247 
248         // this should throw an error
249         frag.setPairingDialogActivity(dialogActivity);
250         fail("Setting the dialog activity multiple times should throw an exception.");
251     }
252 
253     @Test
dialogPositiveButtonDisabledWhenUserInputInvalid()254     public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
255         // set the correct dialog type
256         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
257 
258         // we don't care about these for this test
259         when(controller.getDeviceVariantMessageId())
260                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
261         when(controller.getDeviceVariantMessageHintId())
262                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
263 
264         // force the controller to say that any passkey is valid
265         when(controller.isPasskeyValid(any())).thenReturn(false);
266 
267         // build fragment
268         BluetoothPairingDialogFragment frag = makeFragment();
269 
270         // test that the positive button is enabled when passkey is valid
271         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
272         View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
273         assertThat(button).isNotNull();
274         assertThat(button.isEnabled()).isFalse();
275     }
276 
277     @Test
contactSharingToggle_conditionIsReady_showsUi()278     public void contactSharingToggle_conditionIsReady_showsUi() {
279         // set the dialog variant to confirmation/consent
280         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
281         // set a fake device name and pretend the profile has not been set up for it
282         when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
283         when(controller.isContactSharingVisible()).thenReturn(true);
284 
285         // build the fragment
286         BluetoothPairingDialogFragment frag = makeFragment();
287 
288         // verify that the toggle is visible
289         View sharingToggle =
290                 frag.getmDialog().findViewById(R.id.phonebook_sharing);
291         assertThat(sharingToggle.getVisibility()).isEqualTo(View.VISIBLE);
292     }
293 
294     @Test
contactSharingToggle_conditionIsNotReady_doesNotShowUi()295     public void contactSharingToggle_conditionIsNotReady_doesNotShowUi() {
296         // set the dialog variant to confirmation/consent
297         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
298         // set a fake device name and pretend the profile has been set up for it
299         when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
300         when(controller.isContactSharingVisible()).thenReturn(false);
301 
302         // build the fragment
303         BluetoothPairingDialogFragment frag = makeFragment();
304 
305         // verify that the toggle is gone
306         View sharingToggle =
307                 frag.getmDialog().findViewById(R.id.phonebook_sharing);
308         assertThat(sharingToggle.getVisibility()).isEqualTo(View.GONE);
309     }
310 
311     @Test
dialogShowsMessageOnPinEntryView()312     public void dialogShowsMessageOnPinEntryView() {
313         // set the correct dialog type
314         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
315 
316         // Set the message id to something specific to verify later
317         when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel);
318         when(controller.getDeviceVariantMessageHintId())
319                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
320 
321         // build the fragment
322         BluetoothPairingDialogFragment frag = makeFragment();
323 
324         // verify message is what we expect it to be and is visible
325         TextView message = frag.getmDialog().findViewById(R.id.message_below_pin);
326         assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
327         assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue();
328     }
329 
330     @Test
dialogShowsMessageHintOnPinEntryView()331     public void dialogShowsMessageHintOnPinEntryView() {
332         // set the correct dialog type
333         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
334 
335         // Set the message id hint to something specific to verify later
336         when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel);
337         when(controller.getDeviceVariantMessageId())
338                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
339 
340         // build the fragment
341         BluetoothPairingDialogFragment frag = makeFragment();
342 
343         // verify message is what we expect it to be and is visible
344         TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint);
345         assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE);
346         assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue();
347     }
348 
349     @Test
dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView()350     public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() {
351         // set the correct dialog type
352         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
353 
354         // Set the id's to what is returned when it is not provided
355         when(controller.getDeviceVariantMessageHintId())
356                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
357         when(controller.getDeviceVariantMessageId())
358                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
359 
360         // build the fragment
361         BluetoothPairingDialogFragment frag = makeFragment();
362 
363         // verify message is what we expect it to be and is visible
364         TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint);
365         assertThat(hint.getVisibility()).isEqualTo(View.GONE);
366         TextView message = frag.getmDialog().findViewById(R.id.message_below_pin);
367         assertThat(message.getVisibility()).isEqualTo(View.GONE);
368     }
369 
370     @Test
pairingDialogDismissedOnPositiveClick()371     public void pairingDialogDismissedOnPositiveClick() {
372         // set the dialog variant to confirmation/consent
373         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
374 
375         // we don't care what this does, just that it is called
376         doNothing().when(controller).onDialogPositiveClick(any());
377 
378         // build the fragment
379         BluetoothPairingDialogFragment frag = makeFragment();
380 
381         // click the button and verify that the controller hook was called
382         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
383 
384         verify(controller, times(1)).onDialogPositiveClick(any());
385         verify(dialogActivity, times(1)).dismiss();
386     }
387 
388     @Test
pairingDialogDismissedOnNegativeClick()389     public void pairingDialogDismissedOnNegativeClick() {
390         // set the dialog variant to confirmation/consent
391         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
392 
393         // we don't care what this does, just that it is called
394         doNothing().when(controller).onDialogNegativeClick(any());
395 
396         // build the fragment
397         BluetoothPairingDialogFragment frag = makeFragment();
398 
399         // click the button and verify that the controller hook was called
400         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
401 
402         verify(controller, times(1)).onDialogNegativeClick(any());
403         verify(dialogActivity, times(1)).dismiss();
404     }
405 
406     @Ignore
407     @Test
rotateDialog_nullPinText_okButtonEnabled()408     public void rotateDialog_nullPinText_okButtonEnabled() {
409         userEntryDialogExistingTextTest(null);
410     }
411 
412     @Ignore
413     @Test
rotateDialog_emptyPinText_okButtonEnabled()414     public void rotateDialog_emptyPinText_okButtonEnabled() {
415         userEntryDialogExistingTextTest("");
416     }
417 
418     @Ignore
419     @Test
rotateDialog_nonEmptyPinText_okButtonEnabled()420     public void rotateDialog_nonEmptyPinText_okButtonEnabled() {
421         userEntryDialogExistingTextTest("test");
422     }
423 
424     @Test
groupPairing_setMemberDevice_showsMessageHint()425     public void groupPairing_setMemberDevice_showsMessageHint() {
426         // set the correct dialog type
427         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
428         when(controller.isCoordinatedSetMemberDevice()).thenReturn(true);
429 
430         // build the fragment
431         BluetoothPairingDialogFragment frag = makeFragment();
432 
433         // verify message is what we expect it to be and is visible
434         TextView message = frag.getmDialog().findViewById(R.id.pairing_group_message);
435         assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
436     }
437 
438     @Test
groupPairing_nonSetMemberDevice_hidesMessageHint()439     public void groupPairing_nonSetMemberDevice_hidesMessageHint() {
440         // set the correct dialog type
441         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
442         when(controller.isCoordinatedSetMemberDevice()).thenReturn(false);
443 
444         // build the fragment
445         BluetoothPairingDialogFragment frag = makeFragment();
446 
447         // verify message is what we expect it to be and is visible
448         TextView message = frag.getmDialog().findViewById(R.id.pairing_group_message);
449         assertThat(message.getVisibility()).isEqualTo(View.GONE);
450     }
451 
452     // Runs a test simulating the user entry dialog type in a situation like device rotation, where
453     // the dialog fragment gets created and we already have some existing text entered into the
454     // pin field.
userEntryDialogExistingTextTest(CharSequence existingText)455     private void userEntryDialogExistingTextTest(CharSequence existingText) {
456         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
457         when(controller.getDeviceVariantMessageHintId())
458                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
459         when(controller.getDeviceVariantMessageId())
460                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
461 
462         BluetoothPairingDialogFragment fragment = spy(new BluetoothPairingDialogFragment());
463         when(fragment.getPairingViewText()).thenReturn(existingText);
464         setupFragment(fragment);
465         AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
466         assertThat(dialog).isNotNull();
467         boolean expected = !TextUtils.isEmpty(existingText);
468         assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled()).isEqualTo(expected);
469     }
470 
setupFragment(BluetoothPairingDialogFragment frag)471     private void setupFragment(BluetoothPairingDialogFragment frag) {
472         assertThat(frag.isPairingControllerSet()).isFalse();
473         frag.setPairingController(controller);
474         assertThat(frag.isPairingDialogActivitySet()).isFalse();
475         frag.setPairingDialogActivity(dialogActivity);
476         FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */,
477                 null /* bundle */);
478         assertThat(frag.getmDialog()).isNotNull();
479         assertThat(frag.isPairingControllerSet()).isTrue();
480         assertThat(frag.isPairingDialogActivitySet()).isTrue();
481     }
482 
makeFragment()483     private BluetoothPairingDialogFragment makeFragment() {
484         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
485         setupFragment(frag);
486         return frag;
487     }
488 }
489