Enable Activity Launch Avoidance
Adds utilities to allow one-per-class sharing of an Activity,
as opposed to the current one-per-test.
Test: ./gradlew internal-testutils:cC
Change-Id: I668924efd581703606b249b0a2532c3cd71ad503
diff --git a/recyclerview/recyclerview/build.gradle b/recyclerview/recyclerview/build.gradle
index 431d8ad9..f15f3b9 100644
--- a/recyclerview/recyclerview/build.gradle
+++ b/recyclerview/recyclerview/build.gradle
@@ -44,6 +44,10 @@
buildTypes.all {
consumerProguardFiles("proguard-rules.pro")
}
+
+ defaultConfig {
+ testInstrumentationRunner "androidx.testutils.ActivityRecyclingAndroidJUnitRunner"
+ }
}
androidx {
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
index 0ed5a9f..0a57c56 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/BaseRecyclerViewInstrumentationTest.java
@@ -45,13 +45,15 @@
import androidx.core.view.ViewCompat;
import androidx.recyclerview.test.R;
import androidx.test.platform.app.InstrumentationRegistry;
-import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.ActivityScenarioResetRule;
import androidx.testutils.PollingCheck;
+import androidx.testutils.ResettableActivityScenarioRule;
import org.hamcrest.CoreMatchers;
import org.hamcrest.MatcherAssert;
import org.junit.After;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Rule;
import java.lang.reflect.InvocationTargetException;
@@ -81,9 +83,13 @@
Thread mInstrumentationThread;
+ // One activity launch per test class
+ @ClassRule
+ public static ResettableActivityScenarioRule<TestActivity> mActivityRule =
+ new ResettableActivityScenarioRule<>(TestActivity.class);
@Rule
- public ActivityTestRule<TestActivity> mActivityRule =
- new ActivityTestRule<>(TestActivity.class);
+ public ActivityScenarioResetRule<TestActivity> mActivityResetRule =
+ new TestActivity.ResetRule(mActivityRule.getScenario());
public BaseRecyclerViewInstrumentationTest() {
this(false);
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingA11yScrollTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingA11yScrollTest.java
index 3b24175..0cf7769 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingA11yScrollTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingA11yScrollTest.java
@@ -41,9 +41,11 @@
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
import androidx.test.filters.LargeTest;
import androidx.test.filters.SdkSuppress;
-import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.ActivityScenarioResetRule;
+import androidx.testutils.ResettableActivityScenarioRule;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -64,9 +66,14 @@
@SdkSuppress(minSdkVersion = 16)
public class RecyclerViewNestedScrollingA11yScrollTest {
+
+ @ClassRule
+ public static ResettableActivityScenarioRule<TestActivity> mActivityRule =
+ new ResettableActivityScenarioRule<>(TestActivity.class);
+
@Rule
- public final ActivityTestRule<TestActivity> mActivityTestRule =
- new ActivityTestRule<>(TestActivity.class);
+ public ActivityScenarioResetRule<TestActivity> mActivityResetRule =
+ new TestActivity.ResetRule(mActivityRule.getScenario());
@Parameterized.Parameters(name = "orientationVertical:{0}, scrollForwards:{1}")
public static Collection<Object[]> getParams() {
@@ -105,7 +112,7 @@
@Before
public void setup() throws Throwable {
- Context context = mActivityTestRule.getActivity();
+ Context context = mActivityRule.getActivity();
// Create view hierarchy.
mRecyclerView = new RecyclerView(context);
@@ -129,9 +136,9 @@
// Attach view hierarchy to activity and wait for first layout.
final TestedFrameLayout testContentView =
- mActivityTestRule.getActivity().getContainer();
+ mActivityRule.getActivity().getContainer();
testContentView.expectLayouts(1);
- mActivityTestRule.runOnUiThread(new Runnable() {
+ mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
testContentView.addView(mParent);
@@ -147,7 +154,7 @@
final CountDownLatch countDownLatch = new CountDownLatch(2);
final int[] totalScrolled = new int[1];
- mActivityTestRule.runOnUiThread(new Runnable() {
+ mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingFlingTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingFlingTest.kt
index fc0c0c2..c85bf36 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingFlingTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingFlingTest.kt
@@ -26,10 +26,12 @@
import androidx.core.view.NestedScrollingParent3
import androidx.test.core.app.ApplicationProvider
import androidx.test.filters.LargeTest
-import androidx.test.rule.ActivityTestRule
+import androidx.testutils.ActivityScenarioResetRule
+import androidx.testutils.ResettableActivityScenarioRule
import org.hamcrest.MatcherAssert.assertThat
import org.hamcrest.Matchers.closeTo
import org.junit.Before
+import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -56,7 +58,8 @@
@Rule
@JvmField
- var mActivityRule = ActivityTestRule(TestActivity::class.java)
+ val mActivityResetRule: ActivityScenarioResetRule<TestActivity> = TestActivity.ResetRule(
+ mActivityRule.scenario)
@Before
@Throws(Throwable::class)
@@ -77,9 +80,11 @@
addView(mRecyclerView)
}
- val testedFrameLayout = mActivityRule.activity.container
+ val testedFrameLayout = mActivityRule.getActivity().container
testedFrameLayout.expectLayouts(1)
- mActivityRule.runOnUiThread { testedFrameLayout.addView(mNestedScrollingParent) }
+ mActivityRule.runOnUiThread {
+ testedFrameLayout.addView(mNestedScrollingParent)
+ }
testedFrameLayout.waitForLayout(2)
}
@@ -146,10 +151,10 @@
val up = MotionEvent.obtain(0, elapsedTime, MotionEvent.ACTION_UP, x3, y3, 0)
mActivityRule.runOnUiThread {
- mNestedScrollingParent.dispatchTouchEvent(down)
- mNestedScrollingParent.dispatchTouchEvent(move1)
- mNestedScrollingParent.dispatchTouchEvent(move2)
- mNestedScrollingParent.dispatchTouchEvent(up)
+ mNestedScrollingParent.dispatchTouchEvent(down)
+ mNestedScrollingParent.dispatchTouchEvent(move1)
+ mNestedScrollingParent.dispatchTouchEvent(move2)
+ mNestedScrollingParent.dispatchTouchEvent(up)
}
val (expected, errorRange) =
@@ -412,6 +417,10 @@
companion object {
+ @ClassRule
+ @JvmField
+ val mActivityRule = ResettableActivityScenarioRule<TestActivity>()
+
@JvmStatic
@Parameterized.Parameters(
name = "orientationVertical:{0}, " +
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingSmoothScrollByTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingSmoothScrollByTest.java
index b8b738a..c8000e3 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingSmoothScrollByTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewNestedScrollingSmoothScrollByTest.java
@@ -36,9 +36,11 @@
import androidx.annotation.Nullable;
import androidx.core.view.NestedScrollingParent3;
import androidx.test.filters.LargeTest;
-import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.ActivityScenarioResetRule;
+import androidx.testutils.ResettableActivityScenarioRule;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -59,9 +61,13 @@
@LargeTest
public class RecyclerViewNestedScrollingSmoothScrollByTest {
+ @ClassRule
+ public static ResettableActivityScenarioRule<TestActivity> mActivityRule =
+ new ResettableActivityScenarioRule<>(TestActivity.class);
+
@Rule
- public final ActivityTestRule<TestActivity> mActivityTestRule =
- new ActivityTestRule<>(TestActivity.class);
+ public ActivityScenarioResetRule<TestActivity> mActivityResetRule =
+ new TestActivity.ResetRule(mActivityRule.getScenario());
private enum SmoothScrollType {
TWO_PARAMS, THREE_PARAMS, FOUR_PARAMS
@@ -109,7 +115,7 @@
@Before
public void setup() throws Throwable {
- Context context = mActivityTestRule.getActivity();
+ Context context = mActivityRule.getActivity();
// Create view hierarchy.
mRecyclerView = new RecyclerView(context);
@@ -133,9 +139,9 @@
// Attach view hierarchy to activity and wait for first layout.
final TestedFrameLayout testContentView =
- mActivityTestRule.getActivity().getContainer();
+ mActivityRule.getActivity().getContainer();
testContentView.expectLayouts(1);
- mActivityTestRule.runOnUiThread(new Runnable() {
+ mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
testContentView.addView(mParent);
@@ -151,7 +157,7 @@
final CountDownLatch countDownLatch = new CountDownLatch(1);
final int[] totalScrolled = new int[1];
- mActivityTestRule.runOnUiThread(new Runnable() {
+ mActivityRule.runOnUiThread(new Runnable() {
@Override
public void run() {
doReturn(true).when(mParent).onStartNestedScroll(any(View.class), any(View.class),
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewWithTransitionsTest.kt b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewWithTransitionsTest.kt
index 1035cd3..bdc75b9 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewWithTransitionsTest.kt
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/RecyclerViewWithTransitionsTest.kt
@@ -25,10 +25,12 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
-import androidx.test.rule.ActivityTestRule
+import androidx.testutils.ActivityScenarioResetRule
+import androidx.testutils.ResettableActivityScenarioRule
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
+import org.junit.ClassRule
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -37,10 +39,12 @@
@SmallTest
class RecyclerViewWithTransitionsTest {
+ val activity: TestActivity get() = mActivityRule.getActivity()
+
@Rule
@JvmField
- val activityRule = ActivityTestRule(TestActivity::class.java)
- val activity: TestActivity get() = (activityRule.activity as TestActivity)
+ val mActivityResetRule: ActivityScenarioResetRule<TestActivity> =
+ TestActivity.ResetRule(mActivityRule.scenario)
@Test
fun ignoreCachedViewWhileItIsAttachedToOverlay() {
@@ -49,13 +53,13 @@
layoutManager = LinearLayoutManager(context)
adapter = testAdapter
}
- activityRule.runOnUiThread {
+ activity.runOnUiThread {
activity.container.addView(recyclerView)
}
// helper fun to change itemCount, wait for it to be applied and validate childCount
val changeItemCount = { itemsCount: Int ->
- activityRule.runOnUiThread {
+ activity.runOnUiThread {
testAdapter.itemCount = itemsCount
testAdapter.notifyDataSetChanged()
}
@@ -127,4 +131,10 @@
override fun getItemCount() = itemCount
}
+
+ companion object {
+ @ClassRule
+ @JvmField
+ val mActivityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+ }
}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TestActivity.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TestActivity.java
index 74b3fa6..7bbc529 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TestActivity.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/TestActivity.java
@@ -20,22 +20,31 @@
import android.os.Bundle;
import android.view.WindowManager;
-public class TestActivity extends Activity {
+import androidx.test.core.app.ActivityScenario;
+import androidx.testutils.ActivityScenarioResetRule;
+import androidx.testutils.Resettable;
+
+import kotlin.Unit;
+import kotlin.jvm.functions.Function1;
+
+public class TestActivity extends Activity implements Resettable {
private volatile TestedFrameLayout mContainer;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- overridePendingTransition(0, 0);
+ reset();
+ // disable enter animation.
+ overridePendingTransition(0, 0);
+ }
+
+ public void reset() {
mContainer = new TestedFrameLayout(this);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD);
setContentView(mContainer);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
- // disable enter animation.
- overridePendingTransition(0, 0);
}
public TestedFrameLayout getContainer() {
@@ -44,9 +53,31 @@
@Override
public void finish() {
+ if (!mFinishEnabled) {
+ return;
+ }
super.finish();
// disable exit animation.
overridePendingTransition(0, 0);
}
+
+ private boolean mFinishEnabled;
+
+ @Override
+ public void setFinishEnabled(boolean finishEnabled) {
+ mFinishEnabled = finishEnabled;
+ }
+
+ static class ResetRule extends ActivityScenarioResetRule<TestActivity> {
+ ResetRule(ActivityScenario<TestActivity> scenario) {
+ super(scenario, new Function1<TestActivity, Unit>() {
+ @Override
+ public Unit invoke(TestActivity activity) {
+ activity.reset();
+ return null;
+ }
+ });
+ }
+ }
}
diff --git a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ThreadUtilTest.java b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ThreadUtilTest.java
index 68be966..fdb1ce8a 100644
--- a/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ThreadUtilTest.java
+++ b/recyclerview/recyclerview/src/androidTest/java/androidx/recyclerview/widget/ThreadUtilTest.java
@@ -26,9 +26,11 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.MediumTest;
-import androidx.test.rule.ActivityTestRule;
+import androidx.testutils.ActivityScenarioResetRule;
+import androidx.testutils.ResettableActivityScenarioRule;
import org.junit.Before;
+import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -41,9 +43,12 @@
@RunWith(AndroidJUnit4.class)
@MediumTest
public class ThreadUtilTest {
+ @ClassRule
+ public static ResettableActivityScenarioRule<TestActivity> mActivityRule =
+ new ResettableActivityScenarioRule<>(TestActivity.class);
@Rule
- public ActivityTestRule<TestActivity> mActivityRule =
- new ActivityTestRule<>(TestActivity.class);
+ public ActivityScenarioResetRule<TestActivity> mActivityResetRule =
+ new TestActivity.ResetRule(mActivityRule.getScenario());
Map<String, LockedObject> results = new HashMap<>();
diff --git a/testutils/testutils-runtime/build.gradle b/testutils/testutils-runtime/build.gradle
index 6ffcef1..47ca929 100644
--- a/testutils/testutils-runtime/build.gradle
+++ b/testutils/testutils-runtime/build.gradle
@@ -35,6 +35,9 @@
lintOptions {
disable 'InvalidPackage' // Lint is unhappy about junit package
}
+ defaultConfig {
+ testInstrumentationRunner "androidx.testutils.ActivityRecyclingAndroidJUnitRunner"
+ }
}
androidx {
diff --git a/testutils/testutils-runtime/src/androidTest/AndroidManifest.xml b/testutils/testutils-runtime/src/androidTest/AndroidManifest.xml
new file mode 100644
index 0000000..2ab71cd
--- /dev/null
+++ b/testutils/testutils-runtime/src/androidTest/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+ -->
+<manifest package="androidx.testutils.test"
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
+ <application>
+ <activity
+ android:name="androidx.testutils.TestActivity"
+ android:theme="@android:style/Theme.Light.NoTitleBar.Fullscreen">
+ </activity>
+ </application>
+</manifest>
diff --git a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/ActivityScenarioRulePersist.kt b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/ActivityScenarioRulePersist.kt
new file mode 100644
index 0000000..459ffe1
--- /dev/null
+++ b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/ActivityScenarioRulePersist.kt
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2019 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 androidx.testutils
+
+import android.widget.TextView
+import androidx.test.filters.LargeTest
+import androidx.testutils.test.R
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.BeforeClass
+import org.junit.ClassRule
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.Parameterized
+
+val ITERATIONS: List<Int> = (1..10).toList()
+
+/**
+ * Per-test overhead - ~400ms
+ *
+ * Default ActivityScenarioRule / ActivityTestRule - asserts activity re-launched for each test.
+ */
+@RunWith(Parameterized::class)
+@LargeTest
+class ActivityNoPersist(val index: Int) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun spec() = ITERATIONS
+
+ @BeforeClass
+ @JvmStatic
+ fun setup() {
+ TestActivity.resumes = 1
+ }
+ }
+
+ @get:Rule
+ val activityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+
+ @Test
+ fun test() {
+ activityRule.scenario.onActivity {
+ assertTrue(TestActivity.resumes >= index) // workaround setup ordering unpredictability
+ }
+ }
+}
+
+/**
+ * Per-test overhead - ~5ms
+ *
+ * Using ActivityScenarioRule as a ClassRule - asserts activity only launched once.
+ *
+ * NOTE: Big downside is dangerous state sharing between tests...
+ */
+@RunWith(Parameterized::class)
+@LargeTest
+class ActivityPersist(@Suppress("unused") val ignored: Int) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun spec() = ITERATIONS
+
+ @BeforeClass
+ @JvmStatic
+ fun setup() {
+ TestActivity.resumes = 1
+ }
+
+ @get:ClassRule
+ @JvmStatic
+ val activityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+ }
+
+ @Test
+ fun test() {
+ activityRule.scenario.onActivity {
+ assertTrue(TestActivity.resumes <= 2) // workaround setup ordering unpredictability
+ }
+ }
+}
+
+/**
+ * Per-test overhead - ~5ms
+ *
+ * Using ActivityScenarioRule as a ClassRule - only launched once, and content view is shared...
+ */
+@RunWith(Parameterized::class)
+@LargeTest
+class ActivityPersistNoReset(val index: Int) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun spec() = ITERATIONS
+
+ @get:ClassRule
+ @JvmStatic
+ val activityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+ }
+
+ @Test
+ fun contentViewIsShared() {
+ activityRule.scenario.onActivity {
+ val text: TextView = it.findViewById(R.id.text)
+ if (index != 1) {
+ assertFalse(text.text.isBlank())
+ }
+ text.text = "$index"
+ }
+ }
+}
+
+/**
+ * Per-test overhead - ~5ms
+ *
+ * Using ActivityScenarioRule as a ClassRule - only launched once, BUT RESETTING for each test.
+ */
+@RunWith(Parameterized::class)
+@LargeTest
+class ActivityPersistWithReset(val index: Int) {
+ companion object {
+ @JvmStatic
+ @Parameterized.Parameters(name = "{0}")
+ fun spec() = ITERATIONS
+
+ @get:ClassRule
+ @JvmStatic
+ val activityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+ }
+
+ @get:Rule
+ val resetRule = ActivityScenarioResetRule(activityRule.scenario) {
+ it.setContentView(R.layout.content_view)
+ }
+
+ @Test
+ fun contentViewIsReplaced() {
+ activityRule.scenario.onActivity {
+ val text: TextView = it.findViewById(R.id.text)
+ assertTrue(text.text.isBlank())
+ text.text = "$index"
+ }
+ }
+}
\ No newline at end of file
diff --git a/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt
new file mode 100644
index 0000000..e9ab195
--- /dev/null
+++ b/testutils/testutils-runtime/src/androidTest/java/androidx/testutils/TestActivity.kt
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2019 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 androidx.testutils
+
+import android.app.Activity
+import android.os.Bundle
+import android.util.Log
+import androidx.testutils.test.R
+
+class TestActivity : Activity(), Resettable {
+ override fun setFinishEnabled(finishEnabled: Boolean) {
+ finishEnabledFlag = finishEnabled
+ }
+
+ private var finishEnabledFlag = true
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setContentView(R.layout.content_view)
+ println("onCreate")
+ overridePendingTransition(0, 0)
+ }
+
+ override fun onResume() {
+ super.onResume()
+ resumes++
+ }
+
+ override fun finish() {
+ if (!finishEnabledFlag) {
+ // sometimes we get surprise finish's...
+ Log.d("System.out", "early finish", Exception())
+ return
+ }
+
+ super.finish()
+ overridePendingTransition(0, 0)
+ }
+
+ companion object {
+ var resumes = 0
+ }
+}
\ No newline at end of file
diff --git a/testutils/testutils-runtime/src/main/java/androidx/testutils/ActivityScenarioResetRule.kt b/testutils/testutils-runtime/src/main/java/androidx/testutils/ActivityScenarioResetRule.kt
new file mode 100644
index 0000000..ac423d0
--- /dev/null
+++ b/testutils/testutils-runtime/src/main/java/androidx/testutils/ActivityScenarioResetRule.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2019 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 androidx.testutils
+
+import android.app.Activity
+import androidx.test.core.app.ActivityScenario
+import org.junit.rules.ExternalResource
+
+/**
+ * Use this Rule in conjunction with a [ResettableActivityScenarioRule] to launch one Activity per
+ * test class.
+ *
+ * Example usage:
+ * ```
+ * @RunWith(AndroidJUnit4::class)
+ * @LargeTest
+ * class ActivityPersistWithReset() {
+ * companion object {
+ * @get:ClassRule
+ * @JvmStatic
+ * val activityRule = ResettableActivityScenarioRule(TestActivity::class.java)
+ * }
+ *
+ * @get:Rule
+ * val resetRule = ActivityScenarioResetRule(activityRule.scenario) {
+ * it.setContentView(R.layout.content_view)
+ * }
+ *
+ * @Test
+ * fun test1() {
+ * activityRule.scenario.onActivity {
+ * ...
+ * }
+ * }
+ *
+ * @Test
+ * fun test2() {
+ * activityRule.scenario.onActivity {
+ * ...
+ * }
+ * }
+ * ...
+ * }
+ * ```
+ */
+open class ActivityScenarioResetRule<A : Activity>(
+ private val scenario: ActivityScenario<A>,
+ private val predicate: (A) -> Unit
+) : ExternalResource() {
+ override fun before() {
+ super.before()
+ scenario.onActivity {
+ predicate.invoke(it)
+ }
+ // reset has likely modified activity state, so allow state (e.g. layout/measure) to resolve
+ scenario.onActivity {}
+ }
+
+ override fun after() {
+ // TODO: validate activity hasn't been modified from launch state.
+
+ // If using this reset rule, it's invalid for the activity to not be resumed, for keyguard
+ // to be left up, dialog left open, etc., so we can validate that hasn't happened here.
+ super.after()
+ }
+}
\ No newline at end of file
diff --git a/testutils/testutils-runtime/src/main/java/androidx/testutils/ResettableActivityScenarioRule.kt b/testutils/testutils-runtime/src/main/java/androidx/testutils/ResettableActivityScenarioRule.kt
new file mode 100644
index 0000000..59a2282
--- /dev/null
+++ b/testutils/testutils-runtime/src/main/java/androidx/testutils/ResettableActivityScenarioRule.kt
@@ -0,0 +1,130 @@
+/*
+ * Copyright 2019 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 androidx.testutils
+
+import android.app.Activity
+import android.content.Intent
+import android.os.Looper
+import androidx.test.core.app.ActivityScenario
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.runner.AndroidJUnitRunner
+import org.junit.rules.ExternalResource
+import org.junit.runner.Description
+import org.junit.runners.model.Statement
+
+@Suppress("unused")
+open class ActivityRecyclingAndroidJUnitRunner : AndroidJUnitRunner() {
+ override fun waitForActivitiesToComplete() {
+ }
+}
+
+/**
+ * Implement this interface on your Activity to allow HackyActivityScenarioRule to
+ * launch once-per-test-class.
+ */
+interface Resettable {
+ fun setFinishEnabled(finishEnabled: Boolean)
+}
+
+/**
+ * Copy of ActivityScenarioRule, but which works around AndroidX test infra trying to finish
+ * activities in between each test.
+ */
+class ResettableActivityScenarioRule<A> : ExternalResource where A : Activity, A : Resettable {
+ private val scenarioSupplier: () -> ActivityScenario<A>
+ private lateinit var _scenario: ActivityScenario<A>
+ private var finishEnabled: Boolean = true
+ private var initialTouchMode: Boolean = false
+
+ val scenario: ActivityScenario<A>
+ get() = _scenario
+
+ override fun apply(base: Statement?, description: Description): Statement {
+ // Running as a ClassRule? Disable activity finish
+ finishEnabled = (description.methodName != null)
+ return super.apply(base, description)
+ }
+
+ @JvmOverloads
+ constructor(activityClass: Class<A>, initialTouchMode: Boolean = false) {
+ this.initialTouchMode = initialTouchMode
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(initialTouchMode)
+ scenarioSupplier = { ActivityScenario.launch(activityClass) }
+ }
+
+ @JvmOverloads
+ constructor(startActivityIntent: Intent, initialTouchMode: Boolean = false) {
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(initialTouchMode)
+ scenarioSupplier = { ActivityScenario.launch(startActivityIntent) }
+ }
+
+ @Throws(Throwable::class)
+ override fun before() {
+ _scenario = scenarioSupplier.invoke()
+ _activity = internalGetActivity()
+ if (!finishEnabled) {
+// TODO: Correct approach inside test lib would be removing activity from cleanup list
+ scenario.onActivity {
+ it.setFinishEnabled(false)
+ }
+ }
+ }
+
+ override fun after() {
+ if (!finishEnabled) {
+ scenario.onActivity {
+ it.setFinishEnabled(true)
+ }
+ }
+ scenario.close()
+ InstrumentationRegistry.getInstrumentation().setInTouchMode(initialTouchMode)
+ }
+
+ // Below are compat hacks to get RecyclerView ActivityTestRule tests up and running quickly
+
+ fun runOnUiThread(runnable: Runnable) {
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ runnable.run()
+ } else {
+ InstrumentationRegistry.getInstrumentation().runOnMainSync {
+ runnable.run()
+ }
+ }
+ }
+
+ fun runOnUiThread(action: () -> Unit) {
+ runOnUiThread(Runnable(action))
+ }
+
+ private fun internalGetActivity(): A {
+ val activityReturn = mutableListOf<A?>(null)
+ scenario.onActivity { activity -> activityReturn[0] = activity }
+ return activityReturn[0]!!
+ }
+
+ private lateinit var _activity: A
+ fun getActivity(): A {
+ return _activity
+ }
+}
+
+@Suppress("FunctionName") /* Acts as constructor */
+inline fun <reified A> ResettableActivityScenarioRule(
+ initialTouchMode: Boolean = false
+): ResettableActivityScenarioRule<A> where A : Activity, A : Resettable {
+ return ResettableActivityScenarioRule(A::class.java, initialTouchMode)
+}
\ No newline at end of file
diff --git a/testutils/testutils-runtime/src/main/res/layout/content_view.xml b/testutils/testutils-runtime/src/main/res/layout/content_view.xml
new file mode 100644
index 0000000..e84aae1
--- /dev/null
+++ b/testutils/testutils-runtime/src/main/res/layout/content_view.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ Copyright 2019 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.
+ -->
+
+<TextView xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/text"
+ android:orientation="vertical"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"/>
\ No newline at end of file