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