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