Merge "Robotests for BluetoothPairingDialog"
diff --git a/src/com/android/settings/bluetooth/BluetoothPairingController.java b/src/com/android/settings/bluetooth/BluetoothPairingController.java
index 38b30a8..ce82612 100644
--- a/src/com/android/settings/bluetooth/BluetoothPairingController.java
+++ b/src/com/android/settings/bluetooth/BluetoothPairingController.java
@@ -188,7 +188,7 @@
*
* @return - The message ID to show the user.
*/
- public int getDeviceVariantMessageID() {
+ public int getDeviceVariantMessageId() {
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
case BluetoothDevice.PAIRING_VARIANT_PIN:
@@ -198,7 +198,7 @@
return R.string.bluetooth_enter_passkey_other_device;
default:
- return -1;
+ return INVALID_DIALOG_TYPE;
}
}
@@ -208,7 +208,7 @@
*
* @return - The message ID to show the user.
*/
- public int getDeviceVariantMessageHint() {
+ public int getDeviceVariantMessageHintId() {
switch (mType) {
case BluetoothDevice.PAIRING_VARIANT_PIN_16_DIGITS:
return R.string.bluetooth_pin_values_hint_16_digits;
@@ -218,7 +218,7 @@
return R.string.bluetooth_pin_values_hint;
default:
- return -1;
+ return INVALID_DIALOG_TYPE;
}
}
diff --git a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
index d4247c0..abeb862 100644
--- a/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
+++ b/src/com/android/settings/bluetooth/BluetoothPairingDialogFragment.java
@@ -45,10 +45,9 @@
private static final String TAG = "BTPairingDialogFragment";
private AlertDialog.Builder mBuilder;
- private BluetoothPairingController mPairingController;
private AlertDialog mDialog;
+ private BluetoothPairingController mPairingController;
private EditText mPairingView;
-
/**
* The interface we expect a listener to implement. Typically this should be done by
* the controller.
@@ -106,11 +105,25 @@
}
/**
+ * Used in testing to get a reference to the dialog.
+ * @return - The fragments current dialog
+ */
+ protected AlertDialog getmDialog() {
+ return mDialog;
+ }
+
+ /**
* Sets the controller that the fragment should use. this method MUST be called
* before you try to show the dialog or an error will be thrown. An implementation
- * of a pairing controller can be found at {@link BluetoothPairingController}.
+ * of a pairing controller can be found at {@link BluetoothPairingController}. A
+ * controller may not be substituted once it is assigned. Forcibly switching a
+ * controller for a new one will lead to undefined behavior.
*/
public void setPairingController(BluetoothPairingController pairingController) {
+ if (mPairingController != null) {
+ throw new IllegalStateException("The controller can only be set once. "
+ + "Forcibly replacing it will lead to undefined behavior");
+ }
mPairingController = pairingController;
}
@@ -146,7 +159,7 @@
mBuilder.setPositiveButton(getString(android.R.string.ok), this);
mBuilder.setNegativeButton(getString(android.R.string.cancel), this);
AlertDialog dialog = mBuilder.create();
- dialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false);
+ dialog.setOnShowListener(d -> mDialog.getButton(Dialog.BUTTON_POSITIVE).setEnabled(false));
return dialog;
}
@@ -171,6 +184,7 @@
mPairingView = pairingView;
+ pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
pairingView.addTextChangedListener(this);
alphanumericPin.setOnCheckedChangeListener((buttonView, isChecked) -> {
// change input type for soft keyboard to numeric or alphanumeric
@@ -181,15 +195,21 @@
}
});
- int messageId = mPairingController.getDeviceVariantMessageID();
- int messageIdHint = mPairingController.getDeviceVariantMessageHint();
+ int messageId = mPairingController.getDeviceVariantMessageId();
+ int messageIdHint = mPairingController.getDeviceVariantMessageHintId();
int maxLength = mPairingController.getDeviceMaxPasskeyLength();
alphanumericPin.setVisibility(mPairingController.pairingCodeIsAlphanumeric()
? View.VISIBLE : View.GONE);
-
- messageViewCaptionHint.setText(messageIdHint);
- messageView2.setText(messageId);
- pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
+ if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
+ messageView2.setText(messageId);
+ } else {
+ messageView2.setVisibility(View.GONE);
+ }
+ if (messageIdHint != BluetoothPairingController.INVALID_DIALOG_TYPE) {
+ messageViewCaptionHint.setText(messageIdHint);
+ } else {
+ messageViewCaptionHint.setVisibility(View.GONE);
+ }
pairingView.setFilters(new InputFilter[]{
new LengthFilter(maxLength)});
@@ -203,10 +223,8 @@
mBuilder.setTitle(getString(R.string.bluetooth_pairing_request,
mPairingController.getDeviceName()));
mBuilder.setView(createView());
- mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept),
- this);
- mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline),
- this);
+ mBuilder.setPositiveButton(getString(R.string.bluetooth_pairing_accept), this);
+ mBuilder.setNegativeButton(getString(R.string.bluetooth_pairing_decline), this);
AlertDialog dialog = mBuilder.create();
return dialog;
}
diff --git a/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java
new file mode 100644
index 0000000..e156716
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java
@@ -0,0 +1,76 @@
+/*
+ * 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;
+
+import org.junit.runners.model.InitializationError;
+import org.robolectric.RobolectricTestRunner;
+import org.robolectric.annotation.Config;
+import org.robolectric.manifest.AndroidManifest;
+import org.robolectric.res.Fs;
+import org.robolectric.res.ResourcePath;
+
+import java.util.List;
+
+/**
+ * Custom test runner for the testing of BluetoothPairingDialogs. This is needed because the
+ * default behavior for robolectric is just to grab the resource directory in the target package.
+ * We want to override this to add several spanning different projects.
+ */
+public class SettingsRobolectricTestRunner extends RobolectricTestRunner {
+
+ /**
+ * We don't actually want to change this behavior, so we just call super.
+ */
+ public SettingsRobolectricTestRunner(Class<?> testClass) throws InitializationError {
+ super(testClass);
+ }
+
+ /**
+ * We are going to create our own custom manifest so that we can add multiple resource
+ * paths to it. This lets us access resources in both Settings and SettingsLib in our tests.
+ */
+ @Override
+ protected AndroidManifest getAppManifest(Config config) {
+ // Using the manifest file's relative path, we can figure out the application directory.
+ final String appRoot = "packages/apps/Settings";
+ final String manifestPath = appRoot + "/AndroidManifest.xml";
+ final String resDir = appRoot + "/res";
+ final String assetsDir = appRoot + "/assets";
+
+ // By adding any resources from libraries we need to the AndroidManifest, we can access
+ // them from within the parallel universe's resource loader.
+ final AndroidManifest manifest = new AndroidManifest(Fs.fileFromPath(manifestPath),
+ Fs.fileFromPath(resDir), Fs.fileFromPath(assetsDir)) {
+ @Override
+ public List<ResourcePath> getIncludedResourcePaths() {
+ List<ResourcePath> paths = super.getIncludedResourcePaths();
+ paths.add(new ResourcePath(
+ getPackageName(),
+ Fs.fileFromPath("./packages/apps/Settings/res"),
+ null));
+ paths.add(new ResourcePath(
+ getPackageName(),
+ Fs.fileFromPath("./frameworks/base/packages/SettingsLib/res"),
+ null));
+ return paths;
+ }
+ };
+
+ // Set the package name to the renamed one
+ manifest.setPackageName("com.android.settings");
+ return manifest;
+ }
+}
\ No newline at end of file
diff --git a/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDialogTest.java b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDialogTest.java
new file mode 100644
index 0000000..004e4f9
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/bluetooth/BluetoothPairingDialogTest.java
@@ -0,0 +1,340 @@
+/*
+ * 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.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.app.AlertDialog;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.CheckBox;
+import android.widget.TextView;
+import com.android.settings.SettingsRobolectricTestRunner;
+import com.android.settings.R;
+import com.android.settings.TestConfig;
+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.util.FragmentTestUtil;
+
+@RunWith(SettingsRobolectricTestRunner.class)
+@Config(manifest = TestConfig.MANIFEST_PATH, sdk = TestConfig.SDK_VERSION)
+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;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ }
+
+ @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);
+
+ // build the fragment
+ BluetoothPairingDialogFragment frag = makeFragment();
+
+ // check that the pin/passkey input field is visible to the user
+ View view = frag.getmDialog().findViewById(R.id.text);
+ assertThat(view.getVisibility()).isEqualTo(View.VISIBLE);
+ }
+
+ @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
+ 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);
+ }
+
+ private BluetoothPairingDialogFragment makeFragment() {
+ BluetoothPairingDialogFragment frag = new BluetoothPairingDialogFragment();
+ frag.setPairingController(controller);
+ FragmentTestUtil.startFragment(frag);
+ assertThat(frag.getmDialog()).isNotNull();
+ return frag;
+ }
+}