From bccd65250316ad6f6e65cc9a0cc84235a5c44d86 Mon Sep 17 00:00:00 2001 From: Jaewoong Jung Date: Wed, 30 Nov 2016 15:45:53 -0800 Subject: [PATCH] Makes it possible to robo-test Settings app fragments. This adds bunch of shadow/placeholder classes and logic to handle references to Android internal resources or newly added classes/methods that Robolectric hasn't yet picked up. Developers can follow ManageApplicationsTest example to use the shadow classes and the utility method to start ther fragment in their robolectric tests. Bug: 33431346 Test: This is a test improvement CL. RunSettingsRoboTests still passes. Change-Id: I943ab871631cb8c368d9f9db481c00558c5c4d1f --- .../grandfather_not_implementing_indexable | 3 +- .../android/print/PrintServicesLoader.java | 19 +++ .../internal/app/LocalePickerWithRegion.java | 11 ++ .../SettingsRobolectricTestRunner.java | 21 ++- .../applications/ManageApplicationsTest.java | 61 +++++++++ .../shadow/SettingsShadowResources.java | 126 ++++++++++++++++++ .../ShadowDynamicIndexableContentMonitor.java | 22 +++ 7 files changed, 261 insertions(+), 2 deletions(-) create mode 100644 tests/robotests/src/android/print/PrintServicesLoader.java create mode 100644 tests/robotests/src/com/android/internal/app/LocalePickerWithRegion.java create mode 100644 tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowResources.java create mode 100644 tests/robotests/src/com/android/settings/testutils/shadow/ShadowDynamicIndexableContentMonitor.java diff --git a/tests/robotests/assets/grandfather_not_implementing_indexable b/tests/robotests/assets/grandfather_not_implementing_indexable index c1b8cf52b48..0c539d813d4 100644 --- a/tests/robotests/assets/grandfather_not_implementing_indexable +++ b/tests/robotests/assets/grandfather_not_implementing_indexable @@ -87,4 +87,5 @@ com.android.settings.applications.AppStorageSettings com.android.settings.notification.NotificationAccessSettings com.android.settings.notification.ZenModeSettings com.android.settings.accessibility.ToggleDaltonizerPreferenceFragment -com.android.settings.applications.ConvertToFbe \ No newline at end of file +com.android.settings.applications.ConvertToFbe +com.android.settings.localepicker.LocaleListEditor \ No newline at end of file diff --git a/tests/robotests/src/android/print/PrintServicesLoader.java b/tests/robotests/src/android/print/PrintServicesLoader.java new file mode 100644 index 00000000000..e4975edb813 --- /dev/null +++ b/tests/robotests/src/android/print/PrintServicesLoader.java @@ -0,0 +1,19 @@ +package android.print; + +import android.annotation.NonNull; +import android.content.Context; +import android.content.Loader; +import android.printservice.PrintServiceInfo; +import com.android.internal.util.Preconditions; + +import java.util.List; + +/** + * A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility. + */ +public class PrintServicesLoader extends Loader> { + public PrintServicesLoader(@NonNull PrintManager printManager, @NonNull Context context, + int selectionFlags) { + super(Preconditions.checkNotNull(context)); + } +} diff --git a/tests/robotests/src/com/android/internal/app/LocalePickerWithRegion.java b/tests/robotests/src/com/android/internal/app/LocalePickerWithRegion.java new file mode 100644 index 00000000000..9edda45cfe7 --- /dev/null +++ b/tests/robotests/src/com/android/internal/app/LocalePickerWithRegion.java @@ -0,0 +1,11 @@ +package com.android.internal.app; + +/** + * A placeholder class to prevent ClassNotFound exceptions caused by lack of visibility. + */ +public class LocalePickerWithRegion { + + public interface LocaleSelectedListener { + void onLocaleSelected(LocaleStore.LocaleInfo locale); + } +} diff --git a/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java index 9127d5f94a4..4472025f676 100644 --- a/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java +++ b/tests/robotests/src/com/android/settings/SettingsRobolectricTestRunner.java @@ -15,15 +15,23 @@ */ package com.android.settings; +import android.app.Activity; +import android.app.Fragment; +import android.content.Intent; 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 org.robolectric.util.ActivityController; +import org.robolectric.util.ReflectionHelpers; import java.util.List; +import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT; +import static org.robolectric.Robolectric.getShadowsAdapter; + /** * 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. @@ -77,4 +85,15 @@ public class SettingsRobolectricTestRunner extends RobolectricTestRunner { manifest.setPackageName("com.android.settings"); return manifest; } -} \ No newline at end of file + + // A simple utility class to start a Settings fragment with an intent. The code here is almost + // the same as FragmentTestUtil.startFragment except that it starts an activity with an intent. + public static void startSettingsFragment( + Fragment fragment, Class activityClass) { + Intent intent = new Intent().putExtra(EXTRA_SHOW_FRAGMENT, fragment.getClass().getName()); + SettingsActivity activity = ActivityController.of( + getShadowsAdapter(), ReflectionHelpers.callConstructor(activityClass), intent) + .setup().get(); + activity.getFragmentManager().beginTransaction().add(fragment, null).commit(); + } +} diff --git a/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java new file mode 100644 index 00000000000..e49b8b8e905 --- /dev/null +++ b/tests/robotests/src/com/android/settings/applications/ManageApplicationsTest.java @@ -0,0 +1,61 @@ +package com.android.settings.applications; + +import android.os.Looper; +import android.os.UserManager; +import com.android.settings.Settings; +import com.android.settings.SettingsRobolectricTestRunner; +import com.android.settings.TestConfig; +import com.android.settings.testutils.shadow.ShadowDynamicIndexableContentMonitor; +import com.android.settings.testutils.shadow.SettingsShadowResources; +import com.android.settings.testutils.shadow.SettingsShadowResources.SettingsShadowTheme; +import com.android.settingslib.applications.ApplicationsState; +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.ReflectionHelpers; + +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ManageApplications}. + */ +@RunWith(SettingsRobolectricTestRunner.class) +// TODO: Consider making the shadow class set global using a robolectric.properties file. +@Config(manifest = TestConfig.MANIFEST_PATH, + sdk = TestConfig.SDK_VERSION, + shadows = { + SettingsShadowResources.class, + SettingsShadowTheme.class, + ShadowDynamicIndexableContentMonitor.class + }) +public class ManageApplicationsTest { + + @Mock private ApplicationsState mState; + @Mock private ApplicationsState.Session mSession; + @Mock private UserManager mUserManager; + + private Looper mBgLooper; + + private ManageApplications mFragment; + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + + ReflectionHelpers.setStaticField(ApplicationsState.class, "sInstance", mState); + when(mState.newSession(any())).thenReturn(mSession); + mBgLooper = Looper.myLooper(); + when(mState.getBackgroundLooper()).thenReturn(mBgLooper); + + mFragment = new ManageApplications(); + } + + @Test + public void launchFragment() { + SettingsRobolectricTestRunner.startSettingsFragment( + mFragment, Settings.ManageApplicationsActivity.class); + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowResources.java b/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowResources.java new file mode 100644 index 00000000000..51a187b9d7d --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/SettingsShadowResources.java @@ -0,0 +1,126 @@ +package com.android.settings.testutils.shadow; + +import android.annotation.DimenRes; +import android.content.res.Resources; +import android.content.res.Resources.NotFoundException; +import android.content.res.Resources.Theme; +import android.content.res.TypedArray; +import android.graphics.drawable.ColorDrawable; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; +import android.util.TypedValue; +import org.robolectric.RuntimeEnvironment; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; +import org.robolectric.res.StyleData; +import org.robolectric.res.StyleResolver; +import org.robolectric.res.builder.XmlResourceParserImpl; +import org.robolectric.shadows.ShadowAssetManager; +import org.robolectric.shadows.ShadowResources; +import org.robolectric.util.ReflectionHelpers; +import org.w3c.dom.Node; + +import java.util.List; +import java.util.Map; + +import static android.util.TypedValue.TYPE_REFERENCE; +import static org.robolectric.Shadows.shadowOf; +import static org.robolectric.internal.Shadow.directlyOn; + +/** + * Shadow Resources and Theme classes to handle resource references that Robolectric shadows cannot + * handle because they are too new or private. + */ +@Implements(Resources.class) +public class SettingsShadowResources extends ShadowResources { + + @RealObject Resources realResources; + + @Implementation + public int getDimensionPixelSize(@DimenRes int id) throws NotFoundException { + // Handle requests for private dimension resources, + // TODO: Consider making a set of private dimension resource ids if this happens repeatedly. + if (id == com.android.internal.R.dimen.preference_fragment_padding_bottom) { + return 0; + } + return directlyOn(realResources, Resources.class).getDimensionPixelSize(id); + } + + @Implementation + public Drawable loadDrawable(TypedValue value, int id, Theme theme) + throws NotFoundException { + // The drawable item in switchbar_background.xml refers to a very recent color attribute + // that Robolectric isn't yet aware of. + // TODO: Remove this once Robolectric is updated. + if (id == com.android.settings.R.drawable.switchbar_background) { + return new ColorDrawable(); + } + return super.loadDrawable(value, id, theme); + } + + @Implements(Theme.class) + public static class SettingsShadowTheme extends ShadowTheme { + + @RealObject + Theme realTheme; + + @Implementation + public TypedArray obtainStyledAttributes( + AttributeSet set, int[] attrs, int defStyleAttr, int defStyleRes) { + // Replace all private string references with a placeholder. + if (set != null) { + for (int i = 0; i < set.getAttributeCount(); ++i) { + if (set.getAttributeValue(i).startsWith("@*android:string")) { + Node node = ReflectionHelpers.callInstanceMethod( + XmlResourceParserImpl.class, set, "getAttributeAt", + ReflectionHelpers.ClassParameter.from(int.class, i)); + node.setNodeValue("PLACEHOLDER"); + } + } + } + + // Track down all styles and remove all inheritance from private styles. + ShadowAssetManager assetManager = shadowOf(RuntimeEnvironment.application.getAssets()); + // The Object's below are actually ShadowAssetManager.OverlayedStyle. We can't use it + // here because it's package private. + Map> appliedStylesList = + ReflectionHelpers.getField(assetManager, "appliedStyles"); + for (Long idx : appliedStylesList.keySet()) { + List appliedStyles = appliedStylesList.get(idx); + int i = 1; + for (Object appliedStyle : appliedStyles) { + StyleResolver styleResolver = ReflectionHelpers.getField(appliedStyle, "style"); + List styleDatas = + ReflectionHelpers.getField(styleResolver, "styles"); + for (StyleData styleData : styleDatas) { + if (styleData.getParent() != null && + styleData.getParent().startsWith("@*android:style")) { + ReflectionHelpers.setField(StyleData.class, styleData, "parent", null); + } + } + } + + } + return super.obtainStyledAttributes(set, attrs, defStyleAttr, defStyleRes); + } + + @Implementation + public boolean resolveAttribute(int resid, TypedValue outValue, boolean resolveRefs) { + // The real Resources instance in Robolectric tests somehow fails to find the + // preferenceTheme attribute in the layout. Let's do it ourselves. + if (getResources().getResourceName(resid) + .equals("com.android.settings:attr/preferenceTheme")) { + int preferenceThemeResId = + getResources().getIdentifier( + "PreferenceTheme", "style", "com.android.settings"); + outValue.type = TYPE_REFERENCE; + outValue.data = preferenceThemeResId; + outValue.resourceId = preferenceThemeResId; + return true; + } + return directlyOn(realTheme, Theme.class) + .resolveAttribute(resid, outValue, resolveRefs); + } + } +} diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDynamicIndexableContentMonitor.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDynamicIndexableContentMonitor.java new file mode 100644 index 00000000000..de5d243624f --- /dev/null +++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowDynamicIndexableContentMonitor.java @@ -0,0 +1,22 @@ +package com.android.settings.testutils.shadow; + +import android.app.Activity; +import android.os.UserManager; +import com.android.settings.search.DynamicIndexableContentMonitor; +import org.robolectric.annotation.Implementation; +import org.robolectric.annotation.Implements; +import org.robolectric.annotation.RealObject; + +/** + * A shadow class of {@link DynamicIndexableContentMonitor}. The real implementation of + * {@link DynamicIndexableContentMonitor#register} calls {@link UserManager#isUserUnlocked()}, which + * Robolectric has not yet been updated to support, so throws a NoSuchMethodError exception. + */ +// TODO: Delete this once Robolectric is updated to the latest SDK. +@Implements(DynamicIndexableContentMonitor.class) +public class ShadowDynamicIndexableContentMonitor { + + @Implementation + public void register(Activity activity, int loaderId) { + } +}