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 android.app.AlertDialog;
19 import android.content.Context;
20 import android.text.SpannableStringBuilder;
21 import android.text.TextUtils;
22 import android.view.View;
23 import android.widget.CheckBox;
24 import android.widget.TextView;
25 
26 import com.android.settings.R;
27 import com.android.settings.SettingsRobolectricTestRunner;
28 import com.android.settings.TestConfig;
29 
30 import org.junit.Before;
31 import org.junit.Test;
32 import org.junit.runner.RunWith;
33 import org.mockito.Mock;
34 import org.mockito.MockitoAnnotations;
35 import org.robolectric.annotation.Config;
36 import org.robolectric.shadows.ShadowApplication;
37 import org.robolectric.util.FragmentTestUtil;
38 
39 import static com.google.common.truth.Truth.assertThat;
40 import static org.junit.Assert.fail;
41 import static org.mockito.Matchers.any;
42 import static org.mockito.Mockito.doNothing;
43 import static org.mockito.Mockito.times;
44 import static org.mockito.Mockito.verify;
45 import static org.mockito.Mockito.when;
46 
47 @RunWith(SettingsRobolectricTestRunner.class)
48 @Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
49 public class BluetoothPairingDialogTest {
50 
51     private static final String FILLER = "text that goes in a view";
52     private static final String FAKE_DEVICE_NAME = "Fake Bluetooth Device";
53 
54     @Mock
55     private BluetoothPairingController controller;
56 
57     @Mock
58     private BluetoothPairingDialog dialogActivity;
59 
60     @Before
setUp()61     public void setUp() {
62         MockitoAnnotations.initMocks(this);
63         doNothing().when(dialogActivity).dismiss();
64     }
65 
66     @Test
dialogUpdatesControllerWithUserInput()67     public void dialogUpdatesControllerWithUserInput() {
68         // set the correct dialog type
69         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
70 
71         // we don't care about these for this test
72         when(controller.getDeviceVariantMessageId())
73                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
74         when(controller.getDeviceVariantMessageHintId())
75                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
76 
77         // build fragment
78         BluetoothPairingDialogFragment frag = makeFragment();
79 
80         // test that controller is updated on text change
81         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
82         verify(controller, times(1)).updateUserInput(any());
83     }
84 
85     @Test
dialogEnablesSubmitButtonOnValidationFromController()86     public void dialogEnablesSubmitButtonOnValidationFromController() {
87         // set the correct dialog type
88         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
89 
90         // we don't care about these for this test
91         when(controller.getDeviceVariantMessageId())
92                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
93         when(controller.getDeviceVariantMessageHintId())
94                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
95 
96         // force the controller to say that any passkey is valid
97         when(controller.isPasskeyValid(any())).thenReturn(true);
98 
99         // build fragment
100         BluetoothPairingDialogFragment frag = makeFragment();
101 
102         // test that the positive button is enabled when passkey is valid
103         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
104         View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
105         assertThat(button).isNotNull();
106         assertThat(button.getVisibility()).isEqualTo(View.VISIBLE);
107     }
108 
109     @Test
dialogDoesNotAskForPairCodeOnConsentVariant()110     public void dialogDoesNotAskForPairCodeOnConsentVariant() {
111         // set the dialog variant to confirmation/consent
112         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
113 
114         // build the fragment
115         BluetoothPairingDialogFragment frag = makeFragment();
116 
117         // check that the input field used by the entry dialog fragment does not exist
118         View view = frag.getmDialog().findViewById(R.id.text);
119         assertThat(view).isNull();
120     }
121 
122     @Test
dialogAsksForPairCodeOnUserEntryVariant()123     public void dialogAsksForPairCodeOnUserEntryVariant() {
124         // set the dialog variant to user entry
125         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
126 
127         // we don't care about these for this test
128         when(controller.getDeviceVariantMessageId())
129                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
130         when(controller.getDeviceVariantMessageHintId())
131                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
132 
133         // build the fragment
134         BluetoothPairingDialogFragment frag = makeFragment();
135 
136         // check that the pin/passkey input field is visible to the user
137         View view = frag.getmDialog().findViewById(R.id.text);
138         assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
139     }
140 
141     @Test
dialogDisplaysPairCodeOnDisplayPasskeyVariant()142     public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() {
143         // set the dialog variant to display passkey
144         when(controller.getDialogType())
145                 .thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG);
146 
147         // ensure that the controller returns good values to indicate a passkey needs to be shown
148         when(controller.isDisplayPairingKeyVariant()).thenReturn(true);
149         when(controller.hasPairingContent()).thenReturn(true);
150         when(controller.getPairingContent()).thenReturn(FILLER);
151 
152         // build the fragment
153         BluetoothPairingDialogFragment frag = makeFragment();
154 
155         // get the relevant views
156         View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message);
157         TextView pairingViewContent =
158                 (TextView) frag.getmDialog().findViewById(R.id.pairing_subhead);
159         View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption);
160 
161         // check that the relevant views are visible and that the passkey is shown
162         assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE);
163         assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE);
164         assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE);
165         assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue();
166     }
167 
168     @Test(expected = IllegalStateException.class)
dialogThrowsExceptionIfNoControllerSet()169     public void dialogThrowsExceptionIfNoControllerSet() {
170         // instantiate a fragment
171         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
172 
173         // this should throw an error
174         FragmentTestUtil.startFragment(frag);
175         fail("Starting the fragment with no controller set should have thrown an exception.");
176     }
177 
178     @Test
dialogCallsHookOnPositiveButtonPress()179     public void dialogCallsHookOnPositiveButtonPress() {
180         // set the dialog variant to confirmation/consent
181         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
182 
183         // we don't care what this does, just that it is called
184         doNothing().when(controller).onDialogPositiveClick(any());
185 
186         // build the fragment
187         BluetoothPairingDialogFragment frag = makeFragment();
188 
189         // click the button and verify that the controller hook was called
190         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
191         verify(controller, times(1)).onDialogPositiveClick(any());
192     }
193 
194     @Test
dialogCallsHookOnNegativeButtonPress()195     public void dialogCallsHookOnNegativeButtonPress() {
196         // set the dialog variant to confirmation/consent
197         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
198 
199         // we don't care what this does, just that it is called
200         doNothing().when(controller).onDialogNegativeClick(any());
201 
202         // build the fragment
203         BluetoothPairingDialogFragment frag = makeFragment();
204 
205         // click the button and verify that the controller hook was called
206         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
207         verify(controller, times(1)).onDialogNegativeClick(any());
208     }
209 
210     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingController()211     public void dialogDoesNotAllowSwappingController() {
212         // instantiate a fragment
213         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
214         frag.setPairingController(controller);
215 
216         // this should throw an error
217         frag.setPairingController(controller);
218         fail("Setting the controller multiple times should throw an exception.");
219     }
220 
221     @Test(expected = IllegalStateException.class)
dialogDoesNotAllowSwappingActivity()222     public void dialogDoesNotAllowSwappingActivity() {
223         // instantiate a fragment
224         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
225         frag.setPairingDialogActivity(dialogActivity);
226 
227         // this should throw an error
228         frag.setPairingDialogActivity(dialogActivity);
229         fail("Setting the dialog activity multiple times should throw an exception.");
230     }
231 
232     @Test
dialogPositiveButtonDisabledWhenUserInputInvalid()233     public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
234         // set the correct dialog type
235         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
236 
237         // we don't care about these for this test
238         when(controller.getDeviceVariantMessageId())
239                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
240         when(controller.getDeviceVariantMessageHintId())
241                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
242 
243         // force the controller to say that any passkey is valid
244         when(controller.isPasskeyValid(any())).thenReturn(false);
245 
246         // build fragment
247         BluetoothPairingDialogFragment frag = makeFragment();
248 
249         // test that the positive button is enabled when passkey is valid
250         frag.afterTextChanged(new SpannableStringBuilder(FILLER));
251         View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
252         assertThat(button).isNotNull();
253         assertThat(button.isEnabled()).isFalse();
254     }
255 
256     @Test
dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady()257     public void dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady() {
258         // set the dialog variant to confirmation/consent
259         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
260 
261         // set a fake device name and pretend the profile has not been set up for it
262         when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
263         when(controller.isProfileReady()).thenReturn(false);
264 
265         // build the fragment
266         BluetoothPairingDialogFragment frag = makeFragment();
267 
268         // verify that the checkbox is visible and that the device name is correct
269         CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
270                 .findViewById(R.id.phonebook_sharing_message_confirm_pin);
271         assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.VISIBLE);
272         assertThat(sharingCheckbox.getText().toString().contains(FAKE_DEVICE_NAME)).isTrue();
273     }
274 
275     @Test
dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady()276     public void dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady() {
277         // set the dialog variant to confirmation/consent
278         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
279 
280         // set a fake device name and pretend the profile has been set up for it
281         when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
282         when(controller.isProfileReady()).thenReturn(true);
283 
284         // build the fragment
285         BluetoothPairingDialogFragment frag = makeFragment();
286 
287         // verify that the checkbox is gone
288         CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
289                 .findViewById(R.id.phonebook_sharing_message_confirm_pin);
290         assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.GONE);
291     }
292 
293     @Test
dialogShowsMessageOnPinEntryView()294     public void dialogShowsMessageOnPinEntryView() {
295         // set the correct dialog type
296         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
297 
298         // Set the message id to something specific to verify later
299         when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel);
300         when(controller.getDeviceVariantMessageHintId())
301                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
302 
303         // build the fragment
304         BluetoothPairingDialogFragment frag = makeFragment();
305 
306         // verify message is what we expect it to be and is visible
307         TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
308         assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
309         assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue();
310     }
311 
312     @Test
dialogShowsMessageHintOnPinEntryView()313     public void dialogShowsMessageHintOnPinEntryView() {
314         // set the correct dialog type
315         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
316 
317         // Set the message id hint to something specific to verify later
318         when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel);
319         when(controller.getDeviceVariantMessageId())
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 hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
327         assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE);
328         assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue();
329     }
330 
331     @Test
dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView()332     public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() {
333         // set the correct dialog type
334         when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
335 
336         // Set the id's to what is returned when it is not provided
337         when(controller.getDeviceVariantMessageHintId())
338                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
339         when(controller.getDeviceVariantMessageId())
340                 .thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
341 
342         // build the fragment
343         BluetoothPairingDialogFragment frag = makeFragment();
344 
345         // verify message is what we expect it to be and is visible
346         TextView hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
347         assertThat(hint.getVisibility()).isEqualTo(View.GONE);
348         TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
349         assertThat(message.getVisibility()).isEqualTo(View.GONE);
350     }
351 
352     @Test
pairingStringIsFormattedCorrectly()353     public void pairingStringIsFormattedCorrectly() {
354         final String device = "test_device";
355         final Context context = ShadowApplication.getInstance().getApplicationContext();
356         assertThat(context.getString(R.string.bluetooth_pb_acceptance_dialog_text, device, device))
357                 .contains(device);
358     }
359 
360     @Test
pairingDialogDismissedOnPositiveClick()361     public void pairingDialogDismissedOnPositiveClick() {
362         // set the dialog variant to confirmation/consent
363         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
364 
365         // we don't care what this does, just that it is called
366         doNothing().when(controller).onDialogPositiveClick(any());
367 
368         // build the fragment
369         BluetoothPairingDialogFragment frag = makeFragment();
370 
371         // click the button and verify that the controller hook was called
372         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
373 
374         verify(controller, times(1)).onDialogPositiveClick(any());
375         verify(dialogActivity, times(1)).dismiss();
376     }
377 
378     @Test
pairingDialogDismissedOnNegativeClick()379     public void pairingDialogDismissedOnNegativeClick() {
380         // set the dialog variant to confirmation/consent
381         when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
382 
383         // we don't care what this does, just that it is called
384         doNothing().when(controller).onDialogNegativeClick(any());
385 
386         // build the fragment
387         BluetoothPairingDialogFragment frag = makeFragment();
388 
389         // click the button and verify that the controller hook was called
390         frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
391 
392         verify(controller, times(1)).onDialogNegativeClick(any());
393         verify(dialogActivity, times(1)).dismiss();
394     }
395 
makeFragment()396     private BluetoothPairingDialogFragment makeFragment() {
397         BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
398         assertThat(frag.isPairingControllerSet()).isFalse();
399         frag.setPairingController(controller);
400         assertThat(frag.isPairingDialogActivitySet()).isFalse();
401         frag.setPairingDialogActivity(dialogActivity);
402         FragmentTestUtil.startFragment(frag);
403         assertThat(frag.getmDialog()).isNotNull();
404         assertThat(frag.isPairingControllerSet()).isTrue();
405         assertThat(frag.isPairingDialogActivitySet()).isTrue();
406         return frag;
407     }
408 }
409