BluetoothPairingDialogFragment has code that makes the OK button on the dialog disabled until the user has entered at least one character into the PIN field. However it didn't properly handle the case where the user had entered some text and then rotated the screen - because it always marked the OK button as disabled during onShow even if it already had some content. This CL fixes that by looking at the text content and only disabling the OK button if the content is empty. Bug: 36514895 Test: make RunSettingsRoboTests Change-Id: I4e8e70089a862e67b20ff614bbaa64fc2b641fd4
467 lines
20 KiB
Java
467 lines
20 KiB
Java
/*
|
|
* Copyright (C) 2016 The Android Open Source Project
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
package com.android.settings.bluetooth;
|
|
|
|
import static com.google.common.truth.Truth.assertThat;
|
|
|
|
import static org.junit.Assert.fail;
|
|
import static org.mockito.Matchers.any;
|
|
import static org.mockito.Mockito.doNothing;
|
|
import static org.mockito.Mockito.doReturn;
|
|
import static org.mockito.Mockito.mock;
|
|
import static org.mockito.Mockito.spy;
|
|
import static org.mockito.Mockito.times;
|
|
import static org.mockito.Mockito.verify;
|
|
import static org.mockito.Mockito.when;
|
|
|
|
import android.app.AlertDialog;
|
|
import android.app.Dialog;
|
|
import android.content.Context;
|
|
import android.text.SpannableStringBuilder;
|
|
import android.text.TextUtils;
|
|
import android.view.View;
|
|
import android.view.inputmethod.InputMethodManager;
|
|
import android.widget.CheckBox;
|
|
import android.widget.TextView;
|
|
|
|
import com.android.settings.R;
|
|
import com.android.settings.TestConfig;
|
|
import com.android.settings.testutils.SettingsRobolectricTestRunner;
|
|
import com.android.settings.testutils.shadow.ShadowEventLogWriter;
|
|
|
|
import org.junit.Before;
|
|
import org.junit.Test;
|
|
import org.junit.runner.RunWith;
|
|
import org.mockito.Mock;
|
|
import org.mockito.MockitoAnnotations;
|
|
import org.robolectric.annotation.Config;
|
|
import org.robolectric.shadows.ShadowAlertDialog;
|
|
import org.robolectric.shadows.ShadowApplication;
|
|
import org.robolectric.util.FragmentTestUtil;
|
|
|
|
@RunWith(SettingsRobolectricTestRunner.class)
|
|
@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION,
|
|
shadows=ShadowEventLogWriter.class)
|
|
public class BluetoothPairingDialogTest {
|
|
|
|
private static final String FILLER = "text that goes in a view";
|
|
private static final String FAKE_DEVICE_NAME = "Fake Bluetooth Device";
|
|
|
|
@Mock
|
|
private BluetoothPairingController controller;
|
|
|
|
@Mock
|
|
private BluetoothPairingDialog dialogActivity;
|
|
|
|
@Before
|
|
public void setUp() {
|
|
MockitoAnnotations.initMocks(this);
|
|
doNothing().when(dialogActivity).dismiss();
|
|
}
|
|
|
|
@Test
|
|
public void dialogUpdatesControllerWithUserInput() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// we don't care about these for this test
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// build fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// test that controller is updated on text change
|
|
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
|
|
verify(controller, times(1)).updateUserInput(any());
|
|
}
|
|
|
|
@Test
|
|
public void dialogEnablesSubmitButtonOnValidationFromController() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// we don't care about these for this test
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// force the controller to say that any passkey is valid
|
|
when(controller.isPasskeyValid(any())).thenReturn(true);
|
|
|
|
// build fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// test that the positive button is enabled when passkey is valid
|
|
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
|
|
View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
|
|
assertThat(button).isNotNull();
|
|
assertThat(button.getVisibility()).isEqualTo(View.VISIBLE);
|
|
}
|
|
|
|
@Test
|
|
public void dialogDoesNotAskForPairCodeOnConsentVariant() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// check that the input field used by the entry dialog fragment does not exist
|
|
View view = frag.getmDialog().findViewById(R.id.text);
|
|
assertThat(view).isNull();
|
|
}
|
|
|
|
@Test
|
|
public void dialogAsksForPairCodeOnUserEntryVariant() {
|
|
// set the dialog variant to user entry
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// we don't care about these for this test
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
Context context = spy(ShadowApplication.getInstance().getApplicationContext());
|
|
InputMethodManager imm = mock(InputMethodManager.class);
|
|
doReturn(imm).when(context).getSystemService(Context.INPUT_METHOD_SERVICE);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = spy(new BluetoothPairingDialogFragment());
|
|
when(frag.getContext()).thenReturn(context);
|
|
setupFragment(frag);
|
|
AlertDialog alertDialog = frag.getmDialog();
|
|
|
|
// check that the pin/passkey input field is visible to the user
|
|
View view = alertDialog.findViewById(R.id.text);
|
|
assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
|
|
|
|
// check that showSoftInput was called to make input method appear when the dialog was shown
|
|
assertThat(view.isFocused()).isTrue();
|
|
assertThat(imm.isActive());
|
|
verify(imm).showSoftInput(view, InputMethodManager.SHOW_IMPLICIT);
|
|
}
|
|
|
|
@Test
|
|
public void dialogDisplaysPairCodeOnDisplayPasskeyVariant() {
|
|
// set the dialog variant to display passkey
|
|
when(controller.getDialogType())
|
|
.thenReturn(BluetoothPairingController.DISPLAY_PASSKEY_DIALOG);
|
|
|
|
// ensure that the controller returns good values to indicate a passkey needs to be shown
|
|
when(controller.isDisplayPairingKeyVariant()).thenReturn(true);
|
|
when(controller.hasPairingContent()).thenReturn(true);
|
|
when(controller.getPairingContent()).thenReturn(FILLER);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// get the relevant views
|
|
View messagePairing = frag.getmDialog().findViewById(R.id.pairing_code_message);
|
|
TextView pairingViewContent =
|
|
(TextView) frag.getmDialog().findViewById(R.id.pairing_subhead);
|
|
View pairingViewCaption = frag.getmDialog().findViewById(R.id.pairing_caption);
|
|
|
|
// check that the relevant views are visible and that the passkey is shown
|
|
assertThat(messagePairing.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(pairingViewCaption.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(pairingViewContent.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(TextUtils.equals(FILLER, pairingViewContent.getText())).isTrue();
|
|
}
|
|
|
|
@Test(expected = IllegalStateException.class)
|
|
public void dialogThrowsExceptionIfNoControllerSet() {
|
|
// instantiate a fragment
|
|
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
|
|
|
|
// this should throw an error
|
|
FragmentTestUtil.startFragment(frag);
|
|
fail("Starting the fragment with no controller set should have thrown an exception.");
|
|
}
|
|
|
|
@Test
|
|
public void dialogCallsHookOnPositiveButtonPress() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// we don't care what this does, just that it is called
|
|
doNothing().when(controller).onDialogPositiveClick(any());
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// click the button and verify that the controller hook was called
|
|
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
|
|
verify(controller, times(1)).onDialogPositiveClick(any());
|
|
}
|
|
|
|
@Test
|
|
public void dialogCallsHookOnNegativeButtonPress() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// we don't care what this does, just that it is called
|
|
doNothing().when(controller).onDialogNegativeClick(any());
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// click the button and verify that the controller hook was called
|
|
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
|
|
verify(controller, times(1)).onDialogNegativeClick(any());
|
|
}
|
|
|
|
@Test(expected = IllegalStateException.class)
|
|
public void dialogDoesNotAllowSwappingController() {
|
|
// instantiate a fragment
|
|
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
|
|
frag.setPairingController(controller);
|
|
|
|
// this should throw an error
|
|
frag.setPairingController(controller);
|
|
fail("Setting the controller multiple times should throw an exception.");
|
|
}
|
|
|
|
@Test(expected = IllegalStateException.class)
|
|
public void dialogDoesNotAllowSwappingActivity() {
|
|
// instantiate a fragment
|
|
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
|
|
frag.setPairingDialogActivity(dialogActivity);
|
|
|
|
// this should throw an error
|
|
frag.setPairingDialogActivity(dialogActivity);
|
|
fail("Setting the dialog activity multiple times should throw an exception.");
|
|
}
|
|
|
|
@Test
|
|
public void dialogPositiveButtonDisabledWhenUserInputInvalid() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// we don't care about these for this test
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// force the controller to say that any passkey is valid
|
|
when(controller.isPasskeyValid(any())).thenReturn(false);
|
|
|
|
// build fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// test that the positive button is enabled when passkey is valid
|
|
frag.afterTextChanged(new SpannableStringBuilder(FILLER));
|
|
View button = frag.getmDialog().getButton(AlertDialog.BUTTON_POSITIVE);
|
|
assertThat(button).isNotNull();
|
|
assertThat(button.isEnabled()).isFalse();
|
|
}
|
|
|
|
@Test
|
|
public void dialogShowsContactSharingCheckboxWhenBluetoothProfileNotReady() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// set a fake device name and pretend the profile has not been set up for it
|
|
when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
|
|
when(controller.isProfileReady()).thenReturn(false);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// verify that the checkbox is visible and that the device name is correct
|
|
CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
|
|
.findViewById(R.id.phonebook_sharing_message_confirm_pin);
|
|
assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(sharingCheckbox.getText().toString().contains(FAKE_DEVICE_NAME)).isTrue();
|
|
}
|
|
|
|
@Test
|
|
public void dialogHidesContactSharingCheckboxWhenBluetoothProfileIsReady() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// set a fake device name and pretend the profile has been set up for it
|
|
when(controller.getDeviceName()).thenReturn(FAKE_DEVICE_NAME);
|
|
when(controller.isProfileReady()).thenReturn(true);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// verify that the checkbox is gone
|
|
CheckBox sharingCheckbox = (CheckBox) frag.getmDialog()
|
|
.findViewById(R.id.phonebook_sharing_message_confirm_pin);
|
|
assertThat(sharingCheckbox.getVisibility()).isEqualTo(View.GONE);
|
|
}
|
|
|
|
@Test
|
|
public void dialogShowsMessageOnPinEntryView() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// Set the message id to something specific to verify later
|
|
when(controller.getDeviceVariantMessageId()).thenReturn(R.string.cancel);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// verify message is what we expect it to be and is visible
|
|
TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
|
|
assertThat(message.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(TextUtils.equals(frag.getString(R.string.cancel), message.getText())).isTrue();
|
|
}
|
|
|
|
@Test
|
|
public void dialogShowsMessageHintOnPinEntryView() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// Set the message id hint to something specific to verify later
|
|
when(controller.getDeviceVariantMessageHintId()).thenReturn(R.string.cancel);
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// verify message is what we expect it to be and is visible
|
|
TextView hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
|
|
assertThat(hint.getVisibility()).isEqualTo(View.VISIBLE);
|
|
assertThat(TextUtils.equals(frag.getString(R.string.cancel), hint.getText())).isTrue();
|
|
}
|
|
|
|
@Test
|
|
public void dialogHidesMessageAndHintWhenNotProvidedOnPinEntryView() {
|
|
// set the correct dialog type
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
|
|
// Set the id's to what is returned when it is not provided
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// verify message is what we expect it to be and is visible
|
|
TextView hint = (TextView) frag.getmDialog().findViewById(R.id.pin_values_hint);
|
|
assertThat(hint.getVisibility()).isEqualTo(View.GONE);
|
|
TextView message = (TextView) frag.getmDialog().findViewById(R.id.message_below_pin);
|
|
assertThat(message.getVisibility()).isEqualTo(View.GONE);
|
|
}
|
|
|
|
@Test
|
|
public void pairingStringIsFormattedCorrectly() {
|
|
final String device = "test_device";
|
|
final Context context = ShadowApplication.getInstance().getApplicationContext();
|
|
assertThat(context.getString(R.string.bluetooth_pb_acceptance_dialog_text, device, device))
|
|
.contains(device);
|
|
}
|
|
|
|
@Test
|
|
public void pairingDialogDismissedOnPositiveClick() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// we don't care what this does, just that it is called
|
|
doNothing().when(controller).onDialogPositiveClick(any());
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// click the button and verify that the controller hook was called
|
|
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_POSITIVE);
|
|
|
|
verify(controller, times(1)).onDialogPositiveClick(any());
|
|
verify(dialogActivity, times(1)).dismiss();
|
|
}
|
|
|
|
@Test
|
|
public void pairingDialogDismissedOnNegativeClick() {
|
|
// set the dialog variant to confirmation/consent
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.CONFIRMATION_DIALOG);
|
|
|
|
// we don't care what this does, just that it is called
|
|
doNothing().when(controller).onDialogNegativeClick(any());
|
|
|
|
// build the fragment
|
|
BluetoothPairingDialogFragment frag = makeFragment();
|
|
|
|
// click the button and verify that the controller hook was called
|
|
frag.onClick(frag.getmDialog(), AlertDialog.BUTTON_NEGATIVE);
|
|
|
|
verify(controller, times(1)).onDialogNegativeClick(any());
|
|
verify(dialogActivity, times(1)).dismiss();
|
|
}
|
|
|
|
@Test
|
|
public void rotateDialog_nullPinText_okButtonEnabled() {
|
|
userEntryDialogExistingTextTest(null);
|
|
}
|
|
|
|
@Test
|
|
public void rotateDialog_emptyPinText_okButtonEnabled() {
|
|
userEntryDialogExistingTextTest("");
|
|
}
|
|
|
|
@Test
|
|
public void rotateDialog_nonEmptyPinText_okButtonEnabled() {
|
|
userEntryDialogExistingTextTest("test");
|
|
}
|
|
|
|
// Runs a test simulating the user entry dialog type in a situation like device rotation, where
|
|
// the dialog fragment gets created and we already have some existing text entered into the
|
|
// pin field.
|
|
private void userEntryDialogExistingTextTest(CharSequence existingText) {
|
|
when(controller.getDialogType()).thenReturn(BluetoothPairingController.USER_ENTRY_DIALOG);
|
|
when(controller.getDeviceVariantMessageHintId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
when(controller.getDeviceVariantMessageId())
|
|
.thenReturn(BluetoothPairingController.INVALID_DIALOG_TYPE);
|
|
|
|
BluetoothPairingDialogFragment fragment = spy(new BluetoothPairingDialogFragment());
|
|
when(fragment.getPairingViewText()).thenReturn(existingText);
|
|
setupFragment(fragment);
|
|
AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
|
|
boolean expected = !TextUtils.isEmpty(existingText);
|
|
assertThat(dialog.getButton(Dialog.BUTTON_POSITIVE).isEnabled()).isEqualTo(expected);
|
|
}
|
|
|
|
private void setupFragment(BluetoothPairingDialogFragment frag) {
|
|
assertThat(frag.isPairingControllerSet()).isFalse();
|
|
frag.setPairingController(controller);
|
|
assertThat(frag.isPairingDialogActivitySet()).isFalse();
|
|
frag.setPairingDialogActivity(dialogActivity);
|
|
FragmentTestUtil.startFragment(frag);
|
|
assertThat(frag.getmDialog()).isNotNull();
|
|
assertThat(frag.isPairingControllerSet()).isTrue();
|
|
assertThat(frag.isPairingDialogActivitySet()).isTrue();
|
|
}
|
|
|
|
private BluetoothPairingDialogFragment makeFragment() {
|
|
BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
|
|
setupFragment(frag);
|
|
return frag;
|
|
}
|
|
}
|