• 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 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.CheckBox;
37 import android.widget.TextView;
38 
39 import androidx.appcompat.app.AlertDialog;
40 import androidx.fragment.app.FragmentActivity;
41 
42 import com.android.settings.R;
43 import com.android.settings.testutils.shadow.ShadowAlertDialogCompat;
44 
45 import org.junit.Before;
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     @Test
dialogAsksForPairCodeOnUserEntryVariant()130     public void dialogAsksForPairCodeOnUserEntryVariant() {
131         // set the dialog variant to user entry
132         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
133 
134         // we don't care about these for this test
135         when(controller.getDeviceVariantMessageId())
136                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
137         when(controller.getDeviceVariantMessageHintId())
138                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
139 
140         Context context = spy(RuntimeEnvironment.application);
141         InputMethodManager imm = mock(InputMethodManager.class);
142         doReturn(imm).when(context).getSystemService(Context.INPUT_METHOD_SERVICE);
143 
144         // build the fragment
145         BluetoothPairingDialogFragment frag = spy(new BluetoothPairingDialogFragment());
146         when(frag.getContext()).thenReturn(context);
147         setupFragment(frag);
148         AlertDialog alertDialog = frag.getmDialog();
149 
150         // check that the pin/passkey input field is visible to the user
151         View view = alertDialog.findViewById(R.id.text);
152         assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
153 
154         // check that showSoftInput was called to make input method appear when the dialog was shown
155         assertThat(view.isFocused()).isTrue();
156         // TODO(b/73892004): Figure out why this is failing.
157         // assertThat(imm.isActive()).isTrue();
158         verify(imm).showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
159     }
160 
161     @Test
dialogDisplaysPairCodeOnDisplayPasskeyVariant()162     public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() {
163         // set the dialog variant to display passkey
164         when(controller.getDialogType())
165                 .thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG);
166 
167         // ensure that the controller returns good values to indicate a passkey needs to be shown
168         when(controller.isDisplayPairingKeyVariant()).thenReturn(true);
169         when(controller.hasPairingContent()).thenReturn(true);
170         when(controller.getPairingContent()).thenReturn(FILLER);
171 
172         // build the fragment
173         BluetoothPairingDialogFragment frag = makeFragment();
174 
175         // get the relevant views
176         View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message);
177         TextView pairingViewContent = frag.getmDialog().findViewById(R.id.pairing_subhead);
178         View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption);
179 
180         // check that the relevant views are visible and that the passkey is shown
181         assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE);
182         assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE);
183         assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE);
184         assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue();
185     }
186 
187     @Test(expected = IllegalStateException.class)
dialogThrowsExceptionIfNoControllerSet()188     public void dialogThrowsExceptionIfNoControllerSet() {
189         // instantiate a fragment
190         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
191 
192         // this should throw an error
193         FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */,
194                 null /* bundle */);
195         fail("Starting the fragment with no controller set should have thrown an exception.");
196     }
197 
198     @Test
dialogCallsHookOnPositiveButtonPress()199     public void dialogCallsHookOnPositiveButtonPress() {
200         // set the dialog variant to confirmation/consent
201         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
202 
203         // we don't care what this does, just that it is called
204         doNothing().when(controller).onDialogPositiveClick(any());
205 
206         // build the fragment
207         BluetoothPairingDialogFragment frag = makeFragment();
208 
209         // click the button and verify that the controller hook was called
210         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
211         verify(controller, times(1)).onDialogPositiveClick(any());
212     }
213 
214     @Test
dialogCallsHookOnNegativeButtonPress()215     public void dialogCallsHookOnNegativeButtonPress() {
216         // set the dialog variant to confirmation/consent
217         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
218 
219         // we don't care what this does, just that it is called
220         doNothing().when(controller).onDialogNegativeClick(any());
221 
222         // build the fragment
223         BluetoothPairingDialogFragment frag = makeFragment();
224 
225         // click the button and verify that the controller hook was called
226         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
227         verify(controller, times(1)).onDialogNegativeClick(any());
228     }
229 
230     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingController()231     public void dialogDoesNotAllowSwappingController() {
232         // instantiate a fragment
233         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
234         frag.setPairingController(controller);
235 
236         // this should throw an error
237         frag.setPairingController(controller);
238         fail("Setting the controller multiple times should throw an exception.");
239     }
240 
241     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingActivity()242     public void dialogDoesNotAllowSwappingActivity() {
243         // instantiate a fragment
244         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
245         frag.setPairingDialogActivity(dialogActivity);
246 
247         // this should throw an error
248         frag.setPairingDialogActivity(dialogActivity);
249         fail("Setting the dialog activity multiple times should throw an exception.");
250     }
251 
252     @Test
dialogPositiveButtonDisabledWhenUserInputInvalid()253     public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
254         // set the correct dialog type
255         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
256 
257         // we don't care about these for this test
258         when(controller.getDeviceVariantMessageId())
259                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
260         when(controller.getDeviceVariantMessageHintId())
261                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
262 
263         // force the controller to say that any passkey is valid
264         when(controller.isPasskeyValid(any())).thenReturn(false);
265 
266         // build fragment
267         BluetoothPairingDialogFragment frag = makeFragment();
268 
269         // test that the positive button is enabled when passkey is valid
270         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
271         View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
272         assertThat(button).isNotNull();
273         assertThat(button.isEnabled()).isFalse();
274     }
275 
276     @Test
dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady()277     public void dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady() {
278         // set the dialog variant to confirmation/consent
279         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
280 
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.isProfileReady()).thenReturn(false);
284 
285         // build the fragment
286         BluetoothPairingDialogFragment frag = makeFragment();
287 
288         // verify that the checkbox is visible and that the device name is correct
289         CheckBox sharingCheckbox =
290                 frag.getmDialog().findViewById(R.id.phonebook_sharing_message_confirm_pin);
291         assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.VISIBLE);
292     }
293 
294     @Test
dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady()295     public void dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady() {
296         // set the dialog variant to confirmation/consent
297         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
298 
299         // set a fake device name and pretend the profile has been set up for it
300         when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
301         when(controller.isProfileReady()).thenReturn(true);
302 
303         // build the fragment
304         BluetoothPairingDialogFragment frag = makeFragment();
305 
306         // verify that the checkbox is gone
307         CheckBox sharingCheckbox =
308                 frag.getmDialog().findViewById(R.id.phonebook_sharing_message_confirm_pin);
309         assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.GONE);
310     }
311 
312     @Test
dialogShowsMessageOnPinEntryView()313     public void dialogShowsMessageOnPinEntryView() {
314         // set the correct dialog type
315         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
316 
317         // Set the message id to something specific to verify later
318         when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel);
319         when(controller.getDeviceVariantMessageHintId())
320                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
321 
322         // build the fragment
323         BluetoothPairingDialogFragment frag = makeFragment();
324 
325         // verify message is what we expect it to be and is visible
326         TextView message = frag.getmDialog().findViewById(R.id.message_below_pin);
327         assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
328         assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue();
329     }
330 
331     @Test
dialogShowsMessageHintOnPinEntryView()332     public void dialogShowsMessageHintOnPinEntryView() {
333         // set the correct dialog type
334         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
335 
336         // Set the message id hint to something specific to verify later
337         when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel);
338         when(controller.getDeviceVariantMessageId())
339                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
340 
341         // build the fragment
342         BluetoothPairingDialogFragment frag = makeFragment();
343 
344         // verify message is what we expect it to be and is visible
345         TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint);
346         assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE);
347         assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue();
348     }
349 
350     @Test
dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView()351     public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() {
352         // set the correct dialog type
353         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
354 
355         // Set the id's to what is returned when it is not provided
356         when(controller.getDeviceVariantMessageHintId())
357                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
358         when(controller.getDeviceVariantMessageId())
359                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
360 
361         // build the fragment
362         BluetoothPairingDialogFragment frag = makeFragment();
363 
364         // verify message is what we expect it to be and is visible
365         TextView hint = frag.getmDialog().findViewById(R.id.pin_values_hint);
366         assertThat(hint.getVisibility()).isEqualTo(View.GONE);
367         TextView message = frag.getmDialog().findViewById(R.id.message_below_pin);
368         assertThat(message.getVisibility()).isEqualTo(View.GONE);
369     }
370 
371     @Test
pairingStringIsFormattedCorrectly()372     public void pairingStringIsFormattedCorrectly() {
373         final String device = "test_device";
374         final Context context = RuntimeEnvironment.application;
375         assertThat(context.getString(R.string.bluetooth_pb_acceptance_dialog_text, device, device))
376                 .contains(device);
377     }
378 
379     @Test
pairingDialogDismissedOnPositiveClick()380     public void pairingDialogDismissedOnPositiveClick() {
381         // set the dialog variant to confirmation/consent
382         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
383 
384         // we don't care what this does, just that it is called
385         doNothing().when(controller).onDialogPositiveClick(any());
386 
387         // build the fragment
388         BluetoothPairingDialogFragment frag = makeFragment();
389 
390         // click the button and verify that the controller hook was called
391         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
392 
393         verify(controller, times(1)).onDialogPositiveClick(any());
394         verify(dialogActivity, times(1)).dismiss();
395     }
396 
397     @Test
pairingDialogDismissedOnNegativeClick()398     public void pairingDialogDismissedOnNegativeClick() {
399         // set the dialog variant to confirmation/consent
400         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
401 
402         // we don't care what this does, just that it is called
403         doNothing().when(controller).onDialogNegativeClick(any());
404 
405         // build the fragment
406         BluetoothPairingDialogFragment frag = makeFragment();
407 
408         // click the button and verify that the controller hook was called
409         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
410 
411         verify(controller, times(1)).onDialogNegativeClick(any());
412         verify(dialogActivity, times(1)).dismiss();
413     }
414 
415     @Test
rotateDialog_nullPinText_okButtonEnabled()416     public void rotateDialog_nullPinText_okButtonEnabled() {
417         userEntryDialogExistingTextTest(null);
418     }
419 
420     @Test
rotateDialog_emptyPinText_okButtonEnabled()421     public void rotateDialog_emptyPinText_okButtonEnabled() {
422         userEntryDialogExistingTextTest("");
423     }
424 
425     @Test
rotateDialog_nonEmptyPinText_okButtonEnabled()426     public void rotateDialog_nonEmptyPinText_okButtonEnabled() {
427         userEntryDialogExistingTextTest("test");
428     }
429 
430     // Runs a test simulating the user entry dialog type in a situation like device rotation, where
431     // the dialog fragment gets created and we already have some existing text entered into the
432     // pin field.
userEntryDialogExistingTextTest(CharSequence existingText)433     private void userEntryDialogExistingTextTest(CharSequence existingText) {
434         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
435         when(controller.getDeviceVariantMessageHintId())
436                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
437         when(controller.getDeviceVariantMessageId())
438                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
439 
440         BluetoothPairingDialogFragment fragment = spy(new BluetoothPairingDialogFragment());
441         when(fragment.getPairingViewText()).thenReturn(existingText);
442         setupFragment(fragment);
443         AlertDialog dialog = ShadowAlertDialogCompat.getLatestAlertDialog();
444         assertThat(dialog).isNotNull();
445         boolean expected = !TextUtils.isEmpty(existingText);
446         assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled()).isEqualTo(expected);
447     }
448 
setupFragment(BluetoothPairingDialogFragment frag)449     private void setupFragment(BluetoothPairingDialogFragment frag) {
450         assertThat(frag.isPairingControllerSet()).isFalse();
451         frag.setPairingController(controller);
452         assertThat(frag.isPairingDialogActivitySet()).isFalse();
453         frag.setPairingDialogActivity(dialogActivity);
454         FragmentController.setupFragment(frag, FragmentActivity.class, 0 /* containerViewId */,
455                 null /* bundle */);
456         assertThat(frag.getmDialog()).isNotNull();
457         assertThat(frag.isPairingControllerSet()).isTrue();
458         assertThat(frag.isPairingDialogActivitySet()).isTrue();
459     }
460 
makeFragment()461     private BluetoothPairingDialogFragment makeFragment() {
462         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
463         setupFragment(frag);
464         return frag;
465     }
466 }
467