Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f492c28c authored by Salvador Martinez's avatar Salvador Martinez
Browse files

Robotests for BluetoothPairingDialog

Created some tests to protect some basic bluetooth
pairing dialogs features from regressing. Most of the
tests in this CL ensure that the view is properly
created and that it is properly updating the
associated controller when a relevant action occurs.

Test: make RunSettingsRoboTests
Bug: 32180625
Change-Id: I2f4103a39ffced52353712f952e8ff3d26590169
parent 78c4f30a
Loading
Loading
Loading
Loading
+4 −4
Original line number Diff line number Diff line
@@ -188,7 +188,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
     *
     * @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 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
                return R.string.bluetooth_enter_passkey_other_device;

            default:
                return -1;
                return INVALID_DIALOG_TYPE;
        }
    }

@@ -208,7 +208,7 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
     *
     * @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 @@ public class BluetoothPairingController implements OnCheckedChangeListener,
                return R.string.bluetooth_pin_values_hint;

            default:
                return -1;
                return INVALID_DIALOG_TYPE;
        }
    }

+32 −14
Original line number Diff line number Diff line
@@ -45,10 +45,9 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
    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.
@@ -105,12 +104,26 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
        return MetricsEvent.BLUETOOTH_DIALOG_FRAGMENT;
    }

    /**
     * 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 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
        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 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i

        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 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
            }
        });

        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);
        if (messageId != BluetoothPairingController.INVALID_DIALOG_TYPE) {
            messageView2.setText(messageId);
        pairingView.setInputType(InputType.TYPE_CLASS_NUMBER);
        } 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 @@ public class BluetoothPairingDialogFragment extends InstrumentedDialogFragment i
        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;
    }
+76 −0
Original line number Diff line number Diff line
/*
 * 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
+340 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}