Cherry-pick Ravenwood "core" code

- Copied f/b/r and f/b/t/h

- Ported files under f/b/core, only what needed to for run-ravenwood-tests.sh to pass

- Local changes because of missing resoucres support

  - Added @DisabledOnRavenwood(reason="AOSP is missing resources support") to tests under f/b/r that depends on resources
bivalentinst and servicestest

  - Added try-catch around ResourcesManager.setInstance()

Flag: EXEMPT host test change only
Bug: 292141694
Test: $ANDROID_BUILD_TOP/frameworks/base/ravenwood/scripts/run-ravenwood-tests.sh

Merged-in: I8a9b8374be3ae052ba4f152eb43af20d0871597f
Change-Id: Iefd574dbded8c4ab2e244c4918c26641364a3432
diff --git a/ravenwood/.gitignore b/ravenwood/.gitignore
new file mode 100644
index 0000000..751553b
--- /dev/null
+++ b/ravenwood/.gitignore
@@ -0,0 +1 @@
+*.bak
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index ac62687..10e4f38 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -5,6 +5,10 @@
     // to get the below license kinds:
     //   SPDX-license-identifier-Apache-2.0
     default_applicable_licenses: ["frameworks_base_license"],
+
+    // OWNER: g/ravenwood
+    // Bug component: 25698
+    default_team: "trendy_team_framework_backstage_power",
 }
 
 filegroup {
@@ -94,6 +98,9 @@
     libs: [
         "ravenwood-runtime-common-ravenwood",
     ],
+    static_libs: [
+        "framework-annotations-lib", // should it be "libs" instead?
+    ],
     visibility: ["//visibility:private"],
 }
 
@@ -126,8 +133,9 @@
     ],
     libs: [
         "framework-minus-apex.ravenwood",
-        "ravenwood-junit",
+        "ravenwood-helper-libcore-runtime",
     ],
+    sdk_version: "core_current",
     visibility: ["//visibility:private"],
 }
 
@@ -160,11 +168,22 @@
         "ravenwood-framework",
         "services.core.ravenwood",
         "junit",
+        "framework-annotations-lib",
+        "ravenwood-helper-framework-runtime",
+        "ravenwood-helper-libcore-runtime",
     ],
     visibility: ["//frameworks/base"],
     jarjar_rules: ":ravenwood-services-jarjar-rules",
 }
 
+java_device_for_host {
+    name: "ravenwood-junit-impl-for-ravenizer",
+    libs: [
+        "ravenwood-junit-impl",
+    ],
+    visibility: [":__subpackages__"],
+}
+
 // Separated out from ravenwood-junit-impl since it needs to compile
 // against `module_current`
 java_library {
@@ -202,6 +221,7 @@
     libs: [
         "junit",
         "flag-junit",
+        "framework-annotations-lib",
     ],
     visibility: ["//visibility:public"],
 }
@@ -279,10 +299,10 @@
     src: "scripts/ravenwood-stats-checker.sh",
     test_suites: ["general-tests"],
     data: [
-        ":hoststubgen_framework-minus-apex_stats.csv",
-        ":hoststubgen_framework-minus-apex_apis.csv",
-        ":hoststubgen_framework-minus-apex_keep_all.txt",
-        ":hoststubgen_framework-minus-apex_dump.txt",
+        ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_stats.csv}",
+        ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_apis.csv}",
+        ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_keep_all.txt}",
+        ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_dump.txt}",
         ":services.core.ravenwood-base{hoststubgen_services.core_stats.csv}",
         ":services.core.ravenwood-base{hoststubgen_services.core_apis.csv}",
         ":services.core.ravenwood-base{hoststubgen_services.core_keep_all.txt}",
diff --git a/ravenwood/TEST_MAPPING b/ravenwood/TEST_MAPPING
index 5754837..86246e2 100644
--- a/ravenwood/TEST_MAPPING
+++ b/ravenwood/TEST_MAPPING
@@ -1,20 +1,15 @@
-// Keep the following two TEST_MAPPINGs in sync:
-// frameworks/base/ravenwood/TEST_MAPPING
-// frameworks/base/tools/hoststubgen/TEST_MAPPING
 {
   "presubmit": [
     { "name": "tiny-framework-dump-test" },
     { "name": "hoststubgentest" },
+    { "name": "hoststubgen-test-tiny-test" },
     { "name": "hoststubgen-invoke-test" },
-    {
-      "name": "RavenwoodMockitoTest_device"
-    },
-    {
-      "name": "RavenwoodBivalentTest_device"
-    },
-    {
-      "name": "RavenwoodResApkTest"
-    },
+    { "name": "RavenwoodMockitoTest_device" },
+    { "name": "RavenwoodBivalentTest_device" },
+
+    { "name": "RavenwoodBivalentInstTest_nonself_inst" },
+    { "name": "RavenwoodBivalentInstTest_self_inst_device" },
+
     // The sysui tests should match vendor/unbundled_google/packages/SystemUIGoogle/TEST_MAPPING
     {
       "name": "SystemUIGoogleTests"
@@ -26,6 +21,128 @@
     }
   ],
   "ravenwood-presubmit": [
+    // AUTO-GENERATED-START
+    // DO NOT MODIFY MANUALLY
+    // Use scripts/update-test-mapping.sh to update it.
+    {
+      "name": "AdServicesSharedLibrariesUnitTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "android.test.mock.ravenwood.tests",
+      "host": true
+    },
+    {
+      "name": "CarLibHostUnitTest",
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
+    },
+    {
+      "name": "CarServiceHostUnitTest",
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
+    },
+    {
+      "name": "CarSystemUIRavenTests",
+      "host": true,
+      "keywords": ["automotive_code_coverage"]
+    },
+    {
+      "name": "CtsAccountManagerTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsAppTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsContentTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsDatabaseTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsGraphicsTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsIcuTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsInputMethodTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsOsTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsProtoTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsResourcesTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsTextTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "CtsUtilTestCasesRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksCoreSystemPropertiesTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksCoreTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksInputMethodSystemServerTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksMockingServicesTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksServicesTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "FrameworksUtilTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "InternalTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "PowerStatsTestsRavenwood",
+      "host": true
+    },
+    {
+      "name": "RavenwoodBivalentInstTest_nonself_inst",
+      "host": true
+    },
+    {
+      "name": "RavenwoodBivalentInstTest_self_inst",
+      "host": true
+    },
+    {
+      "name": "RavenwoodBivalentTest",
+      "host": true
+    },
+    {
+      "name": "RavenwoodCoreTest",
+      "host": true
+    },
     {
       "name": "RavenwoodMinimumTest",
       "host": true
@@ -35,15 +152,22 @@
       "host": true
     },
     {
-      "name": "CtsUtilTestCasesRavenwood",
+      "name": "RavenwoodResApkTest",
       "host": true
     },
     {
-      "name": "RavenwoodCoreTest",
+      "name": "RavenwoodRuntimeTest",
       "host": true
     },
     {
-      "name": "RavenwoodBivalentTest",
+      "name": "RavenwoodServicesTest",
+      "host": true
+    }
+    // AUTO-GENERATED-END
+  ],
+  "ravenwood-postsubmit": [
+    {
+      "name": "SystemUiRavenTests",
       "host": true
     }
   ]
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
similarity index 82%
copy from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
copy to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
index 4b9cf85..b582ccf 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirect.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2023 The Android Open Source Project
+ * Copyright (C) 2024 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.
@@ -15,7 +15,7 @@
  */
 package android.ravenwood.annotation;
 
-import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.ElementType.METHOD;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
@@ -29,8 +29,7 @@
  *
  * @hide
  */
-@Target({TYPE})
+@Target({METHOD})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
-    String value();
+public @interface RavenwoodRedirect {
 }
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
similarity index 94%
rename from ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
rename to ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
index 4b9cf85..bee9222 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodNativeSubstitutionClass.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodRedirectionClass.java
@@ -31,6 +31,6 @@
  */
 @Target({TYPE})
 @Retention(RetentionPolicy.CLASS)
-public @interface RavenwoodNativeSubstitutionClass {
+public @interface RavenwoodRedirectionClass {
     String value();
 }
diff --git a/ravenwood/bivalenttest/Android.bp b/ravenwood/bivalenttest/Android.bp
index 06cf08e6..e897735 100644
--- a/ravenwood/bivalenttest/Android.bp
+++ b/ravenwood/bivalenttest/Android.bp
@@ -39,6 +39,9 @@
         "androidx.test.ext.junit",
         "androidx.test.rules",
 
+        "junit-params",
+        "platform-parametric-runner-lib",
+
         // To make sure it won't cause VerifyError (b/324063814)
         "platformprotosnano",
     ],
@@ -65,6 +68,9 @@
         "androidx.test.ext.junit",
         "androidx.test.rules",
 
+        "junit-params",
+        "platform-parametric-runner-lib",
+
         "ravenwood-junit",
     ],
     jni_libs: [
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
index 3a24c0e..e8f59db 100644
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleDeviceOnlyTest.java
@@ -26,6 +26,10 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
+/**
+ * Test to ensure @DisabledOnRavenwood works. Note, now the DisabledOnRavenwood annotation
+ * is handled by the test runner, so it won't really need the class rule.
+ */
 @RunWith(AndroidJUnit4.class)
 @DisabledOnRavenwood
 public class RavenwoodClassRuleDeviceOnlyTest {
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java
deleted file mode 100644
index aa33dc3..0000000
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodClassRuleRavenwoodOnlyTest.java
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.bivalenttest;
-
-import android.platform.test.ravenwood.RavenwoodClassRule;
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-
-import org.junit.Assert;
-import org.junit.ClassRule;
-import org.junit.Ignore;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-@RunWith(AndroidJUnit4.class)
-// TODO: atest RavenwoodBivalentTest_device fails with the following message.
-// `RUNNER ERROR: Instrumentation reported numtests=7 but only ran 6`
-// @android.platform.test.annotations.DisabledOnNonRavenwood
-// Figure it out and then make DisabledOnNonRavenwood support TYPEs as well.
-@Ignore
-public class RavenwoodClassRuleRavenwoodOnlyTest {
-    @ClassRule
-    public static final RavenwoodClassRule sRavenwood = new RavenwoodClassRule();
-
-    @Test
-    public void testRavenwoodOnly() {
-        Assert.assertTrue(RavenwoodRule.isOnRavenwood());
-    }
-}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
new file mode 100644
index 0000000..a5a16c1
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodConfigTest.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest;
+
+import static android.platform.test.ravenwood.RavenwoodConfig.isOnRavenwood;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.ravenwood.RavenwoodConfig;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure the config field is used.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodConfigTest {
+    private static final String PACKAGE_NAME = "com.test";
+
+    @RavenwoodConfig.Config
+    public static RavenwoodConfig sConfig =
+            new RavenwoodConfig.Builder()
+                    .setPackageName(PACKAGE_NAME)
+                    .build();
+
+    @Test
+    public void testConfig() {
+        assumeTrue(isOnRavenwood());
+        assertEquals(PACKAGE_NAME,
+                InstrumentationRegistry.getInstrumentation().getContext().getPackageName());
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
new file mode 100644
index 0000000..c25d2b4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodMultipleRuleTest.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest;
+
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assume;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure having multiple RavenwoodRule's is detected.
+ * (But only when running on ravenwod. Otherwise it'll be ignored.)
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodMultipleRuleTest {
+
+    @Rule(order = Integer.MIN_VALUE)
+    public final ExpectedException mExpectedException = ExpectedException.none();
+
+    @Rule
+    public final RavenwoodRule mRavenwood1 = new RavenwoodRule();
+
+    @Rule
+    public final RavenwoodRule mRavenwood2 = new RavenwoodRule();
+
+    public RavenwoodMultipleRuleTest() {
+        // We can't call it within the test method because the exception happens before
+        // calling the method, so set it up here.
+        if (RavenwoodConfig.isOnRavenwood()) {
+            mExpectedException.expectMessage("Multiple nesting RavenwoodRule");
+        }
+    }
+
+    @Test
+    public void testMultipleRulesNotAllowed() {
+        Assume.assumeTrue(RavenwoodConfig.isOnRavenwood());
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
new file mode 100644
index 0000000..d473305
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodNoConfigNoRuleTest.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest;
+
+import static org.junit.Assert.assertNotNull;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure the environment is still initialized when no config and no rules are set.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodNoConfigNoRuleTest {
+
+    @Test
+    public void testInitialization() {
+        assertNotNull(InstrumentationRegistry.getInstrumentation());
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
index 01e90d8..3de372e 100644
--- a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/RavenwoodRuleTest.java
@@ -15,7 +15,6 @@
  */
 package com.android.ravenwoodtest.bivalenttest;
 
-import android.platform.test.annotations.DisabledOnNonRavenwood;
 import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 import android.util.Log;
@@ -39,12 +38,6 @@
     }
 
     @Test
-    @DisabledOnNonRavenwood
-    public void testRavenwoodOnly() {
-        Assert.assertTrue(RavenwoodRule.isOnRavenwood());
-    }
-
-    @Test
     public void testDumpSystemProperties() {
         Log.w("XXX", "System properties");
         for (var sp : System.getProperties().entrySet()) {
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
new file mode 100644
index 0000000..09a0aa8
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/CallTracker.java
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import static java.lang.StackWalker.Option.RETAIN_CLASS_REFERENCE;
+
+import android.util.Log;
+
+import java.lang.StackWalker.StackFrame;
+import java.util.HashMap;
+
+/**
+ * Used to keep track of and count the number of calls.
+ */
+public class CallTracker {
+    public static final String TAG = "CallTracker";
+
+    private final HashMap<String, Integer> mNumCalled = new HashMap<>();
+
+    /**
+     * Call it when a method is called. It increments the count for the calling method.
+     */
+    public void incrementMethodCallCount() {
+        var methodName = getCallingMethodName(1);
+
+        Log.i(TAG, "Method called: " + methodName);
+
+        mNumCalled.put(methodName, getNumCalled(methodName) + 1);
+    }
+
+    /**
+     * Return the number of calls of a method.
+     */
+    public int getNumCalled(String methodName) {
+        return mNumCalled.getOrDefault(methodName, 0);
+    }
+
+    /**
+     * Return the current method name. (with the class name.)
+     */
+    private static String getCallingMethodName(int frameOffset) {
+        var walker = StackWalker.getInstance(RETAIN_CLASS_REFERENCE);
+        var caller = walker.walk(frames ->
+                frames.skip(1 + frameOffset).findFirst().map(StackFrame::getMethodName)
+        );
+        return caller.get();
+    }
+
+    /**
+     * Check the number of calls stored in {@link #mNumCalled}.
+     */
+    public void assertCalls(Object... methodNameAndCountPairs) {
+        // Create a local copy
+        HashMap<String, Integer> counts = new HashMap<>(mNumCalled);
+        for (int i = 0; i < methodNameAndCountPairs.length - 1; i += 2) {
+            String methodName = (String) methodNameAndCountPairs[i];
+            int expectedCount = (Integer) methodNameAndCountPairs[i + 1];
+
+            if (getNumCalled(methodName) != expectedCount) {
+                fail(String.format("Method %s: expected call count=%d, actual=%d",
+                        methodName, expectedCount, getNumCalled(methodName)));
+            }
+            counts.remove(methodName);
+        }
+        // All other entries are expected to be 0.
+        var sb = new StringBuilder();
+        for (var e : counts.entrySet()) {
+            if (e.getValue() == 0) {
+                continue;
+            }
+            sb.append(String.format("Method %s: expected call count=0, actual=%d",
+                    e.getKey(), e.getValue()));
+        }
+        if (sb.length() > 0) {
+            fail(sb.toString());
+        }
+    }
+
+    /**
+     * Same as {@link #assertCalls(Object...)} but it kills the process if it fails.
+     * Only use in @AfterClass.
+     */
+    public void assertCallsOrDie(Object... methodNameAndCountPairs) {
+        try {
+            assertCalls(methodNameAndCountPairs);
+        } catch (Throwable th) {
+            // TODO: I don't think it's by spec, but the exception here would be ignored both on
+            // ravenwood and on the device side. Look into it.
+            Log.e(TAG, "*** Failure detected in @AfterClass! ***", th);
+            Log.e(TAG, "JUnit seems to ignore exceptions from @AfterClass, so killing self.");
+            System.exit(7);
+        }
+    }
+
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
new file mode 100644
index 0000000..d7c2c6c
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodAwareTestRunnerTest.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertFalse;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure RavenwoodAwareTestRunnerTest properly delegates to the original runner,
+ * and also run the special annotated methods.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class RavenwoodAwareTestRunnerTest {
+    public static final String TAG = "RavenwoodAwareTestRunnerTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    private static int getExpectedRavenwoodRunnerInitializingNumCalls() {
+        return RavenwoodRule.isOnRavenwood() ? 1 : 0;
+    }
+
+    @RavenwoodTestRunnerInitializing
+    public static void ravenwoodRunnerInitializing() {
+        // No other calls should have been made.
+        sCallTracker.assertCalls();
+
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        sCallTracker.assertCalls(
+                "ravenwoodRunnerInitializing",
+                getExpectedRavenwoodRunnerInitializingNumCalls()
+        );
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    public void test1() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    @Parameters({"foo", "bar"})
+    public void testWithParams(String arg) {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    @DisabledOnRavenwood
+    public void testDeviceOnly() {
+        assertFalse(RavenwoodRule.isOnRavenwood());
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "ravenwoodRunnerInitializing",
+                getExpectedRavenwoodRunnerInitializingNumCalls(),
+                "beforeClass", 1,
+                "test1", 1,
+                "testWithParams", 2
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
new file mode 100644
index 0000000..7ef672e
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitClassRuleDeviceOnlyTest.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood
+public class RavenwoodImplicitClassRuleDeviceOnlyTest {
+    public static final String TAG = "RavenwoodImplicitClassRuleDeviceOnlyTest";
+
+    @BeforeClass
+    public static void beforeClass() {
+        // This method shouldn't be called -- unless RUN_DISABLED_TESTS is enabled.
+
+        // If we're doing RUN_DISABLED_TESTS, don't throw here, because that'd confuse junit.
+        if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+            Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+        }
+    }
+
+    @Test
+    public void testDeviceOnly() {
+        Assert.assertFalse(RavenwoodRule.isOnRavenwood());
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        if (RavenwoodRule.isOnRavenwood()) {
+            Log.e(TAG, "Even @AfterClass shouldn't be executed!");
+
+            if (!RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+                System.exit(1);
+            }
+        }
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
new file mode 100644
index 0000000..7ef40dc
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleOrderRewriteTest.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.fail;
+
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Assume;
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+import java.util.HashMap;
+
+/**
+ * Make sure ravenizer will inject implicit rules and rewrite the existing rules' orders.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodImplicitRuleOrderRewriteTest {
+
+    private static final TestRule sEmptyRule = (statement, description) -> statement;
+
+    // We have two sets of 9 rules below, for class rules and instance rules.
+    // - Ravenizer will inject 2 more rules of each kind.
+    // - Ravenizer will adjust their order, so even though we'll add two sets of class and instance
+    //  rules with a MIN / MAX order, there will still be no duplicate in the order.
+
+    private static final int EXPECTED_RULE_COUNT = 9 + 2;
+
+    @ClassRule(order = Integer.MIN_VALUE)
+    public static final TestRule sRule01 = sEmptyRule;
+
+    @ClassRule(order = Integer.MIN_VALUE + 1)
+    public static final TestRule sRule02 = sEmptyRule;
+
+    @ClassRule(order = -10)
+    public static final TestRule sRule03 = sEmptyRule;
+
+    @ClassRule(order = -1)
+    public static final TestRule sRule04 = sEmptyRule;
+
+    @ClassRule(order = 0)
+    public static final TestRule sRule05 = sEmptyRule;
+
+    @ClassRule(order = 1)
+    public static final TestRule sRule06 = sEmptyRule;
+
+    @ClassRule(order = 10)
+    public static final TestRule sRule07 = sEmptyRule;
+
+    @ClassRule(order = Integer.MAX_VALUE - 1)
+    public static final TestRule sRule08 = sEmptyRule;
+
+    @ClassRule(order = Integer.MAX_VALUE)
+    public static final TestRule sRule09 = sEmptyRule;
+
+    @Rule(order = Integer.MIN_VALUE)
+    public final TestRule mRule01 = sEmptyRule;
+
+    @Rule(order = Integer.MIN_VALUE + 1)
+    public final TestRule mRule02 = sEmptyRule;
+
+    @Rule(order = -10)
+    public final TestRule mRule03 = sEmptyRule;
+
+    @Rule(order = -1)
+    public final TestRule mRule04 = sEmptyRule;
+
+    @Rule(order = 0)
+    public final TestRule mRule05 = sEmptyRule;
+
+    @Rule(order = 1)
+    public final TestRule mRule06 = sEmptyRule;
+
+    @Rule(order = 10)
+    public final TestRule mRule07 = sEmptyRule;
+
+    @Rule(order = Integer.MAX_VALUE - 1)
+    public final TestRule mRule08 = sEmptyRule;
+
+    @Rule(order = Integer.MAX_VALUE)
+    public final TestRule mRule09 = sEmptyRule;
+
+    private void checkRules(boolean classRule) {
+        final var anotClass = classRule ? ClassRule.class : Rule.class;
+
+        final HashMap<Integer, Integer> ordersUsed = new HashMap<>();
+
+        for (var field : this.getClass().getDeclaredFields()) {
+            if (!field.isAnnotationPresent(anotClass)) {
+                continue;
+            }
+            final var anot = field.getAnnotation(anotClass);
+            final int order = classRule ? ((ClassRule) anot).order() : ((Rule) anot).order();
+
+            if (ordersUsed.containsKey(order)) {
+                fail("Detected duplicate order=" + order);
+            }
+            ordersUsed.put(order, 1);
+        }
+        assertEquals(EXPECTED_RULE_COUNT, ordersUsed.size());
+    }
+
+    @Test
+    public void testClassRules() {
+        Assume.assumeTrue(RavenwoodRule.isOnRavenwood());
+
+        checkRules(true);
+    }
+
+    @Test
+    public void testInstanceRules() {
+        Assume.assumeTrue(RavenwoodRule.isOnRavenwood());
+
+        checkRules(false);
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
new file mode 100644
index 0000000..ae596b1
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTest.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test to make sure when a test class inherits another test class, the base class's
+ * implicit rules are shadowed and won't be executed.
+ *
+ * ... But for now, we don't have a way to programmatically check it, so for now we need to
+ * check the log file manually.
+ *
+ * TODO: Implement the test.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodImplicitRuleShadowingTest extends RavenwoodImplicitRuleShadowingTestBase {
+    @Test
+    public void testOkInSubClass() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
new file mode 100644
index 0000000..1ca97af
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodImplicitRuleShadowingTestBase.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * A test class that's just inherited by RavenwoodImplicitRuleShadowingTest.
+ */
+@RunWith(AndroidJUnit4.class)
+public abstract class RavenwoodImplicitRuleShadowingTestBase {
+    @Test
+    public void testOkInBaseClass() {
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
new file mode 100644
index 0000000..9d878f4
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodNoRavenizerTest.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+
+import org.junit.Test;
+
+/**
+ * Test for {@link android.platform.test.annotations.NoRavenizer}
+ */
+@NoRavenizer
+public class RavenwoodNoRavenizerTest {
+    public static final String TAG = "RavenwoodNoRavenizerTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    /**
+     * With @NoRavenizer, this method shouldn't be called.
+     */
+    @RavenwoodTestRunnerInitializing
+    public static void ravenwoodRunnerInitializing() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    /**
+     * Make sure ravenwoodRunnerInitializing() wasn't called.
+     */
+    @Test
+    public void testNotRavenized() {
+        sCallTracker.assertCalls(
+                "ravenwoodRunnerInitializing", 0
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
new file mode 100644
index 0000000..c77841b
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsReallyDisabledTest.java
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for "RAVENWOOD_RUN_DISABLED_TESTS" with "REALLY_DISABLED" set.
+ *
+ * This test is only executed on Ravenwood.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunDisabledTestsReallyDisabledTest {
+    private static final String TAG = "RavenwoodRunDisabledTestsTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    @RavenwoodTestRunnerInitializing
+    public static void ravenwoodRunnerInitializing() {
+        RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true,
+                "\\#testReallyDisabled$");
+    }
+
+    /**
+     * This test gets to run with RAVENWOOD_RUN_DISABLED_TESTS set.
+     */
+    @Test
+    @DisabledOnRavenwood
+    public void testDisabledTestGetsToRun() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        sCallTracker.incrementMethodCallCount();
+
+        fail("This test won't pass on Ravenwood.");
+    }
+
+    /**
+     * This will still not be executed due to the "really disabled" pattern.
+     */
+    @Test
+    @DisabledOnRavenwood
+    public void testReallyDisabled() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        sCallTracker.incrementMethodCallCount();
+
+        fail("This test won't pass on Ravenwood.");
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "testDisabledTestGetsToRun", 1,
+                "testReallyDisabled", 0
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
new file mode 100644
index 0000000..ea1a29d
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunDisabledTestsTest.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import static org.junit.Assert.fail;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.RavenwoodTestRunnerInitializing;
+import android.platform.test.ravenwood.RavenwoodRule;
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.junit.runner.RunWith;
+
+/**
+ * Test for "RAVENWOOD_RUN_DISABLED_TESTS". (with no "REALLY_DISABLED" set.)
+ *
+ * This test is only executed on Ravenwood.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunDisabledTestsTest {
+    private static final String TAG = "RavenwoodRunDisabledTestsTest";
+
+    @Rule
+    public ExpectedException mExpectedException = ExpectedException.none();
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    @RavenwoodTestRunnerInitializing
+    public static void ravenwoodRunnerInitializing() {
+        RavenwoodRule.private$ravenwood().overrideRunDisabledTest(true, null);
+    }
+
+    @Test
+    @DisabledOnRavenwood
+    public void testDisabledTestGetsToRun() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        sCallTracker.incrementMethodCallCount();
+
+        fail("This test won't pass on Ravenwood.");
+    }
+
+    @Test
+    @DisabledOnRavenwood
+    public void testDisabledButPass() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        sCallTracker.incrementMethodCallCount();
+
+        // When a @DisabledOnRavenwood actually passed, the runner should make fail().
+        mExpectedException.expectMessage("it actually passed under Ravenwood");
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        if (!RavenwoodRule.isOnRavenwood()) {
+            return;
+        }
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "testDisabledTestGetsToRun", 1,
+                "testDisabledButPass", 1
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
new file mode 100644
index 0000000..c042eb0
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithAndroidXRunnerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure ravenwood's test runner works with {@link AndroidJUnit4}.
+ */
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodRunnerWithAndroidXRunnerTest {
+    public static final String TAG = "RavenwoodRunnerWithAndroidXRunnerTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    @BeforeClass
+    public static void beforeClass() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Before
+    public void beforeTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @After
+    public void afterTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    public void test1() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    public void test2() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "beforeClass", 1,
+                "beforeTest", 2,
+                "afterTest", 2,
+                "test1", 1,
+                "test2", 1
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
new file mode 100644
index 0000000..2feb5ba
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithJUnitParamsRunnerTest.java
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Make sure ravenwood's test runner works with {@link AndroidJUnit4}.
+ */
+@RunWith(JUnitParamsRunner.class)
+public class RavenwoodRunnerWithJUnitParamsRunnerTest  {
+    public static final String TAG = "RavenwoodRunnerTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    @BeforeClass
+    public static void beforeClass() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Before
+    public void beforeTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @After
+    public void afterTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    public void testWithNoParams() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Test
+    @Parameters({"foo", "bar"})
+    public void testWithParams(String arg) {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "beforeClass", 1,
+                "beforeTest", 3,
+                "afterTest", 3,
+                "testWithNoParams", 1,
+                "testWithParams", 2
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
new file mode 100644
index 0000000..7e3bc0f
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodRunnerWithParameterizedAndroidJunit4Test.java
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import org.junit.After;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Make sure ravenwood's test runner works with {@link ParameterizedAndroidJunit4}.
+ */
+@RunWith(ParameterizedAndroidJunit4.class)
+public class RavenwoodRunnerWithParameterizedAndroidJunit4Test {
+    public static final String TAG = "RavenwoodRunnerTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    private final String mParam;
+
+    private static int sNumInsantiation = 0;
+
+    public RavenwoodRunnerWithParameterizedAndroidJunit4Test(String param) {
+        mParam = param;
+        sNumInsantiation++;
+    }
+
+    @BeforeClass
+    public static void beforeClass() {
+        // It seems like ParameterizedAndroidJunit4 calls the @BeforeTest / @AfterTest methods
+        // one time too many.
+        // With two parameters, this method should be called only twice, but it's actually
+        // called three times.
+        // So let's not check the number fo beforeClass calls.
+    }
+
+    @Before
+    public void beforeTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @After
+    public void afterTest() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @Parameters
+    public static List<String> getParams() {
+        var params =  new ArrayList<String>();
+        params.add("foo");
+        params.add("bar");
+        return params;
+    }
+
+    @Test
+    public void testWithParams() {
+        sCallTracker.incrementMethodCallCount();
+    }
+
+    @AfterClass
+    public static void afterClass() {
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "beforeTest", sNumInsantiation,
+                "afterTest", sNumInsantiation,
+                "testWithParams", sNumInsantiation
+        );
+    }
+}
diff --git a/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
new file mode 100644
index 0000000..7e396c2
--- /dev/null
+++ b/ravenwood/bivalenttest/test/com/android/ravenwoodtest/bivalenttest/ravenizer/RavenwoodSuiteTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalenttest.ravenizer;
+
+import android.util.Log;
+
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Suite;
+
+/**
+ * Test to make sure {@link Suite} works with the ravenwood test runner.
+ */
+@RunWith(Suite.class)
[email protected]({
+        RavenwoodSuiteTest.Test1.class,
+        RavenwoodSuiteTest.Test2.class
+})
+public class RavenwoodSuiteTest {
+    public static final String TAG = "RavenwoodSuiteTest";
+
+    private static final CallTracker sCallTracker = new CallTracker();
+
+    @AfterClass
+    public static void afterClass() {
+        Log.i(TAG, "afterClass called");
+
+        sCallTracker.assertCallsOrDie(
+                "test1", 1,
+                "test2", 1
+        );
+    }
+
+    /**
+     * Workaround for the issue where tradefed won't think a class is a test class
+     * if it has a @RunWith but no @Test methods, even if it is a Suite.
+     */
+    @Test
+    public void testEmpty() {
+    }
+
+    public static class Test1 {
+        @Test
+        public void test1() {
+            sCallTracker.incrementMethodCallCount();
+        }
+    }
+
+    public static class Test2 {
+        @Test
+        public void test2() {
+            sCallTracker.incrementMethodCallCount();
+        }
+    }
+}
diff --git a/ravenwood/coretest/README.md b/ravenwood/coretest/README.md
deleted file mode 100644
index b60bfbf..0000000
--- a/ravenwood/coretest/README.md
+++ /dev/null
@@ -1,3 +0,0 @@
-# Ravenwood core test
-
-This test contains (non-bivalent) tests for Ravenwood itself -- e.g. tests for the ravenwood rules.
\ No newline at end of file
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
deleted file mode 100644
index f1e33cb..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/RavenwoodTestRunnerValidationTest.java
+++ /dev/null
@@ -1,53 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.coretest;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4; // Intentionally use the deprecated one.
-
-import org.junit.Assume;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * Test for the test runner validator in RavenwoodRule.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestRunnerValidationTest {
-    // Note the following rules don't have a @Rule, because they need to be applied in a specific
-    // order. So we use a RuleChain instead.
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestRunnerValidationTest() {
-        Assume.assumeTrue(RavenwoodRule._$RavenwoodPrivate.isOptionalValidationEnabled());
-        // Because RavenwoodRule will throw this error before executing the test method,
-        // we can't do it in the test method itself.
-        // So instead, we initialize it here.
-        mThrown.expectMessage("Switch to androidx.test.ext.junit.runners.AndroidJUnit4");
-    }
-
-    @Test
-    public void testValidateTestRunner() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
deleted file mode 100644
index db95fad..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail01_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail01_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail01_Test() {
-        mThrown.expectMessage("Method setUp() doesn't have @Before");
-    }
-
-    @SuppressWarnings("JUnit4SetUpNotRun")
-    public void setUp() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
deleted file mode 100644
index ddc66c7..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail02_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail02_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail02_Test() {
-        mThrown.expectMessage("Method tearDown() doesn't have @After");
-    }
-
-    @SuppressWarnings("JUnit4TearDownNotRun")
-    public void tearDown() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
deleted file mode 100644
index ec8e907..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_Fail03_Test.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.rules.ExpectedException;
-import org.junit.rules.RuleChain;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_Fail03_Test {
-    private ExpectedException mThrown = ExpectedException.none();
-    private final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Rule
-    public final RuleChain chain = RuleChain.outerRule(mThrown).around(mRavenwood);
-
-    public RavenwoodTestMethodValidation_Fail03_Test() {
-        mThrown.expectMessage("Method testFoo() doesn't have @Test");
-    }
-
-    @SuppressWarnings("JUnit4TestNotRun")
-    public void testFoo() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java b/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
deleted file mode 100644
index d952d07..0000000
--- a/ravenwood/coretest/test/com/android/ravenwoodtest/coretest/methodvalidation/RavenwoodTestMethodValidation_OkTest.java
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * Copyright (C) 2024 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.ravenwoodtest.coretest.methodvalidation;
-
-import android.platform.test.ravenwood.RavenwoodRule;
-
-import androidx.test.runner.AndroidJUnit4;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Rule;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-/**
- * RavenwoodRule has a validator to ensure "test-looking" methods have valid JUnit annotations.
- * This class contains tests for this validator.
- */
-@RunWith(AndroidJUnit4.class)
-public class RavenwoodTestMethodValidation_OkTest {
-    @Rule
-    public final RavenwoodRule mRavenwood = new RavenwoodRule();
-
-    @Before
-    public void setUp() {
-    }
-
-    @Before
-    public void testSetUp() {
-    }
-
-    @After
-    public void tearDown() {
-    }
-
-    @After
-    public void testTearDown() {
-    }
-
-    @Test
-    public void testEmpty() {
-    }
-}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
new file mode 100644
index 0000000..478bead
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import static org.junit.Assert.fail;
+
+import android.os.Bundle;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope;
+import android.platform.test.ravenwood.RavenwoodTestStats.Result;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Provide hook points created by {@link RavenwoodAwareTestRunner}.
+ *
+ * States are associated with each {@link RavenwoodAwareTestRunner} are stored in
+ * {@link RavenwoodRunnerState}, rather than as members of {@link RavenwoodAwareTestRunner}.
+ * See its javadoc for the reasons.
+ *
+ * All methods in this class must be called from the test main thread.
+ */
+public class RavenwoodAwareTestRunnerHook {
+    private static final String TAG = RavenwoodAwareTestRunner.TAG;
+
+    private RavenwoodAwareTestRunnerHook() {
+    }
+
+    /**
+     * Called before any code starts. Internally it will only initialize the environment once.
+     */
+    public static void performGlobalInitialization() {
+        RavenwoodRuntimeEnvironmentController.globalInitOnce();
+    }
+
+    /**
+     * Called when a runner starts, before the inner runner gets a chance to run.
+     */
+    public static void onRunnerInitializing(RavenwoodAwareTestRunner runner, TestClass testClass) {
+        Log.i(TAG, "onRunnerInitializing: testClass=" + testClass.getJavaClass()
+                + " runner=" + runner);
+
+        // This is needed to make AndroidJUnit4ClassRunner happy.
+        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+    }
+
+    /**
+     * Called when a whole test class is skipped.
+     */
+    public static void onClassSkipped(Description description) {
+        Log.i(TAG, "onClassSkipped: description=" + description);
+        RavenwoodTestStats.getInstance().onClassSkipped(description);
+    }
+
+    /**
+     * Called before the inner runner starts.
+     */
+    public static void onBeforeInnerRunnerStart(
+            RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+        Log.v(TAG, "onBeforeInnerRunnerStart: description=" + description);
+
+        // Prepare the environment before the inner runner starts.
+        runner.mState.enterTestClass(description);
+    }
+
+    /**
+     * Called after the inner runner finished.
+     */
+    public static void onAfterInnerRunnerFinished(
+            RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+        Log.v(TAG, "onAfterInnerRunnerFinished: description=" + description);
+
+        RavenwoodTestStats.getInstance().onClassFinished(description);
+        runner.mState.exitTestClass();
+    }
+
+    /**
+     * Called before a test / class.
+     *
+     * Return false if it should be skipped.
+     */
+    public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description,
+            Scope scope, Order order) throws Throwable {
+        Log.v(TAG, "onBefore: description=" + description + ", " + scope + ", " + order);
+
+        if (scope == Scope.Instance && order == Order.Outer) {
+            // Start of a test method.
+            runner.mState.enterTestMethod(description);
+        }
+
+        final var classDescription = runner.mState.getClassDescription();
+
+        // Class-level annotations are checked by the runner already, so we only check
+        // method-level annotations here.
+        if (scope == Scope.Instance && order == Order.Outer) {
+            if (!RavenwoodEnablementChecker.shouldEnableOnRavenwood(
+                    description, true)) {
+                RavenwoodTestStats.getInstance().onTestFinished(
+                        classDescription, description, Result.Skipped);
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Called after a test / class.
+     *
+     * Return false if the exception should be ignored.
+     */
+    public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description,
+            Scope scope, Order order, Throwable th) {
+        Log.v(TAG, "onAfter: description=" + description + ", " + scope + ", " + order + ", " + th);
+
+        final var classDescription = runner.mState.getClassDescription();
+
+        if (scope == Scope.Instance && order == Order.Outer) {
+            // End of a test method.
+            runner.mState.exitTestMethod();
+            RavenwoodTestStats.getInstance().onTestFinished(classDescription, description,
+                    th == null ? Result.Passed : Result.Failed);
+        }
+
+        // If RUN_DISABLED_TESTS is set, and the method did _not_ throw, make it an error.
+        if (RavenwoodRule.private$ravenwood().isRunningDisabledTests()
+                && scope == Scope.Instance && order == Order.Outer) {
+
+            boolean isTestEnabled = RavenwoodEnablementChecker.shouldEnableOnRavenwood(
+                    description, false);
+            if (th == null) {
+                // Test passed. Is the test method supposed to be enabled?
+                if (isTestEnabled) {
+                    // Enabled and didn't throw, okay.
+                    return true;
+                } else {
+                    // Disabled and didn't throw. We should report it.
+                    fail("Test wasn't included under Ravenwood, but it actually "
+                            + "passed under Ravenwood; consider updating annotations");
+                    return true; // unreachable.
+                }
+            } else {
+                // Test failed.
+                if (isTestEnabled) {
+                    // Enabled but failed. We should throw the exception.
+                    return true;
+                } else {
+                    // Disabled and failed. Expected. Don't throw.
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Called by {@link RavenwoodAwareTestRunner} to see if it should run a test class or not.
+     */
+    public static boolean shouldRunClassOnRavenwood(Class<?> clazz) {
+        return RavenwoodEnablementChecker.shouldRunClassOnRavenwood(clazz, true);
+    }
+
+    /**
+     * Called by RavenwoodRule.
+     */
+    public static void onRavenwoodRuleEnter(RavenwoodAwareTestRunner runner,
+            Description description, RavenwoodRule rule) throws Throwable {
+        Log.v(TAG, "onRavenwoodRuleEnter: description=" + description);
+
+        runner.mState.enterRavenwoodRule(rule);
+    }
+
+
+    /**
+     * Called by RavenwoodRule.
+     */
+    public static void onRavenwoodRuleExit(RavenwoodAwareTestRunner runner,
+            Description description, RavenwoodRule rule) throws Throwable {
+        Log.v(TAG, "onRavenwoodRuleExit: description=" + description);
+
+        runner.mState.exitRavenwoodRule(rule);
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java
new file mode 100644
index 0000000..3535cb2
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodConfigState.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_EMPTY_RESOURCES_APK;
+
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import android.annotation.Nullable;
+import android.app.ResourcesManager;
+import android.content.res.Resources;
+import android.view.DisplayAdjustments;
+
+import java.io.File;
+import java.util.HashMap;
+
+/**
+ * Used to store various states associated with {@link RavenwoodConfig} that's inly needed
+ * in junit-impl.
+ *
+ * We don't want to put it in junit-src to avoid having to recompile all the downstream
+ * dependencies after changing this class.
+ *
+ * All members must be called from the runner's main thread.
+ */
+public class RavenwoodConfigState {
+    private static final String TAG = "RavenwoodConfigState";
+
+    private final RavenwoodConfig mConfig;
+
+    public RavenwoodConfigState(RavenwoodConfig config) {
+        mConfig = config;
+    }
+
+    /** Map from path -> resources. */
+    private final HashMap<File, Resources> mCachedResources = new HashMap<>();
+
+    /**
+     * Load {@link Resources} from an APK, with cache.
+     */
+    public Resources loadResources(@Nullable File apkPath) {
+        var cached = mCachedResources.get(apkPath);
+        if (cached != null) {
+            return cached;
+        }
+
+        var fileToLoad = apkPath != null ? apkPath : new File(RAVENWOOD_EMPTY_RESOURCES_APK);
+
+        assertTrue("File " + fileToLoad + " doesn't exist.", fileToLoad.isFile());
+
+        final String path = fileToLoad.getAbsolutePath();
+        final var emptyPaths = new String[0];
+
+        ResourcesManager.getInstance().initializeApplicationPaths(path, emptyPaths);
+
+        final var ret = ResourcesManager.getInstance().getResources(null, path,
+                emptyPaths, emptyPaths, emptyPaths,
+                emptyPaths, null, null,
+                new DisplayAdjustments().getCompatibilityInfo(),
+                RavenwoodRuntimeEnvironmentController.class.getClassLoader(), null);
+
+        assertNotNull(ret);
+
+        mCachedResources.put(apkPath, ret);
+        return ret;
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
index 1dd5e1d..239c806 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -16,8 +16,13 @@
 
 package android.platform.test.ravenwood;
 
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
+
 import android.content.ClipboardManager;
 import android.content.Context;
+import android.content.res.AssetManager;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
 import android.hardware.ISerialManager;
 import android.hardware.SerialManager;
 import android.os.Handler;
@@ -31,11 +36,18 @@
 import android.util.ArrayMap;
 import android.util.Singleton;
 
+import com.android.internal.annotations.GuardedBy;
+
+import java.io.File;
+import java.io.IOException;
 import java.util.Objects;
 import java.util.concurrent.Executor;
 import java.util.function.Supplier;
 
 public class RavenwoodContext extends RavenwoodBaseContext {
+    private static final String TAG = "Ravenwood";
+
+    private final Object mLock = new Object();
     private final String mPackageName;
     private final HandlerThread mMainThread;
 
@@ -44,15 +56,31 @@
     private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>();
     private final ArrayMap<String, Supplier<?>> mNameToFactory = new ArrayMap<>();
 
+    private final File mFilesDir;
+    private final File mCacheDir;
+    private final Supplier<Resources> mResourcesSupplier;
+
+    private RavenwoodContext mAppContext;
+
+    @GuardedBy("mLock")
+    private Resources mResources;
+
+    @GuardedBy("mLock")
+    private Resources.Theme mTheme;
+
     private void registerService(Class<?> serviceClass, String serviceName,
             Supplier<?> serviceSupplier) {
         mClassToName.put(serviceClass, serviceName);
         mNameToFactory.put(serviceName, serviceSupplier);
     }
 
-    public RavenwoodContext(String packageName, HandlerThread mainThread) {
+    public RavenwoodContext(String packageName, HandlerThread mainThread,
+            Supplier<Resources> resourcesSupplier) throws IOException {
         mPackageName = packageName;
         mMainThread = mainThread;
+        mResourcesSupplier = resourcesSupplier;
+        mFilesDir = createTempDir(packageName + "_files-dir");
+        mCacheDir = createTempDir(packageName + "_cache-dir");
 
         // Services provided by a typical shipping device
         registerService(ClipboardManager.class,
@@ -85,6 +113,11 @@
         }
     }
 
+    void cleanUp() {
+        deleteDir(mFilesDir);
+        deleteDir(mCacheDir);
+    }
+
     @Override
     public String getSystemServiceName(Class<?> serviceClass) {
         // TODO: pivot to using SystemServiceRegistry
@@ -100,34 +133,35 @@
     @Override
     public Looper getMainLooper() {
         Objects.requireNonNull(mMainThread,
-                "Test must request setProvideMainThread() via RavenwoodRule");
+                "Test must request setProvideMainThread() via RavenwoodConfig");
         return mMainThread.getLooper();
     }
 
     @Override
     public Handler getMainThreadHandler() {
         Objects.requireNonNull(mMainThread,
-                "Test must request setProvideMainThread() via RavenwoodRule");
+                "Test must request setProvideMainThread() via RavenwoodConfig");
         return mMainThread.getThreadHandler();
     }
 
     @Override
     public Executor getMainExecutor() {
         Objects.requireNonNull(mMainThread,
-                "Test must request setProvideMainThread() via RavenwoodRule");
+                "Test must request setProvideMainThread() via RavenwoodConfig");
         return mMainThread.getThreadExecutor();
     }
 
     @Override
     public String getPackageName() {
         return Objects.requireNonNull(mPackageName,
-                "Test must request setPackageName() via RavenwoodRule");
+                "Test must request setPackageName() (or setTargetPackageName())"
+                + " via RavenwoodConfig");
     }
 
     @Override
     public String getOpPackageName() {
         return Objects.requireNonNull(mPackageName,
-                "Test must request setPackageName() via RavenwoodRule");
+                "Test must request setPackageName() via RavenwoodConfig");
     }
 
     @Override
@@ -150,6 +184,61 @@
         return Context.DEVICE_ID_DEFAULT;
     }
 
+    @Override
+    public File getFilesDir() {
+        return mFilesDir;
+    }
+
+    @Override
+    public File getCacheDir() {
+        return mCacheDir;
+    }
+
+    @Override
+    public boolean deleteFile(String name) {
+        File f = new File(name);
+        return f.delete();
+    }
+
+    @Override
+    public Resources getResources() {
+        synchronized (mLock) {
+            if (mResources == null) {
+                mResources = mResourcesSupplier.get();
+            }
+            return mResources;
+        }
+    }
+
+    @Override
+    public AssetManager getAssets() {
+        return getResources().getAssets();
+    }
+
+    @Override
+    public Theme getTheme() {
+        synchronized (mLock) {
+            if (mTheme == null) {
+                mTheme = getResources().newTheme();
+            }
+            return mTheme;
+        }
+    }
+
+    @Override
+    public String getPackageResourcePath() {
+        return new File(RAVENWOOD_RESOURCE_APK).getAbsolutePath();
+    }
+
+    public void setApplicationContext(RavenwoodContext appContext) {
+        mAppContext = appContext;
+    }
+
+    @Override
+    public Context getApplicationContext() {
+        return mAppContext;
+    }
+
     /**
      * Wrap the given {@link Supplier} to become memoized.
      *
@@ -175,4 +264,26 @@
     public interface ThrowingSupplier<T> {
         T get() throws Exception;
     }
+
+
+    static File createTempDir(String prefix) throws IOException {
+        // Create a temp file, delete it and recreate it as a directory.
+        final File dir = File.createTempFile(prefix + "-", "");
+        dir.delete();
+        dir.mkdirs();
+        return dir;
+    }
+
+    static void deleteDir(File dir) {
+        File[] children = dir.listFiles();
+        if (children != null) {
+            for (File child : children) {
+                if (child.isDirectory()) {
+                    deleteDir(child);
+                } else {
+                    child.delete();
+                }
+            }
+        }
+    }
 }
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java
new file mode 100644
index 0000000..77275c4
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodEnablementChecker.java
@@ -0,0 +1,117 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.EnabledOnRavenwood;
+import android.platform.test.annotations.IgnoreUnderRavenwood;
+
+import org.junit.runner.Description;
+
+/**
+ * Calculates which tests need to be executed on Ravenwood.
+ */
+public class RavenwoodEnablementChecker {
+    private static final String TAG = "RavenwoodDisablementChecker";
+
+    private RavenwoodEnablementChecker() {
+    }
+
+    /**
+     * Determine if the given {@link Description} should be enabled when running on the
+     * Ravenwood test environment.
+     *
+     * A more specific method-level annotation always takes precedence over any class-level
+     * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over
+     * an {@link DisabledOnRavenwood} annotation.
+     */
+    public static boolean shouldEnableOnRavenwood(Description description,
+            boolean takeIntoAccountRunDisabledTestsFlag) {
+        // First, consult any method-level annotations
+        if (description.isTest()) {
+            Boolean result = null;
+
+            // Stopgap for http://g/ravenwood/EPAD-N5ntxM
+            if (description.getMethodName().endsWith("$noRavenwood")) {
+                result = false;
+            } else if (description.getAnnotation(EnabledOnRavenwood.class) != null) {
+                result = true;
+            } else if (description.getAnnotation(DisabledOnRavenwood.class) != null) {
+                result = false;
+            } else if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) {
+                result = false;
+            }
+            if (result != null) {
+                if (takeIntoAccountRunDisabledTestsFlag
+                        && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+                    result = !shouldStillIgnoreInProbeIgnoreMode(
+                            description.getTestClass(), description.getMethodName());
+                }
+            }
+            if (result != null) {
+                return result;
+            }
+        }
+
+        // Otherwise, consult any class-level annotations
+        return shouldRunClassOnRavenwood(description.getTestClass(),
+                takeIntoAccountRunDisabledTestsFlag);
+    }
+
+    public static boolean shouldRunClassOnRavenwood(@NonNull Class<?> testClass,
+            boolean takeIntoAccountRunDisabledTestsFlag) {
+        boolean result = true;
+        if (testClass.getAnnotation(EnabledOnRavenwood.class) != null) {
+            result = true;
+        } else if (testClass.getAnnotation(DisabledOnRavenwood.class) != null) {
+            result = false;
+        } else if (testClass.getAnnotation(IgnoreUnderRavenwood.class) != null) {
+            result = false;
+        }
+        if (!result) {
+            if (takeIntoAccountRunDisabledTestsFlag
+                    && RavenwoodRule.private$ravenwood().isRunningDisabledTests()) {
+                result = !shouldStillIgnoreInProbeIgnoreMode(testClass, null);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Check if a test should _still_ disabled even if {@code RUN_DISABLED_TESTS}
+     * is true, using {@code REALLY_DISABLED_PATTERN}.
+     *
+     * This only works on tests, not on classes.
+     */
+    static boolean shouldStillIgnoreInProbeIgnoreMode(
+            @NonNull Class<?> testClass, @Nullable String methodName) {
+        if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().pattern().isEmpty()) {
+            return false;
+        }
+
+        final var fullname = testClass.getName() + (methodName != null ? "#" + methodName : "");
+
+        System.out.println("XXX=" + fullname);
+
+        if (RavenwoodRule.private$ravenwood().getReallyDisabledPattern().matcher(fullname).find()) {
+            System.out.println("Still ignoring " + fullname);
+            return true;
+        }
+        return false;
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
new file mode 100644
index 0000000..e548611
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodNativeLoader.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+import java.util.Arrays;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * We use this class to load libandroid_runtime.
+ * In the future, we may load other native libraries.
+ */
+public final class RavenwoodNativeLoader {
+    public static final String CORE_NATIVE_CLASSES = "core_native_classes";
+    public static final String ICU_DATA_PATH = "icu.data.path";
+    public static final String KEYBOARD_PATHS = "keyboard_paths";
+    public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes";
+
+    public static final String LIBANDROID_RUNTIME_NAME = "android_runtime";
+
+    /**
+     * Classes with native methods that are backed by libandroid_runtime.
+     *
+     * See frameworks/base/core/jni/platform/host/HostRuntime.cpp
+     */
+    private static final Class<?>[] sLibandroidClasses = {
+            android.util.Log.class,
+            android.os.Parcel.class,
+            android.os.Binder.class,
+            android.content.res.ApkAssets.class,
+            android.content.res.AssetManager.class,
+            android.content.res.StringBlock.class,
+            android.content.res.XmlBlock.class,
+    };
+
+    /**
+     * Classes with native methods that are backed by libhwui.
+     *
+     * See frameworks/base/libs/hwui/apex/LayoutlibLoader.cpp
+     */
+    private static final Class<?>[] sLibhwuiClasses = {
+            android.graphics.Interpolator.class,
+            android.graphics.Matrix.class,
+            android.graphics.Path.class,
+            android.graphics.Color.class,
+            android.graphics.ColorSpace.class,
+    };
+
+    /**
+     * Extra strings needed to pass to register_android_graphics_classes().
+     *
+     * `android.graphics.Graphics` is not actually a class, so we just hardcode it here.
+     */
+    public final static String[] GRAPHICS_EXTRA_INIT_PARAMS = new String[] {
+            "android.graphics.Graphics"
+    };
+
+    private RavenwoodNativeLoader() {
+    }
+
+    private static void log(String message) {
+        System.out.println("RavenwoodNativeLoader: " + message);
+    }
+
+    private static void log(String fmt, Object... args) {
+        log(String.format(fmt, args));
+    }
+
+    private static void ensurePropertyNotSet(String key) {
+        if (System.getProperty(key) != null) {
+            throw new RuntimeException("System property \"" + key + "\" is set unexpectedly");
+        }
+    }
+
+    private static void setProperty(String key, String value) {
+        System.setProperty(key, value);
+        log("Property set: %s=\"%s\"", key, value);
+    }
+
+    private static void dumpSystemProperties() {
+        for (var prop : System.getProperties().entrySet()) {
+            log("  %s=\"%s\"", prop.getKey(), prop.getValue());
+        }
+    }
+
+    /**
+     * libandroid_runtime uses Java's system properties to decide what JNI methods to set up.
+     * Set up these properties and load the native library
+     */
+    public static void loadFrameworkNativeCode() {
+        if ("1".equals(System.getenv("RAVENWOOD_DUMP_PROPERTIES"))) {
+            log("Java system properties:");
+            dumpSystemProperties();
+        }
+
+        // Make sure these properties are not set.
+        ensurePropertyNotSet(CORE_NATIVE_CLASSES);
+        ensurePropertyNotSet(ICU_DATA_PATH);
+        ensurePropertyNotSet(KEYBOARD_PATHS);
+        ensurePropertyNotSet(GRAPHICS_NATIVE_CLASSES);
+
+        // Build the property values
+        final var joiner = Collectors.joining(",");
+        final var libandroidClasses =
+                Arrays.stream(sLibandroidClasses).map(Class::getName).collect(joiner);
+        final var libhwuiClasses = Stream.concat(
+                Arrays.stream(sLibhwuiClasses).map(Class::getName),
+                Arrays.stream(GRAPHICS_EXTRA_INIT_PARAMS)
+        ).collect(joiner);
+
+        // Load the libraries
+        setProperty(CORE_NATIVE_CLASSES, libandroidClasses);
+        setProperty(GRAPHICS_NATIVE_CLASSES, libhwuiClasses);
+        log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libandroidClasses + "' and '"
+                + libhwuiClasses + "'");
+        RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
deleted file mode 100644
index 4357f2b..0000000
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ /dev/null
@@ -1,319 +0,0 @@
-/*
- * Copyright (C) 2023 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 android.platform.test.ravenwood;
-
-import static org.junit.Assert.assertFalse;
-
-import android.app.ActivityManager;
-import android.app.Instrumentation;
-import android.os.Build;
-import android.os.Bundle;
-import android.os.HandlerThread;
-import android.os.Looper;
-import android.os.ServiceManager;
-import android.util.Log;
-
-import androidx.test.platform.app.InstrumentationRegistry;
-
-import com.android.internal.os.RuntimeInit;
-import com.android.server.LocalServices;
-
-import org.junit.After;
-import org.junit.AfterClass;
-import org.junit.Assert;
-import org.junit.Before;
-import org.junit.BeforeClass;
-import org.junit.Test;
-import org.junit.runner.Description;
-import org.junit.runner.RunWith;
-import org.junit.runners.model.Statement;
-
-import java.io.PrintStream;
-import java.lang.annotation.Annotation;
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-import java.util.Objects;
-import java.util.concurrent.Executors;
-import java.util.concurrent.ScheduledExecutorService;
-import java.util.concurrent.ScheduledFuture;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicReference;
-
-public class RavenwoodRuleImpl {
-    private static final String MAIN_THREAD_NAME = "RavenwoodMain";
-
-    /**
-     * When enabled, attempt to dump all thread stacks just before we hit the
-     * overall Tradefed timeout, to aid in debugging deadlocks.
-     */
-    private static final boolean ENABLE_TIMEOUT_STACKS = false;
-    private static final int TIMEOUT_MILLIS = 9_000;
-
-    private static final ScheduledExecutorService sTimeoutExecutor =
-            Executors.newScheduledThreadPool(1);
-
-    private static ScheduledFuture<?> sPendingTimeout;
-
-    /**
-     * When enabled, attempt to detect uncaught exceptions from background threads.
-     */
-    private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION = false;
-
-    /**
-     * When set, an unhandled exception was discovered (typically on a background thread), and we
-     * capture it here to ensure it's reported as a test failure.
-     */
-    private static final AtomicReference<Throwable> sPendingUncaughtException =
-            new AtomicReference<>();
-
-    private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
-            (thread, throwable) -> {
-                // Remember the first exception we discover
-                sPendingUncaughtException.compareAndSet(null, throwable);
-            };
-
-    public static void init(RavenwoodRule rule) {
-        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
-            maybeThrowPendingUncaughtException(false);
-            Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
-        }
-
-        RuntimeInit.redirectLogStreams();
-
-        android.os.Process.init$ravenwood(rule.mUid, rule.mPid);
-        android.os.Binder.init$ravenwood();
-//        android.os.SystemProperties.init$ravenwood(
-//                rule.mSystemProperties.getValues(),
-//                rule.mSystemProperties.getKeyReadablePredicate(),
-//                rule.mSystemProperties.getKeyWritablePredicate());
-        setSystemProperties(rule.mSystemProperties);
-
-        ServiceManager.init$ravenwood();
-        LocalServices.removeAllServicesForTest();
-
-        ActivityManager.init$ravenwood(rule.mCurrentUser);
-
-        final HandlerThread main;
-        if (rule.mProvideMainThread) {
-            main = new HandlerThread(MAIN_THREAD_NAME);
-            main.start();
-            Looper.setMainLooperForTest(main.getLooper());
-        } else {
-            main = null;
-        }
-
-        rule.mContext = new RavenwoodContext(rule.mPackageName, main);
-        rule.mInstrumentation = new Instrumentation();
-        rule.mInstrumentation.basicInit(rule.mContext);
-        InstrumentationRegistry.registerInstance(rule.mInstrumentation, Bundle.EMPTY);
-
-        RavenwoodSystemServer.init(rule);
-
-        if (ENABLE_TIMEOUT_STACKS) {
-            sPendingTimeout = sTimeoutExecutor.schedule(RavenwoodRuleImpl::dumpStacks,
-                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
-        }
-
-        // Touch some references early to ensure they're <clinit>'ed
-        Objects.requireNonNull(Build.TYPE);
-        Objects.requireNonNull(Build.VERSION.SDK);
-    }
-
-    public static void reset(RavenwoodRule rule) {
-        if (ENABLE_TIMEOUT_STACKS) {
-            sPendingTimeout.cancel(false);
-        }
-
-        RavenwoodSystemServer.reset(rule);
-
-        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
-        rule.mInstrumentation = null;
-        rule.mContext = null;
-
-        if (rule.mProvideMainThread) {
-            Looper.getMainLooper().quit();
-            Looper.clearMainLooperForTest();
-        }
-
-        ActivityManager.reset$ravenwood();
-
-        LocalServices.removeAllServicesForTest();
-        ServiceManager.reset$ravenwood();
-
-        setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
-        android.os.Binder.reset$ravenwood();
-        android.os.Process.reset$ravenwood();
-
-        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
-            maybeThrowPendingUncaughtException(true);
-        }
-    }
-
-    public static void logTestRunner(String label, Description description) {
-        // This message string carefully matches the exact format emitted by on-device tests, to
-        // aid developers in debugging raw text logs
-        Log.e("TestRunner", label + ": " + description.getMethodName()
-                + "(" + description.getTestClass().getName() + ")");
-    }
-
-    private static void dumpStacks() {
-        final PrintStream out = System.err;
-        out.println("-----BEGIN ALL THREAD STACKS-----");
-        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
-        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
-            out.println();
-            Thread t = stack.getKey();
-            out.println(t.toString() + " ID=" + t.getId());
-            for (StackTraceElement e : stack.getValue()) {
-                out.println("\tat " + e);
-            }
-        }
-        out.println("-----END ALL THREAD STACKS-----");
-    }
-
-    /**
-     * If there's a pending uncaught exception, consume and throw it now. Typically used to
-     * report an exception on a background thread as a failure for the currently running test.
-     */
-    private static void maybeThrowPendingUncaughtException(boolean duringReset) {
-        final Throwable pending = sPendingUncaughtException.getAndSet(null);
-        if (pending != null) {
-            if (duringReset) {
-                throw new IllegalStateException(
-                        "Found an uncaught exception during this test", pending);
-            } else {
-                throw new IllegalStateException(
-                        "Found an uncaught exception before this test started", pending);
-            }
-        }
-    }
-
-    public static void validate(Statement base, Description description,
-            boolean enableOptionalValidation) {
-        validateTestRunner(base, description, enableOptionalValidation);
-        validateTestAnnotations(base, description, enableOptionalValidation);
-    }
-
-    private static void validateTestRunner(Statement base, Description description,
-            boolean shouldFail) {
-        final var testClass = description.getTestClass();
-        final var runWith = testClass.getAnnotation(RunWith.class);
-        if (runWith == null) {
-            return;
-        }
-
-        // Due to build dependencies, we can't directly refer to androidx classes here,
-        // so just check the class name instead.
-        if (runWith.value().getCanonicalName().equals("androidx.test.runner.AndroidJUnit4")) {
-            var message = "Test " + testClass.getCanonicalName() + " uses deprecated"
-                    + " test runner androidx.test.runner.AndroidJUnit4."
-                    + " Switch to androidx.test.ext.junit.runners.AndroidJUnit4.";
-            if (shouldFail) {
-                Assert.fail(message);
-            } else {
-                System.err.println("Warning: " + message);
-            }
-        }
-    }
-
-    /**
-     * @return if a method has any of annotations.
-     */
-    private static boolean hasAnyAnnotations(Method m, Class<? extends Annotation>... annotations) {
-        for (var anno : annotations) {
-            if (m.getAnnotation(anno) != null) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    private static void validateTestAnnotations(Statement base, Description description,
-            boolean enableOptionalValidation) {
-        final var testClass = description.getTestClass();
-
-        final var message = new StringBuilder();
-
-        boolean hasErrors = false;
-        for (Method m : collectMethods(testClass)) {
-            if (Modifier.isPublic(m.getModifiers()) && m.getName().startsWith("test")) {
-                if (!hasAnyAnnotations(m, Test.class, Before.class, After.class,
-                        BeforeClass.class, AfterClass.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @Test");
-                    hasErrors = true;
-                }
-            }
-            if ("setUp".equals(m.getName())) {
-                if (!hasAnyAnnotations(m, Before.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @Before");
-                    hasErrors = true;
-                }
-                if (!Modifier.isPublic(m.getModifiers())) {
-                    message.append("\nMethod " + m.getName() + "() must be public");
-                    hasErrors = true;
-                }
-            }
-            if ("tearDown".equals(m.getName())) {
-                if (!hasAnyAnnotations(m, After.class)) {
-                    message.append("\nMethod " + m.getName() + "() doesn't have @After");
-                    hasErrors = true;
-                }
-                if (!Modifier.isPublic(m.getModifiers())) {
-                    message.append("\nMethod " + m.getName() + "() must be public");
-                    hasErrors = true;
-                }
-            }
-        }
-        assertFalse("Problem(s) detected in class " + testClass.getCanonicalName() + ":"
-                + message, hasErrors);
-    }
-
-    /**
-     * Collect all (public or private or any) methods in a class, including inherited methods.
-     */
-    private static List<Method> collectMethods(Class<?> clazz) {
-        var ret = new ArrayList<Method>();
-        collectMethods(clazz, ret);
-        return ret;
-    }
-
-    private static void collectMethods(Class<?> clazz, List<Method> result) {
-        // Class.getMethods() only return public methods, so we need to use getDeclaredMethods()
-        // instead, and recurse.
-        for (var m : clazz.getDeclaredMethods()) {
-            result.add(m);
-        }
-        if (clazz.getSuperclass() != null) {
-            collectMethods(clazz.getSuperclass(), result);
-        }
-    }
-
-    /**
-     * Set the current configuration to the actual SystemProperties.
-     */
-    public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) {
-        var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true);
-
-        android.os.SystemProperties.init$ravenwood(
-                clone.getValues(),
-                clone.getKeyReadablePredicate(),
-                clone.getKeyWritablePredicate());
-    }
-}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
new file mode 100644
index 0000000..03513ab
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicMember;
+
+import static org.junit.Assert.fail;
+
+import android.annotation.Nullable;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.ravenwood.common.RavenwoodRuntimeException;
+
+import org.junit.ClassRule;
+import org.junit.Rule;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.WeakHashMap;
+
+/**
+ * Used to store various states associated with the current test runner that's inly needed
+ * in junit-impl.
+ *
+ * We don't want to put it in junit-src to avoid having to recompile all the downstream
+ * dependencies after changing this class.
+ *
+ * All members must be called from the runner's main thread.
+ */
+public final class RavenwoodRunnerState {
+    private static final String TAG = "RavenwoodRunnerState";
+
+    @GuardedBy("sStates")
+    private static final WeakHashMap<RavenwoodAwareTestRunner, RavenwoodRunnerState> sStates =
+            new WeakHashMap<>();
+
+    private final RavenwoodAwareTestRunner mRunner;
+
+    /**
+     * Ctor.
+     */
+    public RavenwoodRunnerState(RavenwoodAwareTestRunner runner) {
+        mRunner = runner;
+    }
+
+    private Description mClassDescription;
+    private Description mMethodDescription;
+
+    private RavenwoodConfig mCurrentConfig;
+    private RavenwoodRule mCurrentRule;
+    private boolean mHasRavenwoodRule;
+
+    public Description getClassDescription() {
+        return mClassDescription;
+    }
+
+    public void enterTestClass(Description classDescription) throws IOException {
+        mClassDescription = classDescription;
+
+        mHasRavenwoodRule = hasRavenwoodRule(mRunner.getTestClass().getJavaClass());
+        mCurrentConfig = extractConfiguration(mRunner.getTestClass().getJavaClass());
+
+        if (mCurrentConfig != null) {
+            RavenwoodRuntimeEnvironmentController.init(mCurrentConfig);
+        }
+    }
+
+    public void exitTestClass() {
+        if (mCurrentConfig != null) {
+            try {
+                RavenwoodRuntimeEnvironmentController.reset();
+            } finally {
+                mClassDescription = null;
+            }
+        }
+    }
+
+    public void enterTestMethod(Description description) {
+        mMethodDescription = description;
+    }
+
+    public void exitTestMethod() {
+        mMethodDescription = null;
+    }
+
+    public void enterRavenwoodRule(RavenwoodRule rule) throws IOException {
+        if (!mHasRavenwoodRule) {
+            fail("If you have a RavenwoodRule in your test, make sure the field type is"
+                    + " RavenwoodRule so Ravenwood can detect it.");
+        }
+        if (mCurrentConfig != null) {
+            fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
+                    + " Suggest migrating to RavenwoodConfig.");
+        }
+        if (mCurrentRule != null) {
+            fail("Multiple nesting RavenwoodRule's are detected in the same class,"
+                    + " which is not supported.");
+        }
+        mCurrentRule = rule;
+        RavenwoodRuntimeEnvironmentController.init(rule.getConfiguration());
+    }
+
+    public void exitRavenwoodRule(RavenwoodRule rule) {
+        if (mCurrentRule != rule) {
+            return; // This happens if the rule did _not_ take effect somehow.
+        }
+
+        try {
+            RavenwoodRuntimeEnvironmentController.reset();
+        } finally {
+            mCurrentRule = null;
+        }
+    }
+
+    /**
+     * @return a configuration from a test class, if any.
+     */
+    @Nullable
+    private RavenwoodConfig extractConfiguration(Class<?> testClass) {
+        var field = findConfigurationField(testClass);
+        if (field == null) {
+            if (mHasRavenwoodRule) {
+                // Should be handled by RavenwoodRule
+                return null;
+            }
+
+            // If no RavenwoodConfig and no RavenwoodRule, return a default config
+            return new RavenwoodConfig.Builder().build();
+        }
+        if (mHasRavenwoodRule) {
+            fail("RavenwoodConfig and RavenwoodRule cannot be used in the same class."
+                    + " Suggest migrating to RavenwoodConfig.");
+        }
+
+        try {
+            return (RavenwoodConfig) field.get(null);
+        } catch (IllegalAccessException e) {
+            throw new RavenwoodRuntimeException("Failed to fetch from the configuration field", e);
+        }
+    }
+
+    /**
+     * @return true if the current target class (or its super classes) has any @Rule / @ClassRule
+     * fields of type RavenwoodRule.
+     *
+     * Note, this check won't detect cases where a Rule is of type
+     * {@link TestRule} and still be a {@link RavenwoodRule}. But that'll be detected at runtime
+     * as a failure, in {@link #enterRavenwoodRule}.
+     */
+    private static boolean hasRavenwoodRule(Class<?> testClass) {
+        for (var field : testClass.getDeclaredFields()) {
+            if (!field.isAnnotationPresent(Rule.class)
+                    && !field.isAnnotationPresent(ClassRule.class)) {
+                continue;
+            }
+            if (field.getType().equals(RavenwoodRule.class)) {
+                return true;
+            }
+        }
+        // JUnit supports rules as methods, so we need to check them too.
+        for (var method : testClass.getDeclaredMethods()) {
+            if (!method.isAnnotationPresent(Rule.class)
+                    && !method.isAnnotationPresent(ClassRule.class)) {
+                continue;
+            }
+            if (method.getReturnType().equals(RavenwoodRule.class)) {
+                return true;
+            }
+        }
+        // Look into the super class.
+        if (!testClass.getSuperclass().equals(Object.class)) {
+            return hasRavenwoodRule(testClass.getSuperclass());
+        }
+        return false;
+    }
+
+    /**
+     * Find and return a field with @RavenwoodConfig.Config, which must be of type
+     * RavenwoodConfig.
+     */
+    @Nullable
+    private static Field findConfigurationField(Class<?> testClass) {
+        Field foundField = null;
+
+        for (var field : testClass.getDeclaredFields()) {
+            final var hasAnot = field.isAnnotationPresent(RavenwoodConfig.Config.class);
+            final var isType = field.getType().equals(RavenwoodConfig.class);
+
+            if (hasAnot) {
+                if (isType) {
+                    // Good, use this field.
+                    if (foundField != null) {
+                        fail(String.format(
+                                "Class %s has multiple fields with %s",
+                                testClass.getCanonicalName(),
+                                "@RavenwoodConfig.Config"));
+                    }
+                    // Make sure it's static public
+                    ensureIsPublicMember(field, true);
+
+                    foundField = field;
+                } else {
+                    fail(String.format(
+                            "Field %s.%s has %s but type is not %s",
+                            testClass.getCanonicalName(),
+                            field.getName(),
+                            "@RavenwoodConfig.Config",
+                            "RavenwoodConfig"));
+                    return null; // unreachable
+                }
+            } else {
+                if (isType) {
+                    fail(String.format(
+                            "Field %s.%s does not have %s but type is %s",
+                            testClass.getCanonicalName(),
+                            field.getName(),
+                            "@RavenwoodConfig.Config",
+                            "RavenwoodConfig"));
+                    return null; // unreachable
+                } else {
+                    // Unrelated field, ignore.
+                    continue;
+                }
+            }
+        }
+        if (foundField != null) {
+            return foundField;
+        }
+        if (!testClass.getSuperclass().equals(Object.class)) {
+            return findConfigurationField(testClass.getSuperclass());
+        }
+        return null;
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
new file mode 100644
index 0000000..40b14db
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuntimeEnvironmentController.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2023 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_INST_RESOURCE_APK;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_RESOURCE_APK;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERSION_JAVA_SYSPROP;
+
+import android.app.ActivityManager;
+import android.app.Instrumentation;
+import android.app.ResourcesManager;
+import android.content.res.Resources;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.ServiceManager;
+import android.system.ErrnoException;
+import android.system.Os;
+import android.util.Log;
+
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.internal.os.RuntimeInit;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+import com.android.ravenwood.common.RavenwoodRuntimeException;
+import com.android.ravenwood.common.SneakyThrow;
+import com.android.server.LocalServices;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintStream;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.function.Supplier;
+
+/**
+ * Responsible for initializing and de-initializing the environment, according to a
+ * {@link RavenwoodConfig}.
+ */
+public class RavenwoodRuntimeEnvironmentController {
+    private static final String TAG = "RavenwoodRuntimeEnvironmentController";
+
+    private RavenwoodRuntimeEnvironmentController() {
+    }
+
+    private static final String MAIN_THREAD_NAME = "RavenwoodMain";
+
+    /**
+     * When enabled, attempt to dump all thread stacks just before we hit the
+     * overall Tradefed timeout, to aid in debugging deadlocks.
+     */
+    private static final boolean ENABLE_TIMEOUT_STACKS =
+            "1".equals(System.getenv("RAVENWOOD_ENABLE_TIMEOUT_STACKS"));
+
+    private static final int TIMEOUT_MILLIS = 9_000;
+
+    private static final ScheduledExecutorService sTimeoutExecutor =
+            Executors.newScheduledThreadPool(1);
+
+    private static ScheduledFuture<?> sPendingTimeout;
+
+    private static long sOriginalIdentityToken = -1;
+
+    /**
+     * When enabled, attempt to detect uncaught exceptions from background threads.
+     */
+    private static final boolean ENABLE_UNCAUGHT_EXCEPTION_DETECTION =
+            "1".equals(System.getenv("RAVENWOOD_ENABLE_UNCAUGHT_EXCEPTION_DETECTION"));
+
+    /**
+     * When set, an unhandled exception was discovered (typically on a background thread), and we
+     * capture it here to ensure it's reported as a test failure.
+     */
+    private static final AtomicReference<Throwable> sPendingUncaughtException =
+            new AtomicReference<>();
+
+    private static final Thread.UncaughtExceptionHandler sUncaughtExceptionHandler =
+            (thread, throwable) -> {
+                // Remember the first exception we discover
+                sPendingUncaughtException.compareAndSet(null, throwable);
+            };
+
+    // TODO: expose packCallingIdentity function in libbinder and use it directly
+    // See: packCallingIdentity in frameworks/native/libs/binder/IPCThreadState.cpp
+    private static long packBinderIdentityToken(
+            boolean hasExplicitIdentity, int callingUid, int callingPid) {
+        long res = ((long) callingUid << 32) | callingPid;
+        if (hasExplicitIdentity) {
+            res |= (0x1 << 30);
+        } else {
+            res &= ~(0x1 << 30);
+        }
+        return res;
+    }
+
+    private static RavenwoodConfig sConfig;
+    private static boolean sInitialized = false;
+
+    /**
+     * Initialize the global environment.
+     */
+    public static void globalInitOnce() {
+        if (sInitialized) {
+            return;
+        }
+        sInitialized = true;
+
+        // We haven't initialized liblog yet, so directly write to System.out here.
+        RavenwoodCommonUtils.log(TAG, "globalInit()");
+
+        // Do the basic set up for the android sysprops.
+        setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
+
+        // Make sure libandroid_runtime is loaded.
+        RavenwoodNativeLoader.loadFrameworkNativeCode();
+
+        // Redirect stdout/stdin to liblog.
+        RuntimeInit.redirectLogStreams();
+
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            RavenwoodCommonUtils.log(TAG, "Force enabling verbose logging");
+            try {
+                Os.setenv("ANDROID_LOG_TAGS", "*:v", true);
+            } catch (ErrnoException e) {
+                // Shouldn't happen.
+            }
+        }
+
+        System.setProperty(RAVENWOOD_VERSION_JAVA_SYSPROP, "1");
+        // This will let AndroidJUnit4 use the original runner.
+        System.setProperty("android.junit.runner",
+                "androidx.test.internal.runner.junit4.AndroidJUnit4ClassRunner");
+    }
+
+    /**
+     * Initialize the environment.
+     */
+    public static void init(RavenwoodConfig config) throws IOException {
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            Log.i(TAG, "init() called here", new RuntimeException("STACKTRACE"));
+        }
+        try {
+            initInner(config);
+        } catch (Exception th) {
+            Log.e(TAG, "init() failed", th);
+            reset();
+            SneakyThrow.sneakyThrow(th);
+        }
+    }
+
+    private static void initInner(RavenwoodConfig config) throws IOException {
+        if (sConfig != null) {
+            throw new RavenwoodRuntimeException("Internal error: init() called without reset()");
+        }
+        sConfig = config;
+        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+            maybeThrowPendingUncaughtException(false);
+            Thread.setDefaultUncaughtExceptionHandler(sUncaughtExceptionHandler);
+        }
+
+        android.os.Process.init$ravenwood(config.mUid, config.mPid);
+        sOriginalIdentityToken = Binder.clearCallingIdentity();
+        Binder.restoreCallingIdentity(packBinderIdentityToken(false, config.mUid, config.mPid));
+        setSystemProperties(config.mSystemProperties);
+
+        ServiceManager.init$ravenwood();
+        LocalServices.removeAllServicesForTest();
+
+        ActivityManager.init$ravenwood(config.mCurrentUser);
+
+        final HandlerThread main;
+        if (config.mProvideMainThread) {
+            main = new HandlerThread(MAIN_THREAD_NAME);
+            main.start();
+            Looper.setMainLooperForTest(main.getLooper());
+        } else {
+            main = null;
+        }
+
+        final boolean isSelfInstrumenting =
+                Objects.equals(config.mTestPackageName, config.mTargetPackageName);
+
+        // This will load the resources from the apk set to `resource_apk` in the build file.
+        // This is supposed to be the "target app"'s resources.
+        final Supplier<Resources> targetResourcesLoader = () -> {
+            var file = new File(RAVENWOOD_RESOURCE_APK);
+            return config.mState.loadResources(file.exists() ? file : null);
+        };
+
+        // Set up test context's (== instrumentation context's) resources.
+        // If the target package name == test package name, then we use the main resources.
+        final Supplier<Resources> instResourcesLoader;
+        if (isSelfInstrumenting) {
+            instResourcesLoader = targetResourcesLoader;
+        } else {
+            instResourcesLoader = () -> {
+                var file = new File(RAVENWOOD_INST_RESOURCE_APK);
+                return config.mState.loadResources(file.exists() ? file : null);
+            };
+        }
+
+        var instContext = new RavenwoodContext(
+                config.mTestPackageName, main, instResourcesLoader);
+        var targetContext = new RavenwoodContext(
+                config.mTargetPackageName, main, targetResourcesLoader);
+
+        // Set up app context.
+        var appContext = new RavenwoodContext(
+                config.mTargetPackageName, main, targetResourcesLoader);
+        appContext.setApplicationContext(appContext);
+        if (isSelfInstrumenting) {
+            instContext.setApplicationContext(appContext);
+            targetContext.setApplicationContext(appContext);
+        } else {
+            // When instrumenting into another APK, the test context doesn't have an app context.
+            targetContext.setApplicationContext(appContext);
+        }
+        config.mInstContext = instContext;
+        config.mTargetContext = targetContext;
+
+        // Prepare other fields.
+        config.mInstrumentation = new Instrumentation();
+        config.mInstrumentation.basicInit(config.mInstContext, config.mTargetContext);
+        InstrumentationRegistry.registerInstance(config.mInstrumentation, Bundle.EMPTY);
+
+        RavenwoodSystemServer.init(config);
+
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout = sTimeoutExecutor.schedule(
+                    RavenwoodRuntimeEnvironmentController::dumpStacks,
+                    TIMEOUT_MILLIS, TimeUnit.MILLISECONDS);
+        }
+
+        // Touch some references early to ensure they're <clinit>'ed
+        Objects.requireNonNull(Build.TYPE);
+        Objects.requireNonNull(Build.VERSION.SDK);
+    }
+
+    /**
+     * De-initialize.
+     */
+    public static void reset() {
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            Log.i(TAG, "reset() called here", new RuntimeException("STACKTRACE"));
+        }
+        if (sConfig == null) {
+            throw new RavenwoodRuntimeException("Internal error: reset() already called");
+        }
+        var config = sConfig;
+        sConfig = null;
+
+        if (ENABLE_TIMEOUT_STACKS) {
+            sPendingTimeout.cancel(false);
+        }
+
+        RavenwoodSystemServer.reset(config);
+
+        InstrumentationRegistry.registerInstance(null, Bundle.EMPTY);
+        config.mInstrumentation = null;
+        if (config.mInstContext != null) {
+            ((RavenwoodContext) config.mInstContext).cleanUp();
+        }
+        if (config.mTargetContext != null) {
+            ((RavenwoodContext) config.mTargetContext).cleanUp();
+        }
+        config.mInstContext = null;
+        config.mTargetContext = null;
+
+        if (config.mProvideMainThread) {
+            Looper.getMainLooper().quit();
+            Looper.clearMainLooperForTest();
+        }
+
+        ActivityManager.reset$ravenwood();
+
+        LocalServices.removeAllServicesForTest();
+        ServiceManager.reset$ravenwood();
+
+        setSystemProperties(RavenwoodSystemProperties.DEFAULT_VALUES);
+        if (sOriginalIdentityToken != -1) {
+            Binder.restoreCallingIdentity(sOriginalIdentityToken);
+        }
+        android.os.Process.reset$ravenwood();
+
+        try {
+            ResourcesManager.setInstance(null); // Better structure needed.
+        } catch (Exception e) {
+            // AOSP-CHANGE: AOSP doesn't support resources yet.
+        }
+
+        if (ENABLE_UNCAUGHT_EXCEPTION_DETECTION) {
+            maybeThrowPendingUncaughtException(true);
+        }
+    }
+
+    public static void logTestRunner(String label, Description description) {
+        // This message string carefully matches the exact format emitted by on-device tests, to
+        // aid developers in debugging raw text logs
+        Log.e("TestRunner", label + ": " + description.getMethodName()
+                + "(" + description.getTestClass().getName() + ")");
+    }
+
+    private static void dumpStacks() {
+        final PrintStream out = System.err;
+        out.println("-----BEGIN ALL THREAD STACKS-----");
+        final Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
+        for (Map.Entry<Thread, StackTraceElement[]> stack : stacks.entrySet()) {
+            out.println();
+            Thread t = stack.getKey();
+            out.println(t.toString() + " ID=" + t.getId());
+            for (StackTraceElement e : stack.getValue()) {
+                out.println("\tat " + e);
+            }
+        }
+        out.println("-----END ALL THREAD STACKS-----");
+    }
+
+    /**
+     * If there's a pending uncaught exception, consume and throw it now. Typically used to
+     * report an exception on a background thread as a failure for the currently running test.
+     */
+    private static void maybeThrowPendingUncaughtException(boolean duringReset) {
+        final Throwable pending = sPendingUncaughtException.getAndSet(null);
+        if (pending != null) {
+            if (duringReset) {
+                throw new IllegalStateException(
+                        "Found an uncaught exception during this test", pending);
+            } else {
+                throw new IllegalStateException(
+                        "Found an uncaught exception before this test started", pending);
+            }
+        }
+    }
+
+    /**
+     * Set the current configuration to the actual SystemProperties.
+     */
+    public static void setSystemProperties(RavenwoodSystemProperties ravenwoodSystemProperties) {
+        var clone = new RavenwoodSystemProperties(ravenwoodSystemProperties, true);
+
+        android.os.SystemProperties.init$ravenwood(
+                clone.getValues(),
+                clone.getKeyReadablePredicate(),
+                clone.getKeyWritablePredicate());
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
index cd6b61d..3946dd84 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -61,19 +61,19 @@
     private static TimingsTraceAndSlog sTimings;
     private static SystemServiceManager sServiceManager;
 
-    public static void init(RavenwoodRule rule) {
+    public static void init(RavenwoodConfig config) {
         // Avoid overhead if no services required
-        if (rule.mServicesRequired.isEmpty()) return;
+        if (config.mServicesRequired.isEmpty()) return;
 
         sStartedServices = new ArraySet<>();
         sTimings = new TimingsTraceAndSlog();
-        sServiceManager = new SystemServiceManager(rule.mContext);
+        sServiceManager = new SystemServiceManager(config.mInstContext);
         sServiceManager.setStartInfo(false,
                 SystemClock.elapsedRealtime(),
                 SystemClock.uptimeMillis());
         LocalServices.addService(SystemServiceManager.class, sServiceManager);
 
-        startServices(rule.mServicesRequired);
+        startServices(config.mServicesRequired);
         sServiceManager.sealStartedServices();
 
         // TODO: expand to include additional boot phases when relevant
@@ -81,7 +81,7 @@
         sServiceManager.startBootPhase(sTimings, SystemService.PHASE_BOOT_COMPLETED);
     }
 
-    public static void reset(RavenwoodRule rule) {
+    public static void reset(RavenwoodConfig config) {
         // TODO: consider introducing shutdown boot phases
 
         LocalServices.removeServiceForTest(SystemServiceManager.class);
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
new file mode 100644
index 0000000..428eb57
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodTestStats.java
@@ -0,0 +1,191 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import android.util.Log;
+
+import org.junit.runner.Description;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Collect test result stats and write them into a CSV file containing the test results.
+ *
+ * The output file is created as `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_[TIMESTAMP].csv`.
+ * A symlink to the latest result will be created as
+ * `/tmp/Ravenwood-stats_[TEST-MODULE=NAME]_latest.csv`.
+ */
+public class RavenwoodTestStats {
+    private static final String TAG = "RavenwoodTestStats";
+    private static final String HEADER = "Module,Class,ClassDesc,Passed,Failed,Skipped";
+
+    private static RavenwoodTestStats sInstance;
+
+    /**
+     * @return a singleton instance.
+     */
+    public static RavenwoodTestStats getInstance() {
+        if (sInstance == null) {
+            sInstance = new RavenwoodTestStats();
+        }
+        return sInstance;
+    }
+
+    /**
+     * Represents a test result.
+     */
+    public enum Result {
+        Passed,
+        Failed,
+        Skipped,
+    }
+
+    private final File mOutputFile;
+    private final PrintWriter mOutputWriter;
+    private final String mTestModuleName;
+
+    public final Map<Description, Map<Description, Result>> mStats = new HashMap<>();
+
+    /** Ctor */
+    public RavenwoodTestStats() {
+        mTestModuleName = guessTestModuleName();
+
+        var basename = "Ravenwood-stats_" + mTestModuleName + "_";
+
+        // Get the current time
+        LocalDateTime now = LocalDateTime.now();
+        DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMdd-HHmmss");
+
+        var tmpdir = System.getProperty("java.io.tmpdir");
+        mOutputFile = new File(tmpdir, basename + now.format(fmt) + ".csv");
+
+        try {
+            mOutputWriter = new PrintWriter(mOutputFile);
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+        }
+
+        // Crete the "latest" symlink.
+        Path symlink = Paths.get(tmpdir, basename + "latest.csv");
+        try {
+            if (Files.exists(symlink)) {
+                Files.delete(symlink);
+            }
+            Files.createSymbolicLink(symlink, Paths.get(mOutputFile.getName()));
+
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to crete logfile. File=" + mOutputFile, e);
+        }
+
+        Log.i(TAG, "Test result stats file: " + mOutputFile);
+
+        // Print the header.
+        mOutputWriter.println(HEADER);
+        mOutputWriter.flush();
+    }
+
+    private String guessTestModuleName() {
+        // Assume the current directory name is the test module name.
+        File cwd;
+        try {
+            cwd = new File(".").getCanonicalFile();
+        } catch (IOException e) {
+            throw new RuntimeException("Failed to get the current directory", e);
+        }
+        return cwd.getName();
+    }
+
+    private void addResult(Description classDescription, Description methodDescription,
+            Result result) {
+        mStats.compute(classDescription, (classDesc, value) -> {
+            if (value == null) {
+                value = new HashMap<>();
+            }
+            value.put(methodDescription, result);
+            return value;
+        });
+    }
+
+    /**
+     * Call it when a test class is skipped.
+     */
+    public void onClassSkipped(Description classDescription) {
+        addResult(classDescription, Description.EMPTY, Result.Skipped);
+        onClassFinished(classDescription);
+    }
+
+    /**
+     * Call it when a test method is finished.
+     */
+    public void onTestFinished(Description classDescription, Description testDescription,
+            Result result) {
+        addResult(classDescription, testDescription, result);
+    }
+
+    /**
+     * Call it when a test class is finished.
+     */
+    public void onClassFinished(Description classDescription) {
+        int passed = 0;
+        int skipped = 0;
+        int failed = 0;
+        var stats = mStats.get(classDescription);
+        if (stats == null) {
+            return;
+        }
+        for (var e : stats.values()) {
+            switch (e) {
+                case Passed: passed++; break;
+                case Skipped: skipped++; break;
+                case Failed: failed++; break;
+            }
+        }
+
+        var testClass = extractTestClass(classDescription);
+
+        mOutputWriter.printf("%s,%s,%s,%d,%d,%d\n",
+                mTestModuleName, (testClass == null ? "?" : testClass.getCanonicalName()),
+                classDescription, passed, failed, skipped);
+        mOutputWriter.flush();
+    }
+
+    /**
+     * Try to extract the class from a description, which is needed because
+     * ParameterizedAndroidJunit4's description doesn't contain a class.
+     */
+    private Class<?> extractTestClass(Description desc) {
+        if (desc.getTestClass() != null) {
+            return desc.getTestClass();
+        }
+        // Look into the children.
+        for (var child : desc.getChildren()) {
+            var fromChild = extractTestClass(child);
+            if (fromChild != null) {
+                return fromChild;
+            }
+        }
+        return null;
+    }
+}
diff --git a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java b/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
deleted file mode 100644
index 2fb8074..0000000
--- a/ravenwood/junit-src/android/platform/test/annotations/DisabledOnNonRavenwood.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2024 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 android.platform.test.annotations;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Inherited;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Tests marked with this annotation are only executed when running on Ravenwood, but not
- * on a device.
- *
- * This is basically equivalent to the opposite of {@link DisabledOnRavenwood}, but in order to
- * avoid complex structure, and there's no equivalent to the opposite {@link EnabledOnRavenwood},
- * which means if a test class has this annotation, you can't negate it in subclasses or
- * on a per-method basis.
- *
- * THIS ANNOTATION CANNOT BE ADDED TO CLASSES AT THIS PONINT.
- * See {@link com.android.ravenwoodtest.bivalenttest.RavenwoodClassRuleRavenwoodOnlyTest}
- * for the reason.
- *
- * The {@code RAVENWOOD_RUN_DISABLED_TESTS} environmental variable won't work because it won't be
- * propagated to the device. (We may support it in the future, possibly using a debug. sysprop.)
- *
- * @hide
- */
-@Inherited
-@Target({ElementType.METHOD})
-@Retention(RetentionPolicy.RUNTIME)
-public @interface DisabledOnNonRavenwood {
-    /**
-     * General free-form description of why this test is being ignored.
-     */
-    String reason() default "";
-}
diff --git a/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java b/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java
new file mode 100644
index 0000000..a84f16f
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/annotations/NoRavenizer.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Disable the ravenizer preprocessor for a class. This should be only used for testing
+ * ravenizer itself, or to workaround issues with the preprocessor. A test class probably won't run
+ * properly if it's not preprocessed.
+ *
+ * @hide
+ */
+@Inherited
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+public @interface NoRavenizer {
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
new file mode 100644
index 0000000..5ba972d
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodAwareTestRunner.java
@@ -0,0 +1,733 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import static com.android.ravenwood.common.RavenwoodCommonUtils.RAVENWOOD_VERBOSE_LOGGING;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.ensureIsPublicVoidMethod;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.isOnRavenwood;
+
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.util.Log;
+
+import org.junit.Assume;
+import org.junit.AssumptionViolatedException;
+import org.junit.internal.builders.AllDefaultPossibilitiesBuilder;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.Result;
+import org.junit.runner.Runner;
+import org.junit.runner.manipulation.Filter;
+import org.junit.runner.manipulation.Filterable;
+import org.junit.runner.manipulation.InvalidOrderingException;
+import org.junit.runner.manipulation.NoTestsRemainException;
+import org.junit.runner.manipulation.Orderable;
+import org.junit.runner.manipulation.Orderer;
+import org.junit.runner.manipulation.Sortable;
+import org.junit.runner.manipulation.Sorter;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runner.notification.StoppedByUserException;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.Suite;
+import org.junit.runners.model.MultipleFailureException;
+import org.junit.runners.model.RunnerBuilder;
+import org.junit.runners.model.Statement;
+import org.junit.runners.model.TestClass;
+
+import java.lang.annotation.Annotation;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Stack;
+import java.util.function.BiConsumer;
+
+/**
+ * A test runner used for Ravenwood.
+ *
+ * It will delegate to another runner specified with {@link InnerRunner}
+ * (default = {@link BlockJUnit4ClassRunner}) with the following features.
+ * - Add a {@link RavenwoodAwareTestRunnerHook#onRunnerInitializing} hook, which is called before
+ *   the inner runner gets a chance to run. This can be used to initialize stuff used by the
+ *   inner runner.
+ * - Add hook points, which are handed by RavenwoodAwareTestRunnerHook, with help from
+ *   the four test rules such as {@link #sImplicitClassOuterRule}, which are also injected by
+ *   the ravenizer tool.
+ *
+ * We use this runner to:
+ * - Initialize the bare minimum environmnet just to be enough to make the actual test runners
+ *   happy.
+ * - Handle {@link android.platform.test.annotations.DisabledOnRavenwood}.
+ *
+ * This class is built such that it can also be used on a real device, but in that case
+ * it will basically just delegate to the inner wrapper, and won't do anything special.
+ * (no hooks, etc.)
+ */
+public final class RavenwoodAwareTestRunner extends Runner implements Filterable, Orderable {
+    public static final String TAG = "Ravenwood";
+
+    @Inherited
+    @Target({TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface InnerRunner {
+        Class<? extends Runner> value();
+    }
+
+    /**
+     * An annotation similar to JUnit's BeforeClass, but this gets executed before
+     * the inner runner is instantiated, and only on Ravenwood.
+     * It can be used to initialize what's needed by the inner runner.
+     */
+    @Target({METHOD})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface RavenwoodTestRunnerInitializing {
+    }
+
+    /** Scope of a hook. */
+    public enum Scope {
+        Class,
+        Instance,
+    }
+
+    /** Order of a hook. */
+    public enum Order {
+        Outer,
+        Inner,
+    }
+
+    // The following four rule instances will be injected to tests by the Ravenizer tool.
+    private static class RavenwoodClassOuterRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().wrapWithHooks(base, description, Scope.Class, Order.Outer);
+        }
+    }
+
+    private static class RavenwoodClassInnerRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().wrapWithHooks(base, description, Scope.Class, Order.Inner);
+        }
+    }
+
+    private static class RavenwoodInstanceOuterRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().wrapWithHooks(
+                    base, description, Scope.Instance, Order.Outer);
+        }
+    }
+
+    private static class RavenwoodInstanceInnerRule implements TestRule {
+        @Override
+        public Statement apply(Statement base, Description description) {
+            return getCurrentRunner().wrapWithHooks(
+                    base, description, Scope.Instance, Order.Inner);
+        }
+    }
+
+    public static final TestRule sImplicitClassOuterRule = new RavenwoodClassOuterRule();
+    public static final TestRule sImplicitClassInnerRule = new RavenwoodClassInnerRule();
+    public static final TestRule sImplicitInstOuterRule = new RavenwoodInstanceOuterRule();
+    public static final TestRule sImplicitInstInnerRule = new RavenwoodInstanceOuterRule();
+
+    public static final String IMPLICIT_CLASS_OUTER_RULE_NAME = "sImplicitClassOuterRule";
+    public static final String IMPLICIT_CLASS_INNER_RULE_NAME = "sImplicitClassInnerRule";
+    public static final String IMPLICIT_INST_OUTER_RULE_NAME = "sImplicitInstOuterRule";
+    public static final String IMPLICIT_INST_INNER_RULE_NAME = "sImplicitInstInnerRule";
+
+    /** Keeps track of the runner on the current thread. */
+    private static final ThreadLocal<RavenwoodAwareTestRunner> sCurrentRunner = new ThreadLocal<>();
+
+    static RavenwoodAwareTestRunner getCurrentRunner() {
+        var runner = sCurrentRunner.get();
+        if (runner == null) {
+            throw new RuntimeException("Current test runner not set!");
+        }
+        return runner;
+    }
+
+    private final Class<?> mTestJavaClass;
+    private TestClass mTestClass = null;
+    private Runner mRealRunner = null;
+    private Description mDescription = null;
+    private Throwable mExceptionInConstructor = null;
+    private boolean mRealRunnerTakesRunnerBuilder = false;
+
+    /**
+     * Stores internal states / methods associated with this runner that's only needed in
+     * junit-impl.
+     */
+    final RavenwoodRunnerState mState = new RavenwoodRunnerState(this);
+
+    private Error logAndFail(String message, Throwable exception) {
+        Log.e(TAG, message, exception);
+        throw new AssertionError(message, exception);
+    }
+
+    public TestClass getTestClass() {
+        return mTestClass;
+    }
+
+    /**
+     * Constructor.
+     */
+    public RavenwoodAwareTestRunner(Class<?> testClass) {
+        mTestJavaClass = testClass;
+        try {
+            performGlobalInitialization();
+
+            /*
+             * If the class has @DisabledOnRavenwood, then we'll delegate to
+             * ClassSkippingTestRunner, which simply skips it.
+             *
+             * We need to do it before instantiating TestClass for b/367694651.
+             */
+            if (isOnRavenwood() && !RavenwoodAwareTestRunnerHook.shouldRunClassOnRavenwood(
+                    testClass)) {
+                mRealRunner = new ClassSkippingTestRunner(testClass);
+                mDescription = mRealRunner.getDescription();
+                return;
+            }
+
+            mTestClass = new TestClass(testClass);
+
+            Log.v(TAG, "RavenwoodAwareTestRunner starting for " + testClass.getCanonicalName());
+
+            onRunnerInitializing();
+
+            // Find the real runner.
+            final Class<? extends Runner> realRunnerClass;
+            final InnerRunner innerRunnerAnnotation = mTestClass.getAnnotation(InnerRunner.class);
+            if (innerRunnerAnnotation != null) {
+                realRunnerClass = innerRunnerAnnotation.value();
+            } else {
+                // Default runner.
+                realRunnerClass = BlockJUnit4ClassRunner.class;
+            }
+
+            try {
+                Log.i(TAG, "Initializing the inner runner: " + realRunnerClass);
+
+                mRealRunner = instantiateRealRunner(realRunnerClass, testClass);
+                mDescription = mRealRunner.getDescription();
+
+            } catch (InstantiationException | IllegalAccessException
+                     | InvocationTargetException | NoSuchMethodException e) {
+                throw logAndFail("Failed to instantiate " + realRunnerClass, e);
+            }
+        } catch (Throwable th) {
+            // If we throw in the constructor, Tradefed may not report it and just ignore the class,
+            // so record it and throw it when the test actually started.
+            Log.e(TAG, "Fatal: Exception detected in constructor", th);
+            mExceptionInConstructor = new RuntimeException("Exception detected in constructor",
+                    th);
+            mDescription = Description.createTestDescription(testClass, "Constructor");
+
+            // This is for testing if tradefed is fixed.
+            if ("1".equals(System.getenv("RAVENWOOD_THROW_EXCEPTION_IN_TEST_RUNNER"))) {
+                throw th;
+            }
+        }
+    }
+
+    private Runner instantiateRealRunner(
+            Class<? extends Runner> realRunnerClass,
+            Class<?> testClass)
+            throws NoSuchMethodException, InvocationTargetException, InstantiationException,
+            IllegalAccessException {
+        try {
+            return realRunnerClass.getConstructor(Class.class).newInstance(testClass);
+        } catch (NoSuchMethodException e) {
+            var constructor = realRunnerClass.getConstructor(Class.class, RunnerBuilder.class);
+            mRealRunnerTakesRunnerBuilder = true;
+            return constructor.newInstance(testClass, new AllDefaultPossibilitiesBuilder());
+        }
+    }
+
+    private void performGlobalInitialization() {
+        if (!isOnRavenwood()) {
+            return;
+        }
+        RavenwoodAwareTestRunnerHook.performGlobalInitialization();
+    }
+
+    /**
+     * Run the bare minimum setup to initialize the wrapped runner.
+     */
+    // This method is called by the ctor, so never make it virtual.
+    private void onRunnerInitializing() {
+        if (!isOnRavenwood()) {
+            return;
+        }
+
+        RavenwoodAwareTestRunnerHook.onRunnerInitializing(this, mTestClass);
+
+        // Hook point to allow more customization.
+        runAnnotatedMethodsOnRavenwood(RavenwoodTestRunnerInitializing.class, null);
+    }
+
+    private void runAnnotatedMethodsOnRavenwood(Class<? extends Annotation> annotationClass,
+            Object instance) {
+        if (!isOnRavenwood()) {
+            return;
+        }
+        Log.v(TAG, "runAnnotatedMethodsOnRavenwood() " + annotationClass.getName());
+
+        for (var method : getTestClass().getAnnotatedMethods(annotationClass)) {
+            ensureIsPublicVoidMethod(method.getMethod(), /* isStatic=*/ instance == null);
+
+            var methodDesc = method.getDeclaringClass().getName() + "."
+                    + method.getMethod().toString();
+            try {
+                method.getMethod().invoke(instance);
+            } catch (IllegalAccessException | InvocationTargetException e) {
+                throw logAndFail("Caught exception while running method " + methodDesc, e);
+            }
+        }
+    }
+
+    @Override
+    public Description getDescription() {
+        return mDescription;
+    }
+
+    @Override
+    public void run(RunNotifier realNotifier) {
+        final RavenwoodRunNotifier notifier = new RavenwoodRunNotifier(realNotifier);
+
+        if (mRealRunner instanceof ClassSkippingTestRunner) {
+            mRealRunner.run(notifier);
+            RavenwoodAwareTestRunnerHook.onClassSkipped(getDescription());
+            return;
+        }
+
+        Log.v(TAG, "Starting " + mTestJavaClass.getCanonicalName());
+        if (RAVENWOOD_VERBOSE_LOGGING) {
+            dumpDescription(getDescription());
+        }
+
+        if (maybeReportExceptionFromConstructor(notifier)) {
+            return;
+        }
+
+        // TODO(b/365976974): handle nested classes better
+        final boolean skipRunnerHook =
+                mRealRunnerTakesRunnerBuilder && mRealRunner instanceof Suite;
+
+        sCurrentRunner.set(this);
+        try {
+            if (!skipRunnerHook) {
+                try {
+                    RavenwoodAwareTestRunnerHook.onBeforeInnerRunnerStart(
+                            this, getDescription());
+                } catch (Throwable th) {
+                    notifier.reportBeforeTestFailure(getDescription(), th);
+                    return;
+                }
+            }
+
+            // Delegate to the inner runner.
+            mRealRunner.run(notifier);
+        } finally {
+            sCurrentRunner.remove();
+
+            if (!skipRunnerHook) {
+                try {
+                    RavenwoodAwareTestRunnerHook.onAfterInnerRunnerFinished(
+                            this, getDescription());
+                } catch (Throwable th) {
+                    notifier.reportAfterTestFailure(th);
+                }
+            }
+        }
+    }
+
+    /** Throw the exception detected in the constructor, if any. */
+    private boolean maybeReportExceptionFromConstructor(RunNotifier notifier) {
+        if (mExceptionInConstructor == null) {
+            return false;
+        }
+        notifier.fireTestStarted(mDescription);
+        notifier.fireTestFailure(new Failure(mDescription, mExceptionInConstructor));
+        notifier.fireTestFinished(mDescription);
+
+        return true;
+    }
+
+    @Override
+    public void filter(Filter filter) throws NoTestsRemainException {
+        if (mRealRunner instanceof Filterable r) {
+            r.filter(filter);
+        }
+    }
+
+    @Override
+    public void order(Orderer orderer) throws InvalidOrderingException {
+        if (mRealRunner instanceof Orderable r) {
+            r.order(orderer);
+        }
+    }
+
+    @Override
+    public void sort(Sorter sorter) {
+        if (mRealRunner instanceof Sortable r) {
+            r.sort(sorter);
+        }
+    }
+
+    private Statement wrapWithHooks(Statement base, Description description, Scope scope,
+            Order order) {
+        if (!isOnRavenwood()) {
+            return base;
+        }
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                runWithHooks(description, scope, order, base);
+            }
+        };
+    }
+
+    private void runWithHooks(Description description, Scope scope, Order order, Runnable r)
+            throws Throwable {
+        runWithHooks(description, scope, order, new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                r.run();
+            }
+        });
+    }
+
+    private void runWithHooks(Description description, Scope scope, Order order, Statement s)
+            throws Throwable {
+        if (isOnRavenwood()) {
+            Assume.assumeTrue(
+                    RavenwoodAwareTestRunnerHook.onBefore(this, description, scope, order));
+        }
+        try {
+            s.evaluate();
+            if (isOnRavenwood()) {
+                RavenwoodAwareTestRunnerHook.onAfter(this, description, scope, order, null);
+            }
+        } catch (Throwable t) {
+            boolean shouldThrow = true;
+            if (isOnRavenwood()) {
+                shouldThrow = RavenwoodAwareTestRunnerHook.onAfter(
+                        this, description, scope, order, t);
+            }
+            if (shouldThrow) {
+                throw t;
+            }
+        }
+    }
+
+    /**
+     * A runner that simply skips a class. It still has to support {@link Filterable}
+     * because otherwise the result still says "SKIPPED" even when it's not included in the
+     * filter.
+     */
+    private static class ClassSkippingTestRunner extends Runner implements Filterable {
+        private final Description mDescription;
+        private boolean mFilteredOut;
+
+        ClassSkippingTestRunner(Class<?> testClass) {
+            mDescription = Description.createTestDescription(testClass, testClass.getSimpleName());
+            mFilteredOut = false;
+        }
+
+        @Override
+        public Description getDescription() {
+            return mDescription;
+        }
+
+        @Override
+        public void run(RunNotifier notifier) {
+            if (mFilteredOut) {
+                return;
+            }
+            notifier.fireTestSuiteStarted(mDescription);
+            notifier.fireTestIgnored(mDescription);
+            notifier.fireTestSuiteFinished(mDescription);
+        }
+
+        @Override
+        public void filter(Filter filter) throws NoTestsRemainException {
+            if (filter.shouldRun(mDescription)) {
+                mFilteredOut = false;
+            } else {
+                throw new NoTestsRemainException();
+            }
+        }
+    }
+
+    private void dumpDescription(Description desc) {
+        dumpDescription(desc, "[TestDescription]=", "  ");
+    }
+
+    private void dumpDescription(Description desc, String header, String indent) {
+        Log.v(TAG, indent + header + desc);
+
+        var children = desc.getChildren();
+        var childrenIndent = "  " + indent;
+        for (int i = 0; i < children.size(); i++) {
+            dumpDescription(children.get(i), "#" + i + ": ", childrenIndent);
+        }
+    }
+
+    /**
+     * A run notifier that wraps another notifier and provides the following features:
+     * - Handle a failure that happened before testStarted and testEnded (typically that means
+     *   it's from @BeforeClass or @AfterClass, or a @ClassRule) and deliver it as if
+     *   individual tests in the class reported it. This is for b/364395552.
+     *
+     * - Logging.
+     */
+    private class RavenwoodRunNotifier extends RunNotifier {
+        private final RunNotifier mRealNotifier;
+
+        private final Stack<Description> mSuiteStack = new Stack<>();
+        private Description mCurrentSuite = null;
+        private final ArrayList<Throwable> mOutOfTestFailures = new ArrayList<>();
+
+        private boolean mBeforeTest = true;
+        private boolean mAfterTest = false;
+
+        private RavenwoodRunNotifier(RunNotifier realNotifier) {
+            mRealNotifier = realNotifier;
+        }
+
+        private boolean isInTest() {
+            return !mBeforeTest && !mAfterTest;
+        }
+
+        @Override
+        public void addListener(RunListener listener) {
+            mRealNotifier.addListener(listener);
+        }
+
+        @Override
+        public void removeListener(RunListener listener) {
+            mRealNotifier.removeListener(listener);
+        }
+
+        @Override
+        public void addFirstListener(RunListener listener) {
+            mRealNotifier.addFirstListener(listener);
+        }
+
+        @Override
+        public void fireTestRunStarted(Description description) {
+            Log.i(TAG, "testRunStarted: " + description);
+            mRealNotifier.fireTestRunStarted(description);
+        }
+
+        @Override
+        public void fireTestRunFinished(Result result) {
+            Log.i(TAG, "testRunFinished: "
+                    + result.getRunCount() + ","
+                    + result.getFailureCount() + ","
+                    + result.getAssumptionFailureCount() + ","
+                    + result.getIgnoreCount());
+            mRealNotifier.fireTestRunFinished(result);
+        }
+
+        @Override
+        public void fireTestSuiteStarted(Description description) {
+            Log.i(TAG, "testSuiteStarted: " + description);
+            mRealNotifier.fireTestSuiteStarted(description);
+
+            mBeforeTest = true;
+            mAfterTest = false;
+
+            // Keep track of the current suite, needed if the outer test is a Suite,
+            // in which case its children are test classes. (not test methods)
+            mCurrentSuite = description;
+            mSuiteStack.push(description);
+
+            mOutOfTestFailures.clear();
+        }
+
+        @Override
+        public void fireTestSuiteFinished(Description description) {
+            Log.i(TAG, "testSuiteFinished: " + description);
+            mRealNotifier.fireTestSuiteFinished(description);
+
+            maybeHandleOutOfTestFailures();
+
+            mBeforeTest = true;
+            mAfterTest = false;
+
+            // Restore the upper suite.
+            mSuiteStack.pop();
+            mCurrentSuite = mSuiteStack.size() == 0 ? null : mSuiteStack.peek();
+        }
+
+        @Override
+        public void fireTestStarted(Description description) throws StoppedByUserException {
+            Log.i(TAG, "testStarted: " + description);
+            mRealNotifier.fireTestStarted(description);
+
+            mAfterTest = false;
+            mBeforeTest = false;
+        }
+
+        @Override
+        public void fireTestFailure(Failure failure) {
+            Log.i(TAG, "testFailure: " + failure);
+
+            if (isInTest()) {
+                mRealNotifier.fireTestFailure(failure);
+            } else {
+                mOutOfTestFailures.add(failure.getException());
+            }
+        }
+
+        @Override
+        public void fireTestAssumptionFailed(Failure failure) {
+            Log.i(TAG, "testAssumptionFailed: " + failure);
+
+            if (isInTest()) {
+                mRealNotifier.fireTestAssumptionFailed(failure);
+            } else {
+                mOutOfTestFailures.add(failure.getException());
+            }
+        }
+
+        @Override
+        public void fireTestIgnored(Description description) {
+            Log.i(TAG, "testIgnored: " + description);
+            mRealNotifier.fireTestIgnored(description);
+        }
+
+        @Override
+        public void fireTestFinished(Description description) {
+            Log.i(TAG, "testFinished: " + description);
+            mRealNotifier.fireTestFinished(description);
+
+            mAfterTest = true;
+        }
+
+        @Override
+        public void pleaseStop() {
+            Log.w(TAG, "pleaseStop:");
+            mRealNotifier.pleaseStop();
+        }
+
+        /**
+         * At the end of each Suite, we handle failures happened out of test methods.
+         * (typically in @BeforeClass or @AfterClasses)
+         *
+         * This is to work around b/364395552.
+         */
+        private boolean maybeHandleOutOfTestFailures() {
+            if (mOutOfTestFailures.size() == 0) {
+                return false;
+            }
+            Throwable th;
+            if (mOutOfTestFailures.size() == 1) {
+                th = mOutOfTestFailures.get(0);
+            } else {
+                th = new MultipleFailureException(mOutOfTestFailures);
+            }
+            if (mBeforeTest) {
+                reportBeforeTestFailure(mCurrentSuite, th);
+                return true;
+            }
+            if (mAfterTest) {
+                reportAfterTestFailure(th);
+                return true;
+            }
+            return false;
+        }
+
+        public void reportBeforeTestFailure(Description suiteDesc, Throwable th) {
+            // If a failure happens befere running any tests, we'll need to pretend
+            // as if each test in the suite reported the failure, to work around b/364395552.
+            for (var child : suiteDesc.getChildren()) {
+                if (child.isSuite()) {
+                    // If the chiil is still a "parent" -- a test class or a test suite
+                    // -- propagate to its children.
+                    mRealNotifier.fireTestSuiteStarted(child);
+                    reportBeforeTestFailure(child, th);
+                    mRealNotifier.fireTestSuiteFinished(child);
+                } else {
+                    mRealNotifier.fireTestStarted(child);
+                    Failure f = new Failure(child, th);
+                    if (th instanceof AssumptionViolatedException) {
+                        mRealNotifier.fireTestAssumptionFailed(f);
+                    } else {
+                        mRealNotifier.fireTestFailure(f);
+                    }
+                    mRealNotifier.fireTestFinished(child);
+                }
+            }
+        }
+
+        public void reportAfterTestFailure(Throwable th) {
+            // Unfortunately, there's no good way to report it, so kill the own process.
+            onCriticalError(
+                    "Failures detected in @AfterClass, which would be swallowed by tradefed",
+                    th);
+        }
+    }
+
+    private static volatile BiConsumer<String, Throwable> sCriticalErrorHanler;
+
+    private void onCriticalError(@NonNull String message, @Nullable Throwable th) {
+        Log.e(TAG, "Critical error! " + message, th);
+        var handler = sCriticalErrorHanler;
+        if (handler == null) {
+            handler = sDefaultCriticalErrorHandler;
+        }
+        handler.accept(message, th);
+    }
+
+    private static BiConsumer<String, Throwable> sDefaultCriticalErrorHandler = (message, th) -> {
+        Log.e(TAG, "Ravenwood cannot continue. Killing self process.", th);
+        System.exit(1);
+    };
+
+    /**
+     * Contains Ravenwood private APIs.
+     */
+    public static class RavenwoodPrivate {
+        private RavenwoodPrivate() {
+        }
+
+        /**
+         * Set a listener for onCriticalError(), for testing. If a listener is set, we won't call
+         * System.exit().
+         */
+        public void setCriticalErrorHandler(
+                @Nullable BiConsumer<String, Throwable> handler) {
+            sCriticalErrorHanler = handler;
+        }
+    }
+
+    private static final RavenwoodPrivate sRavenwoodPrivate = new RavenwoodPrivate();
+
+    public static RavenwoodPrivate private$ravenwood() {
+        return sRavenwoodPrivate;
+    }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
index f4b7ec36..85297fe 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodClassRule.java
@@ -16,42 +16,20 @@
 
 package android.platform.test.ravenwood;
 
-import static android.platform.test.ravenwood.RavenwoodRule.ENABLE_PROBE_IGNORED;
-import static android.platform.test.ravenwood.RavenwoodRule.IS_ON_RAVENWOOD;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnDevice;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldEnableOnRavenwood;
-import static android.platform.test.ravenwood.RavenwoodRule.shouldStillIgnoreInProbeIgnoreMode;
-
-import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.annotations.EnabledOnRavenwood;
-
-import org.junit.Assert;
-import org.junit.Assume;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
 /**
- * {@code @ClassRule} that respects Ravenwood-specific class annotations. This rule has no effect
- * when tests are run on non-Ravenwood test environments.
+ * No longer needed.
  *
- * By default, all tests are executed on Ravenwood, but annotations such as
- * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method
- * and class level to "ignore" tests that may not be ready.
+ * @deprecated this class used to be used to handle the class level annotation, which
+ * is now done by the test runner, so this class is not needed.
  */
+@Deprecated
 public class RavenwoodClassRule implements TestRule {
     @Override
     public Statement apply(Statement base, Description description) {
-        if (!IS_ON_RAVENWOOD) {
-            // This should be "Assume", not Assert, but if we use assume here, the device side
-            // test runner would complain.
-            // See the TODO comment in RavenwoodClassRuleRavenwoodOnlyTest.
-            Assert.assertTrue(shouldEnableOnDevice(description));
-        } else if (ENABLE_PROBE_IGNORED) {
-            Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
-        } else {
-            Assume.assumeTrue(shouldEnableOnRavenwood(description));
-        }
         return base;
     }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
new file mode 100644
index 0000000..446f819
--- /dev/null
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodConfig.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import static android.os.Process.FIRST_APPLICATION_UID;
+import static android.os.Process.SYSTEM_UID;
+import static android.os.UserHandle.SYSTEM;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.Instrumentation;
+import android.content.Context;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * Represents how to configure the ravenwood environment for a test class.
+ *
+ * If a ravenwood test class has a public static field with the {@link Config} annotation,
+ * Ravenwood will extract the config from it and initializes the environment. The type of the
+ * field must be of {@link RavenwoodConfig}.
+ */
+public final class RavenwoodConfig {
+    /**
+     * Use this to mark a field as the configuration.
+     * @hide
+     */
+    @Target({ElementType.FIELD})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface Config {
+    }
+
+    private static final int NOBODY_UID = 9999;
+
+    private static final AtomicInteger sNextPid = new AtomicInteger(100);
+
+    int mCurrentUser = SYSTEM.getIdentifier();
+
+    /**
+     * Unless the test author requests differently, run as "nobody", and give each collection of
+     * tests its own unique PID.
+     */
+    int mUid = NOBODY_UID;
+    int mPid = sNextPid.getAndIncrement();
+
+    String mTestPackageName;
+    String mTargetPackageName;
+
+    int mMinSdkLevel;
+
+    boolean mProvideMainThread = false;
+
+    final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
+
+    final List<Class<?>> mServicesRequired = new ArrayList<>();
+
+    volatile Context mInstContext;
+    volatile Context mTargetContext;
+    volatile Instrumentation mInstrumentation;
+
+    /**
+     * Stores internal states / methods associated with this config that's only needed in
+     * junit-impl.
+     */
+    final RavenwoodConfigState mState = new RavenwoodConfigState(this);
+    private RavenwoodConfig() {
+    }
+
+    /**
+     * Return if the current process is running on a Ravenwood test environment.
+     */
+    public static boolean isOnRavenwood() {
+        return RavenwoodRule.isOnRavenwood();
+    }
+
+    private void setDefaults() {
+        if (mTargetPackageName == null) {
+            mTargetPackageName = mTestPackageName;
+        }
+    }
+
+    public static class Builder {
+        private final RavenwoodConfig mConfig = new RavenwoodConfig();
+
+        public Builder() {
+        }
+
+        /**
+         * Configure the identity of this process to be the system UID for the duration of the
+         * test. Has no effect on non-Ravenwood environments.
+         */
+        public Builder setProcessSystem() {
+            mConfig.mUid = SYSTEM_UID;
+            return this;
+        }
+
+        /**
+         * Configure the identity of this process to be an app UID for the duration of the
+         * test. Has no effect on non-Ravenwood environments.
+         */
+        public Builder setProcessApp() {
+            mConfig.mUid = FIRST_APPLICATION_UID;
+            return this;
+        }
+
+        /**
+         * Configure the package name of the test, which corresponds to
+         * {@link Instrumentation#getContext()}.
+         */
+        public Builder setPackageName(@NonNull String packageName) {
+            mConfig.mTestPackageName = Objects.requireNonNull(packageName);
+            return this;
+        }
+
+        /**
+         * Configure the package name of the target app, which corresponds to
+         * {@link Instrumentation#getTargetContext()}. Defaults to {@link #setPackageName}.
+         */
+        public Builder setTargetPackageName(@NonNull String packageName) {
+            mConfig.mTargetPackageName = Objects.requireNonNull(packageName);
+            return this;
+        }
+
+        /**
+         * Configure the min SDK level of the test.
+         */
+        public Builder setMinSdkLevel(int sdkLevel) {
+            mConfig.mMinSdkLevel = sdkLevel;
+            return this;
+        }
+
+        /**
+         * Configure a "main" thread to be available for the duration of the test, as defined
+         * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
+         */
+        public Builder setProvideMainThread(boolean provideMainThread) {
+            mConfig.mProvideMainThread = provideMainThread;
+            return this;
+        }
+
+        /**
+         * Configure the given system property as immutable for the duration of the test.
+         * Read access to the key is allowed, and write access will fail. When {@code value} is
+         * {@code null}, the value is left as undefined.
+         *
+         * All properties in the {@code debug.*} namespace are automatically mutable, with no
+         * developer action required.
+         *
+         * Has no effect on non-Ravenwood environments.
+         */
+        public Builder setSystemPropertyImmutable(@NonNull String key,
+                @Nullable Object value) {
+            mConfig.mSystemProperties.setValue(key, value);
+            mConfig.mSystemProperties.setAccessReadOnly(key);
+            return this;
+        }
+
+        /**
+         * Configure the given system property as mutable for the duration of the test.
+         * Both read and write access to the key is allowed, and its value will be reset between
+         * each test. When {@code value} is {@code null}, the value is left as undefined.
+         *
+         * All properties in the {@code debug.*} namespace are automatically mutable, with no
+         * developer action required.
+         *
+         * Has no effect on non-Ravenwood environments.
+         */
+        public Builder setSystemPropertyMutable(@NonNull String key,
+                @Nullable Object value) {
+            mConfig.mSystemProperties.setValue(key, value);
+            mConfig.mSystemProperties.setAccessReadWrite(key);
+            return this;
+        }
+
+        /**
+         * Configure the set of system services that are required for this test to operate.
+         *
+         * For example, passing {@code android.hardware.SerialManager.class} as an argument will
+         * ensure that the underlying service is created, initialized, and ready to use for the
+         * duration of the test. The {@code SerialManager} instance can be obtained via
+         * {@code RavenwoodRule.getContext()} and {@code Context.getSystemService()}, and
+         * {@code SerialManagerInternal} can be obtained via {@code LocalServices.getService()}.
+         */
+        public Builder setServicesRequired(@NonNull Class<?>... services) {
+            mConfig.mServicesRequired.clear();
+            for (Class<?> service : services) {
+                mConfig.mServicesRequired.add(service);
+            }
+            return this;
+        }
+
+        public RavenwoodConfig build() {
+            mConfig.setDefaults();
+            return mConfig;
+        }
+    }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index 825c91a..4196d8e 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -16,56 +16,43 @@
 
 package android.platform.test.ravenwood;
 
-import static android.os.Process.FIRST_APPLICATION_UID;
-import static android.os.Process.SYSTEM_UID;
-import static android.os.UserHandle.SYSTEM;
+import static com.android.ravenwood.common.RavenwoodCommonUtils.log;
 
-import static org.junit.Assert.fail;
-
+import android.annotation.NonNull;
+import android.annotation.Nullable;
 import android.app.Instrumentation;
 import android.content.Context;
-import android.platform.test.annotations.DisabledOnNonRavenwood;
 import android.platform.test.annotations.DisabledOnRavenwood;
-import android.platform.test.annotations.EnabledOnRavenwood;
-import android.platform.test.annotations.IgnoreUnderRavenwood;
 
 import com.android.ravenwood.common.RavenwoodCommonUtils;
 
-import org.junit.Assume;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Objects;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
 
 /**
- * {@code @Rule} that configures the Ravenwood test environment. This rule has no effect when
- * tests are run on non-Ravenwood test environments.
- *
- * This rule initializes and resets the Ravenwood environment between each test method to offer a
- * hermetic testing environment.
- *
- * By default, all tests are executed on Ravenwood, but annotations such as
- * {@link DisabledOnRavenwood} and {@link EnabledOnRavenwood} can be used at both the method
- * and class level to "ignore" tests that may not be ready. When needed, a
- * {@link RavenwoodClassRule} can be used in addition to a {@link RavenwoodRule} to ignore tests
- * before a test class is fully initialized.
+ * @deprecated Use {@link RavenwoodConfig} to configure the ravenwood environment instead.
+ * A {@link RavenwoodRule} is no longer needed for {@link DisabledOnRavenwood}. To get the
+ * {@link Context} and {@link Instrumentation}, use
+ * {@link androidx.test.platform.app.InstrumentationRegistry} instead.
  */
-public class RavenwoodRule implements TestRule {
+@Deprecated
+public final class RavenwoodRule implements TestRule {
+    private static final String TAG = "RavenwoodRule";
+
     static final boolean IS_ON_RAVENWOOD = RavenwoodCommonUtils.isOnRavenwood();
 
     /**
-     * When probing is enabled, all tests will be unconditionally run on Ravenwood to detect
-     * cases where a test is able to pass despite being marked as {@code IgnoreUnderRavenwood}.
+     * When this flag is enabled, all tests will be unconditionally run on Ravenwood to detect
+     * cases where a test is able to pass despite being marked as {@link DisabledOnRavenwood}.
      *
      * This is typically helpful for internal maintainers discovering tests that had previously
      * been ignored, but now have enough Ravenwood-supported functionality to be enabled.
      */
-    static final boolean ENABLE_PROBE_IGNORED = "1".equals(
+    private static final boolean RUN_DISABLED_TESTS = "1".equals(
             System.getenv("RAVENWOOD_RUN_DISABLED_TESTS"));
 
     /**
@@ -90,56 +77,34 @@
      *
      * Because we use a regex-find, setting "." would disable all tests.
      */
-    private static final Pattern REALLY_DISABLE_PATTERN = Pattern.compile(
-            Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLE"), ""));
+    private static final Pattern REALLY_DISABLED_PATTERN = Pattern.compile(
+            Objects.requireNonNullElse(System.getenv("RAVENWOOD_REALLY_DISABLED"), ""));
 
-    private static final boolean ENABLE_REALLY_DISABLE_PATTERN =
-            !REALLY_DISABLE_PATTERN.pattern().isEmpty();
-
-    /**
-     * If true, enable optional validation on running tests.
-     */
-    private static final boolean ENABLE_OPTIONAL_VALIDATION = "1".equals(
-            System.getenv("RAVENWOOD_OPTIONAL_VALIDATION"));
+    private static final boolean HAS_REALLY_DISABLE_PATTERN =
+            !REALLY_DISABLED_PATTERN.pattern().isEmpty();
 
     static {
-        if (ENABLE_PROBE_IGNORED) {
-            System.out.println("$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
-            if (ENABLE_REALLY_DISABLE_PATTERN) {
-                System.out.println("$RAVENWOOD_REALLY_DISABLE=" + REALLY_DISABLE_PATTERN.pattern());
+        if (RUN_DISABLED_TESTS) {
+            log(TAG, "$RAVENWOOD_RUN_DISABLED_TESTS enabled: force running all tests");
+            if (HAS_REALLY_DISABLE_PATTERN) {
+                log(TAG, "$RAVENWOOD_REALLY_DISABLED=" + REALLY_DISABLED_PATTERN.pattern());
             }
         }
     }
 
-    private static final int NOBODY_UID = 9999;
-
-    private static final AtomicInteger sNextPid = new AtomicInteger(100);
-
-    int mCurrentUser = SYSTEM.getIdentifier();
-
-    /**
-     * Unless the test author requests differently, run as "nobody", and give each collection of
-     * tests its own unique PID.
-     */
-    int mUid = NOBODY_UID;
-    int mPid = sNextPid.getAndIncrement();
-
-    String mPackageName;
-
-    boolean mProvideMainThread = false;
-
-    final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
-
-    final List<Class<?>> mServicesRequired = new ArrayList<>();
-
-    volatile Context mContext;
-    volatile Instrumentation mInstrumentation;
+    private final RavenwoodConfig mConfiguration;
 
     public RavenwoodRule() {
+        mConfiguration = new RavenwoodConfig.Builder().build();
+    }
+
+    private RavenwoodRule(RavenwoodConfig config) {
+        mConfiguration = config;
     }
 
     public static class Builder {
-        private RavenwoodRule mRule = new RavenwoodRule();
+        private final RavenwoodConfig.Builder mBuilder =
+                new RavenwoodConfig.Builder();
 
         public Builder() {
         }
@@ -149,7 +114,7 @@
          * test. Has no effect on non-Ravenwood environments.
          */
         public Builder setProcessSystem() {
-            mRule.mUid = SYSTEM_UID;
+            mBuilder.setProcessSystem();
             return this;
         }
 
@@ -158,7 +123,7 @@
          * test. Has no effect on non-Ravenwood environments.
          */
         public Builder setProcessApp() {
-            mRule.mUid = FIRST_APPLICATION_UID;
+            mBuilder.setProcessApp();
             return this;
         }
 
@@ -166,8 +131,8 @@
          * Configure the identity of this process to be the given package name for the duration
          * of the test. Has no effect on non-Ravenwood environments.
          */
-        public Builder setPackageName(/* @NonNull */ String packageName) {
-            mRule.mPackageName = Objects.requireNonNull(packageName);
+        public Builder setPackageName(@NonNull String packageName) {
+            mBuilder.setPackageName(packageName);
             return this;
         }
 
@@ -176,7 +141,7 @@
          * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments.
          */
         public Builder setProvideMainThread(boolean provideMainThread) {
-            mRule.mProvideMainThread = provideMainThread;
+            mBuilder.setProvideMainThread(provideMainThread);
             return this;
         }
 
@@ -190,10 +155,8 @@
          *
          * Has no effect on non-Ravenwood environments.
          */
-        public Builder setSystemPropertyImmutable(/* @NonNull */ String key,
-                /* @Nullable */ Object value) {
-            mRule.mSystemProperties.setValue(key, value);
-            mRule.mSystemProperties.setAccessReadOnly(key);
+        public Builder setSystemPropertyImmutable(@NonNull String key, @Nullable Object value) {
+            mBuilder.setSystemPropertyImmutable(key, value);
             return this;
         }
 
@@ -207,10 +170,8 @@
          *
          * Has no effect on non-Ravenwood environments.
          */
-        public Builder setSystemPropertyMutable(/* @NonNull */ String key,
-                /* @Nullable */ Object value) {
-            mRule.mSystemProperties.setValue(key, value);
-            mRule.mSystemProperties.setAccessReadWrite(key);
+        public Builder setSystemPropertyMutable(@NonNull String key, @Nullable Object value) {
+            mBuilder.setSystemPropertyMutable(key, value);
             return this;
         }
 
@@ -223,16 +184,13 @@
          * {@code RavenwoodRule.getContext()} and {@code Context.getSystemService()}, and
          * {@code SerialManagerInternal} can be obtained via {@code LocalServices.getService()}.
          */
-        public Builder setServicesRequired(Class<?>... services) {
-            mRule.mServicesRequired.clear();
-            for (Class<?> service : services) {
-                mRule.mServicesRequired.add(service);
-            }
+        public Builder setServicesRequired(@NonNull Class<?>... services) {
+            mBuilder.setServicesRequired(services);
             return this;
         }
 
         public RavenwoodRule build() {
-            return mRule;
+            return new RavenwoodRule(mBuilder.build());
         }
     }
 
@@ -252,184 +210,50 @@
     }
 
     /**
-     * Return a {@code Context} available for usage during the currently running test case.
-     *
-     * Each test should obtain needed information or references via this method;
-     * references must not be stored beyond the scope of a test case.
+     * @deprecated Use
+     * {@code androidx.test.platform.app.InstrumentationRegistry.getInstrumentation().getContext()}
+     * instead.
      */
+    @Deprecated
     public Context getContext() {
-        return Objects.requireNonNull(mContext,
+        return Objects.requireNonNull(mConfiguration.mInstContext,
                 "Context is only available during @Test execution");
     }
 
     /**
-     * Return a {@code Instrumentation} available for usage during the currently running test case.
-     *
-     * Each test should obtain needed information or references via this method;
-     * references must not be stored beyond the scope of a test case.
+     * @deprecated Use
+     * {@code androidx.test.platform.app.InstrumentationRegistry.getInstrumentation()}
+     * instead.
      */
+    @Deprecated
     public Instrumentation getInstrumentation() {
-        return Objects.requireNonNull(mInstrumentation,
+        return Objects.requireNonNull(mConfiguration.mInstrumentation,
                 "Instrumentation is only available during @Test execution");
     }
 
-    static boolean shouldEnableOnDevice(Description description) {
-        if (description.isTest()) {
-            if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
-                return false;
-            }
-        }
-        final var clazz = description.getTestClass();
-        if (clazz != null) {
-            if (clazz.getAnnotation(DisabledOnNonRavenwood.class) != null) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    /**
-     * Determine if the given {@link Description} should be enabled when running on the
-     * Ravenwood test environment.
-     *
-     * A more specific method-level annotation always takes precedence over any class-level
-     * annotation, and an {@link EnabledOnRavenwood} annotation always takes precedence over
-     * an {@link DisabledOnRavenwood} annotation.
-     */
-    static boolean shouldEnableOnRavenwood(Description description) {
-        // First, consult any method-level annotations
-        if (description.isTest()) {
-            // Stopgap for http://g/ravenwood/EPAD-N5ntxM
-            if (description.getMethodName().endsWith("$noRavenwood")) {
-                return false;
-            }
-            if (description.getAnnotation(EnabledOnRavenwood.class) != null) {
-                return true;
-            }
-            if (description.getAnnotation(DisabledOnRavenwood.class) != null) {
-                return false;
-            }
-            if (description.getAnnotation(IgnoreUnderRavenwood.class) != null) {
-                return false;
-            }
-        }
-
-        // Otherwise, consult any class-level annotations
-        final var clazz = description.getTestClass();
-        if (clazz != null) {
-            if (description.getTestClass().getAnnotation(EnabledOnRavenwood.class) != null) {
-                return true;
-            }
-            if (description.getTestClass().getAnnotation(DisabledOnRavenwood.class) != null) {
-                return false;
-            }
-            if (description.getTestClass().getAnnotation(IgnoreUnderRavenwood.class) != null) {
-                return false;
-            }
-        }
-
-        // When no annotations have been requested, assume test should be included
-        return true;
-    }
-
-    static boolean shouldStillIgnoreInProbeIgnoreMode(Description description) {
-        if (!ENABLE_REALLY_DISABLE_PATTERN) {
-            return false;
-        }
-
-        final var fullname = description.getTestClass().getName()
-                + (description.isTest() ? "#" + description.getMethodName() : "");
-
-        if (REALLY_DISABLE_PATTERN.matcher(fullname).find()) {
-            System.out.println("Still ignoring " + fullname);
-            return true;
-        }
-        return false;
-    }
-
     @Override
     public Statement apply(Statement base, Description description) {
-        // No special treatment when running outside Ravenwood; run tests as-is
-        if (!IS_ON_RAVENWOOD) {
-            Assume.assumeTrue(shouldEnableOnDevice(description));
+        if (!RavenwoodConfig.isOnRavenwood()) {
             return base;
         }
-
-        if (ENABLE_PROBE_IGNORED) {
-            return applyProbeIgnored(base, description);
-        } else {
-            return applyDefault(base, description);
-        }
-    }
-
-    private void commonPrologue(Statement base, Description description) {
-        RavenwoodRuleImpl.logTestRunner("started", description);
-        RavenwoodRuleImpl.validate(base, description, ENABLE_OPTIONAL_VALIDATION);
-        RavenwoodRuleImpl.init(RavenwoodRule.this);
-    }
-
-    /**
-     * Run the given {@link Statement} with no special treatment.
-     */
-    private Statement applyDefault(Statement base, Description description) {
         return new Statement() {
             @Override
             public void evaluate() throws Throwable {
-                Assume.assumeTrue(shouldEnableOnRavenwood(description));
-
-                commonPrologue(base, description);
+                RavenwoodAwareTestRunnerHook.onRavenwoodRuleEnter(
+                        RavenwoodAwareTestRunner.getCurrentRunner(), description,
+                        RavenwoodRule.this);
                 try {
                     base.evaluate();
-                    RavenwoodRuleImpl.logTestRunner("finished", description);
-                } catch (Throwable t) {
-                    RavenwoodRuleImpl.logTestRunner("failed", description);
-                    throw t;
                 } finally {
-                    RavenwoodRuleImpl.reset(RavenwoodRule.this);
+                    RavenwoodAwareTestRunnerHook.onRavenwoodRuleExit(
+                            RavenwoodAwareTestRunner.getCurrentRunner(), description,
+                            RavenwoodRule.this);
                 }
             }
         };
     }
 
     /**
-     * Run the given {@link Statement} with probing enabled. All tests will be unconditionally
-     * run on Ravenwood to detect cases where a test is able to pass despite being marked as
-     * {@code IgnoreUnderRavenwood}.
-     */
-    private Statement applyProbeIgnored(Statement base, Description description) {
-        return new Statement() {
-            @Override
-            public void evaluate() throws Throwable {
-                Assume.assumeFalse(shouldStillIgnoreInProbeIgnoreMode(description));
-
-                commonPrologue(base, description);
-                try {
-                    base.evaluate();
-                } catch (Throwable t) {
-                    // If the test isn't included, eat the exception and report the
-                    // assumption failure that test authors expect; otherwise throw
-                    Assume.assumeTrue(shouldEnableOnRavenwood(description));
-                    throw t;
-                } finally {
-                    RavenwoodRuleImpl.logTestRunner("finished", description);
-                    RavenwoodRuleImpl.reset(RavenwoodRule.this);
-                }
-
-                if (!shouldEnableOnRavenwood(description)) {
-                    fail("Test wasn't included under Ravenwood, but it actually "
-                            + "passed under Ravenwood; consider updating annotations");
-                }
-            }
-        };
-    }
-
-    public static class _$RavenwoodPrivate {
-        public static boolean isOptionalValidationEnabled() {
-            return ENABLE_OPTIONAL_VALIDATION;
-        }
-    }
-
-    /**
      * Returns the "real" result from {@link System#currentTimeMillis()}.
      *
      * Currently, it's the same thing as calling {@link System#currentTimeMillis()},
@@ -439,4 +263,51 @@
     public long realCurrentTimeMillis() {
         return System.currentTimeMillis();
     }
+
+    // Below are internal to ravenwood. Don't use them from normal tests...
+
+    public static class RavenwoodPrivate {
+        private RavenwoodPrivate() {
+        }
+
+        private volatile Boolean mRunDisabledTestsOverride = null;
+
+        private volatile Pattern mReallyDisabledPattern = null;
+
+        public boolean isRunningDisabledTests() {
+            if (mRunDisabledTestsOverride != null) {
+                return mRunDisabledTestsOverride;
+            }
+            return RUN_DISABLED_TESTS;
+        }
+
+        public Pattern getReallyDisabledPattern() {
+            if (mReallyDisabledPattern != null) {
+                return mReallyDisabledPattern;
+            }
+            return REALLY_DISABLED_PATTERN;
+        }
+
+        public void overrideRunDisabledTest(boolean runDisabledTests,
+                @Nullable String reallyDisabledPattern) {
+            mRunDisabledTestsOverride = runDisabledTests;
+            mReallyDisabledPattern =
+                    reallyDisabledPattern == null ? null : Pattern.compile(reallyDisabledPattern);
+        }
+
+        public void resetRunDisabledTest() {
+            mRunDisabledTestsOverride = null;
+            mReallyDisabledPattern = null;
+        }
+    }
+
+    private static final RavenwoodPrivate sRavenwoodPrivate = new  RavenwoodPrivate();
+
+    public static RavenwoodPrivate private$ravenwood() {
+        return sRavenwoodPrivate;
+    }
+
+    RavenwoodConfig getConfiguration() {
+        return mConfiguration;
+    }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
index 5f1b0c2..ef8f584 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodSystemProperties.java
@@ -48,11 +48,13 @@
         switch (key) {
             case "gsm.version.baseband":
             case "no.such.thing":
+            case "qemu.sf.lcd_density":
             case "ro.bootloader":
             case "ro.debuggable":
             case "ro.hardware":
             case "ro.hw_timeout_multiplier":
             case "ro.odm.build.media_performance_class":
+            case "ro.sf.lcd_density":
             case "ro.treble.enabled":
             case "ro.vndk.version":
                 return true;
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
new file mode 100644
index 0000000..aa8c299
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodAwareTestRunnerHook.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Order;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner.Scope;
+
+import org.junit.runner.Description;
+import org.junit.runners.model.TestClass;
+
+/**
+ * Provide hook points created by {@link RavenwoodAwareTestRunner}. This is a version
+ * that's used on a device side test.
+ *
+ * All methods are no-op in real device tests.
+ *
+ * TODO: Use some kind of factory to provide different implementation for the device test
+ * and the ravenwood test.
+ */
+public class RavenwoodAwareTestRunnerHook {
+    private RavenwoodAwareTestRunnerHook() {
+    }
+
+    /**
+     * Called before any code starts. Internally it will only initialize the environment once.
+     */
+    public static void performGlobalInitialization() {
+    }
+
+    /**
+     * Called when a runner starts, before the inner runner gets a chance to run.
+     */
+    public static void onRunnerInitializing(RavenwoodAwareTestRunner runner, TestClass testClass) {
+    }
+
+    /**
+     * Called when a whole test class is skipped.
+     */
+    public static void onClassSkipped(Description description) {
+    }
+
+    /**
+     * Called before the inner runner starts.
+     */
+    public static void onBeforeInnerRunnerStart(
+            RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+    }
+
+    /**
+     * Called after the inner runner finished.
+     */
+    public static void onAfterInnerRunnerFinished(
+            RavenwoodAwareTestRunner runner, Description description) throws Throwable {
+    }
+
+    /**
+     * Called before a test / class.
+     *
+     * Return false if it should be skipped.
+     */
+    public static boolean onBefore(RavenwoodAwareTestRunner runner, Description description,
+            Scope scope, Order order) throws Throwable {
+        return true;
+    }
+
+    public static void onRavenwoodRuleEnter(RavenwoodAwareTestRunner runner,
+            Description description, RavenwoodRule rule) throws Throwable {
+    }
+
+    public static void onRavenwoodRuleExit(RavenwoodAwareTestRunner runner,
+            Description description, RavenwoodRule rule) throws Throwable {
+    }
+
+
+    /**
+     * Called after a test / class.
+     *
+     * Return false if the exception should be ignored.
+     */
+    public static boolean onAfter(RavenwoodAwareTestRunner runner, Description description,
+            Scope scope, Order order, Throwable th) {
+        return true;
+    }
+
+    public static boolean shouldRunClassOnRavenwood(Class<?> clazz) {
+        return true;
+    }
+}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java
new file mode 100644
index 0000000..43a28ba
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodConfigState.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+/** Stub class. The actual implementaetion is in junit-impl-src. */
+public class RavenwoodConfigState {
+    public RavenwoodConfigState(RavenwoodConfig config) {
+    }
+}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
deleted file mode 100644
index 483b98a..0000000
--- a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 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 android.platform.test.ravenwood;
-
-import org.junit.runner.Description;
-import org.junit.runners.model.Statement;
-
-public class RavenwoodRuleImpl {
-    public static void init(RavenwoodRule rule) {
-        // No-op when running on a real device
-    }
-
-    public static void reset(RavenwoodRule rule) {
-        // No-op when running on a real device
-    }
-
-    public static void logTestRunner(String label, Description description) {
-        // No-op when running on a real device
-    }
-
-    public static void validate(Statement base, Description description,
-            boolean enableOptionalValidation) {
-    }
-
-    public static long realCurrentTimeMillis() {
-        return System.currentTimeMillis();
-    }
-}
diff --git a/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
new file mode 100644
index 0000000..83cbc52
--- /dev/null
+++ b/ravenwood/junit-stub-src/android/platform/test/ravenwood/RavenwoodRunnerState.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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 android.platform.test.ravenwood;
+
+/** Stub class. The actual implementaetion is in junit-impl-src. */
+public class RavenwoodRunnerState {
+    public RavenwoodRunnerState(RavenwoodAwareTestRunner runner) {
+    }
+}
diff --git a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
similarity index 97%
rename from ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
rename to ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
index b477117..30abaa2 100644
--- a/ravenwood/minimum-test/test/com/android/ravenwood/RavenwoodMinimumTest.java
+++ b/ravenwood/minimum-test/test/com/android/ravenwoodtest/RavenwoodMinimumTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood;
+package com.android.ravenwoodtest;
 
 import android.platform.test.annotations.IgnoreUnderRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
diff --git a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
similarity index 96%
rename from ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
rename to ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
index 1029ed2..e547114 100644
--- a/ravenwood/resapk_test/test/com/android/ravenwood/resapk_test/RavenwoodResApkTest.java
+++ b/ravenwood/resapk_test/test/com/android/ravenwoodtest/resapk_test/RavenwoodResApkTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.resapk_test;
+package com.android.ravenwoodtest.resapk_test;
 
 
 import static junit.framework.TestCase.assertTrue;
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
index 0238baa..02153a7 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/JvmWorkaround.java
@@ -38,7 +38,6 @@
      */
     public abstract void setFdInt(FileDescriptor fd, int fdInt);
 
-
     /**
      * Equivalent to Android's FileDescriptor.getInt$().
      */
@@ -49,6 +48,10 @@
      */
     public abstract void closeFd(FileDescriptor fd) throws IOException;
 
+    public abstract long addressOf(Object o);
+
+    public abstract <T> T fromAddress(long address);
+
     /**
      * Placeholder implementation for the host side.
      *
@@ -75,5 +78,15 @@
         public void closeFd(FileDescriptor fd) {
             throw calledOnHostside();
         }
+
+        @Override
+        public long addressOf(Object o) {
+            throw calledOnHostside();
+        }
+
+        @Override
+        public <T> T fromAddress(long address) {
+            throw calledOnHostside();
+        }
     }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
index a260147..2323c65 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/OpenJdkWorkaround.java
@@ -18,8 +18,16 @@
 import java.io.FileDescriptor;
 import java.io.IOException;
 import java.lang.reflect.InvocationTargetException;
+import java.util.Map;
+import java.util.WeakHashMap;
 
 class OpenJdkWorkaround extends JvmWorkaround {
+
+    // @GuardedBy("sAddressMap")
+    private static final Map<Object, Long> sAddressMap = new WeakHashMap<>();
+    // @GuardedBy("sAddressMap")
+    private static long sCurrentAddress = 1;
+
     @Override
     public void setFdInt(FileDescriptor fd, int fdInt) {
         try {
@@ -60,4 +68,28 @@
                     + " perhaps JRE has changed?", e);
         }
     }
+
+    @Override
+    public long addressOf(Object o) {
+        synchronized (sAddressMap) {
+            Long address = sAddressMap.get(o);
+            if (address == null) {
+                address = sCurrentAddress++;
+                sAddressMap.put(o, address);
+            }
+            return address;
+        }
+    }
+
+    @Override
+    public <T> T fromAddress(long address) {
+        synchronized (sAddressMap) {
+            for (var e : sAddressMap.entrySet()) {
+                if (e.getValue() == address) {
+                    return (T) e.getKey();
+                }
+            }
+        }
+        return null;
+    }
 }
diff --git a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
index c8cc8d9..989bb6b 100644
--- a/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
+++ b/ravenwood/runtime-common-src/com/android/ravenwood/common/RavenwoodCommonUtils.java
@@ -15,12 +15,20 @@
  */
 package com.android.ravenwood.common;
 
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+
 import com.android.ravenwood.common.divergence.RavenwoodDivergence;
 
 import java.io.File;
 import java.io.FileDescriptor;
 import java.io.FileInputStream;
 import java.io.PrintStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.lang.reflect.Member;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
 import java.util.Arrays;
 
 public class RavenwoodCommonUtils {
@@ -31,6 +39,14 @@
 
     private static final Object sLock = new Object();
 
+    /**
+     * If set to "1", we enable the verbose logging.
+     *
+     * (See also InitLogging() in http://ac/system/libbase/logging.cpp)
+     */
+    public static final boolean RAVENWOOD_VERBOSE_LOGGING = "1".equals(System.getenv(
+            "RAVENWOOD_VERBOSE"));
+
     /** Name of `libravenwood_runtime` */
     private static final String RAVENWOOD_NATIVE_RUNTIME_NAME = "ravenwood_runtime";
 
@@ -42,10 +58,19 @@
 
     private static final boolean IS_ON_RAVENWOOD = RavenwoodDivergence.isOnRavenwood();
 
-    private static final String RAVEWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
+    private static final String RAVENWOOD_RUNTIME_PATH = getRavenwoodRuntimePathInternal();
 
     public static final String RAVENWOOD_SYSPROP = "ro.is_on_ravenwood";
 
+    public static final String RAVENWOOD_RESOURCE_APK = "ravenwood-res-apks/ravenwood-res.apk";
+    public static final String RAVENWOOD_INST_RESOURCE_APK =
+            "ravenwood-res-apks/ravenwood-inst-res.apk";
+
+    public static final String RAVENWOOD_EMPTY_RESOURCES_APK =
+            RAVENWOOD_RUNTIME_PATH + "ravenwood-data/ravenwood-empty-res.apk";
+
+    public static final String RAVENWOOD_VERSION_JAVA_SYSPROP = "android.ravenwood.version";
+
     // @GuardedBy("sLock")
     private static boolean sIntegrityChecked = false;
 
@@ -72,6 +97,18 @@
         return sEnableExtraRuntimeCheck;
     }
 
+    /** Simple logging method. */
+    public static void log(String tag, String message) {
+        // Avoid using Android's Log class, which could be broken for various reasons.
+        // (e.g. the JNI file doesn't exist for whatever reason)
+        System.out.print(tag + ": " + message + "\n");
+    }
+
+    /** Simple logging method. */
+    private void log(String tag, String format, Object... args) {
+        log(tag, String.format(format, args));
+    }
+
     /**
      * Load the main runtime JNI library.
      */
@@ -176,7 +213,7 @@
      */
     public static String getRavenwoodRuntimePath() {
         ensureOnRavenwood();
-        return RAVEWOOD_RUNTIME_PATH;
+        return RAVENWOOD_RUNTIME_PATH;
     }
 
     private static String getRavenwoodRuntimePathInternal() {
@@ -231,4 +268,37 @@
         var is = new FileInputStream(fd);
         RavenwoodCommonUtils.closeQuietly(is);
     }
+
+    public static void ensureIsPublicVoidMethod(Method method, boolean isStatic) {
+        var ok = Modifier.isPublic(method.getModifiers())
+                && (Modifier.isStatic(method.getModifiers()) == isStatic)
+                && (method.getReturnType() == void.class);
+        if (ok) {
+            return; // okay
+        }
+        throw new AssertionError(String.format(
+                "Method %s.%s() expected to be public %svoid",
+                method.getDeclaringClass().getName(), method.getName(),
+                (isStatic ? "static " : "")));
+    }
+
+    public static void ensureIsPublicMember(Member member, boolean isStatic) {
+        var ok = Modifier.isPublic(member.getModifiers())
+                && (Modifier.isStatic(member.getModifiers()) == isStatic);
+        if (ok) {
+            return; // okay
+        }
+        throw new AssertionError(String.format(
+                "%s.%s expected to be public %s",
+                member.getDeclaringClass().getName(), member.getName(),
+                (isStatic ? "static" : "")));
+    }
+
+    @NonNull
+    public static String getStackTraceString(@Nullable Throwable th) {
+        StringWriter stringWriter = new StringWriter();
+        PrintWriter writer = new PrintWriter(stringWriter);
+        th.printStackTrace(writer);
+        return stringWriter.toString();
+    }
 }
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
rename to ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
index f38d565..e21a9cd 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/CursorWindow_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/database/CursorWindow_host.java
@@ -13,9 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.database;
 
-import android.database.Cursor;
 import android.database.sqlite.SQLiteException;
 import android.os.Parcel;
 import android.util.Base64;
@@ -35,8 +34,8 @@
     private String mName;
     private int mColumnNum;
     private static class Row {
-        String[] fields;
-        int[] types;
+        String[] mFields;
+        int[] mTypes;
     }
 
     private final List<Row> mRows = new ArrayList<>();
@@ -69,9 +68,9 @@
     public static boolean nativeAllocRow(long windowPtr) {
         CursorWindow_host instance = sInstances.get(windowPtr);
         Row row = new Row();
-        row.fields = new String[instance.mColumnNum];
-        row.types = new int[instance.mColumnNum];
-        Arrays.fill(row.types, Cursor.FIELD_TYPE_NULL);
+        row.mFields = new String[instance.mColumnNum];
+        row.mTypes = new int[instance.mColumnNum];
+        Arrays.fill(row.mTypes, Cursor.FIELD_TYPE_NULL);
         instance.mRows.add(row);
         return true;
     }
@@ -82,8 +81,8 @@
             return false;
         }
         Row r = instance.mRows.get(row);
-        r.fields[column] = value;
-        r.types[column] = type;
+        r.mFields[column] = value;
+        r.mTypes[column] = type;
         return true;
     }
 
@@ -93,7 +92,7 @@
             return Cursor.FIELD_TYPE_NULL;
         }
 
-        return instance.mRows.get(row).types[column];
+        return instance.mRows.get(row).mTypes[column];
     }
 
     public static boolean nativePutString(long windowPtr, String value,
@@ -107,7 +106,7 @@
             return null;
         }
 
-        return instance.mRows.get(row).fields[column];
+        return instance.mRows.get(row).mFields[column];
     }
 
     public static boolean nativePutLong(long windowPtr, long value, int row, int column) {
@@ -170,8 +169,8 @@
         parcel.writeInt(window.mColumnNum);
         parcel.writeInt(window.mRows.size());
         for (int row = 0; row < window.mRows.size(); row++) {
-            parcel.writeStringArray(window.mRows.get(row).fields);
-            parcel.writeIntArray(window.mRows.get(row).types);
+            parcel.writeStringArray(window.mRows.get(row).mFields);
+            parcel.writeIntArray(window.mRows.get(row).mTypes);
         }
     }
 
@@ -183,8 +182,8 @@
         int rowCount = parcel.readInt();
         for (int row = 0; row < rowCount; row++) {
             Row r = new Row();
-            r.fields = parcel.createStringArray();
-            r.types = parcel.createIntArray();
+            r.mFields = parcel.createStringArray();
+            r.mTypes = parcel.createIntArray();
             window.mRows.add(r);
         }
         return windowPtr;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
similarity index 97%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
index 5e81124..1b63adc 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/MessageQueue_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/MessageQueue_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
 
 import java.util.Map;
 import java.util.concurrent.ConcurrentHashMap;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
similarity index 89%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
rename to ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
index e7479d3..b09bf31 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/SystemProperties_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/os/SystemProperties_host.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.os;
 
 import android.util.SparseArray;
 
@@ -36,9 +36,6 @@
     /** Predicate tested to determine if a given key can be written. */
     @GuardedBy("sLock")
     private static Predicate<String> sKeyWritablePredicate;
-    /** Callback to trigger when values are changed */
-    @GuardedBy("sLock")
-    private static Runnable sChangeCallback;
 
     /**
      * Reverse mapping that provides a way back to an original key from the
@@ -48,7 +45,7 @@
     private static SparseArray<String> sKeyHandles = new SparseArray<>();
 
     /**
-     * Basically the same as {@link #native_init$ravenwood}, but it'll only run if no values are
+     * Basically the same as {@link #init$ravenwood}, but it'll only run if no values are
      * set yet.
      */
     public static void initializeIfNeeded(Map<String, String> values,
@@ -57,30 +54,32 @@
             if (sValues != null) {
                 return; // Already initialized.
             }
-            native_init$ravenwood(values, keyReadablePredicate, keyWritablePredicate,
-                    () -> {});
+            init$ravenwood(values, keyReadablePredicate, keyWritablePredicate);
         }
     }
 
-    public static void native_init$ravenwood(Map<String, String> values,
-            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate,
-            Runnable changeCallback) {
+    public static void init$ravenwood(Map<String, String> values,
+            Predicate<String> keyReadablePredicate, Predicate<String> keyWritablePredicate) {
         synchronized (sLock) {
             sValues = Objects.requireNonNull(values);
             sKeyReadablePredicate = Objects.requireNonNull(keyReadablePredicate);
             sKeyWritablePredicate = Objects.requireNonNull(keyWritablePredicate);
-            sChangeCallback = Objects.requireNonNull(changeCallback);
             sKeyHandles.clear();
+            synchronized (SystemProperties.sChangeCallbacks) {
+                SystemProperties.sChangeCallbacks.clear();
+            }
         }
     }
 
-    public static void native_reset$ravenwood() {
+    public static void reset$ravenwood() {
         synchronized (sLock) {
             sValues = null;
             sKeyReadablePredicate = null;
             sKeyWritablePredicate = null;
-            sChangeCallback = null;
             sKeyHandles.clear();
+            synchronized (SystemProperties.sChangeCallbacks) {
+                SystemProperties.sChangeCallbacks.clear();
+            }
         }
     }
 
@@ -101,7 +100,7 @@
             } else {
                 sValues.put(key, val);
             }
-            sChangeCallback.run();
+            SystemProperties.callChangeCallbacks();
         }
     }
 
@@ -183,7 +182,7 @@
         // Report through callback always registered via init above
         synchronized (sLock) {
             Preconditions.requireNonNullViaRavenwoodRule(sValues);
-            sChangeCallback.run();
+            SystemProperties.callChangeCallbacks();
         }
     }
 
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
similarity index 83%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
index 55d4ffb..878a0ff 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/EventLog_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/EventLog_host.java
@@ -13,12 +13,11 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
 
 import com.android.internal.os.RuntimeInit;
 
 import java.io.PrintStream;
-import java.util.Collection;
 
 public class EventLog_host {
     public static int writeEvent(int tag, int value) {
@@ -58,15 +57,6 @@
         return sb.length();
     }
 
-    public static void readEvents(int[] tags, Collection<android.util.EventLog.Event> output) {
-        throw new UnsupportedOperationException();
-    }
-
-    public static void readEventsOnWrapping(int[] tags, long timestamp,
-            Collection<android.util.EventLog.Event> output) {
-        throw new UnsupportedOperationException();
-    }
-
     /**
      * Return the "real" {@code System.out} if it's been swapped by {@code RavenwoodRuleImpl}, so
      * that we don't end up in a recursive loop.
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
similarity index 95%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
rename to ravenwood/runtime-helper-src/framework/android/util/Log_host.java
index f301b9c..d232ef2 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Log_host.java
+++ b/ravenwood/runtime-helper-src/framework/android/util/Log_host.java
@@ -13,9 +13,8 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.platform.test.ravenwood.nativesubstitution;
+package android.util;
 
-import android.util.Log;
 import android.util.Log.Level;
 
 import com.android.internal.os.RuntimeInit;
@@ -44,7 +43,7 @@
             case Log.LOG_ID_SYSTEM: buffer = "system"; break;
             case Log.LOG_ID_CRASH: buffer = "crash"; break;
             default: buffer = "buf:" + bufID; break;
-        };
+        }
 
         final String prio;
         switch (priority) {
@@ -55,7 +54,7 @@
             case Log.ERROR: prio = "E"; break;
             case Log.ASSERT: prio = "A"; break;
             default: prio = "prio:" + priority; break;
-        };
+        }
 
         for (String s : msg.split("\\n")) {
             getRealOut().println(String.format("logd: [%s] %s %s: %s", buffer, prio, tag, s));
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
new file mode 100644
index 0000000..c18c307
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayContainer_host.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright (C) 2024 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.internal.os;
+
+import java.util.Arrays;
+import java.util.HashMap;
+
+public class LongArrayContainer_host {
+    private static final HashMap<Long, long[]> sInstances = new HashMap<>();
+    private static long sNextId = 1;
+
+    public static long native_init(int arrayLength) {
+        long[] array = new long[arrayLength];
+        long instanceId = sNextId++;
+        sInstances.put(instanceId, array);
+        return instanceId;
+    }
+
+    static long[] getInstance(long instanceId) {
+        return sInstances.get(instanceId);
+    }
+
+    public static void native_setValues(long instanceId, long[] values) {
+        System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
+    }
+
+    public static void native_getValues(long instanceId, long[] values) {
+        System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
+    }
+
+    public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
+        long[] values = getInstance(instanceId);
+
+        boolean nonZero = false;
+        Arrays.fill(array, 0);
+
+        for (int i = 0; i < values.length; i++) {
+            int index = indexMap[i];
+            if (index < 0 || index >= array.length) {
+                throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
+                        + (array.length - 1) + "]");
+            }
+            if (values[i] != 0) {
+                array[index] += values[i];
+                nonZero = true;
+            }
+        }
+        return nonZero;
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
similarity index 87%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
index 0f65544..9ce8ea8 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongArrayMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongArrayMultiStateCounter_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
@@ -28,7 +28,7 @@
 public class LongArrayMultiStateCounter_host {
 
     /**
-     * A reimplementation of {@link com.android.internal.os.LongArrayMultiStateCounter}, only in
+     * A reimplementation of {@link LongArrayMultiStateCounter}, only in
      * Java instead of native.  The majority of the code (in C++) can be found in
      * /frameworks/native/libs/battery/MultiStateCounter.h
      */
@@ -257,50 +257,6 @@
         }
     }
 
-    public static class LongArrayContainer_host {
-        private static final HashMap<Long, long[]> sInstances = new HashMap<>();
-        private static long sNextId = 1;
-
-        public static long native_init(int arrayLength) {
-            long[] array = new long[arrayLength];
-            long instanceId = sNextId++;
-            sInstances.put(instanceId, array);
-            return instanceId;
-        }
-
-        static long[] getInstance(long instanceId) {
-            return sInstances.get(instanceId);
-        }
-
-        public static void native_setValues(long instanceId, long[] values) {
-            System.arraycopy(values, 0, getInstance(instanceId), 0, values.length);
-        }
-
-        public static void native_getValues(long instanceId, long[] values) {
-            System.arraycopy(getInstance(instanceId), 0, values, 0, values.length);
-        }
-
-        public static boolean native_combineValues(long instanceId, long[] array, int[] indexMap) {
-            long[] values = getInstance(instanceId);
-
-            boolean nonZero = false;
-            Arrays.fill(array, 0);
-
-            for (int i = 0; i < values.length; i++) {
-                int index = indexMap[i];
-                if (index < 0 || index >= array.length) {
-                    throw new IndexOutOfBoundsException("Index " + index + " is out of bounds: [0, "
-                                                        + (array.length - 1) + "]");
-                }
-                if (values[i] != 0) {
-                    array[index] += values[i];
-                    nonZero = true;
-                }
-            }
-            return nonZero;
-        }
-    }
-
     private static final HashMap<Long, LongArrayMultiStateCounterRavenwood> sInstances =
             new HashMap<>();
     private static long sNextId = 1;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
similarity index 98%
rename from ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
rename to ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
index 9486651..1d95aa1 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/LongMultiStateCounter_host.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/os/LongMultiStateCounter_host.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.platform.test.ravenwood.nativesubstitution;
+package com.android.internal.os;
 
 import android.os.BadParcelableException;
 import android.os.Parcel;
diff --git a/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
new file mode 100644
index 0000000..e12ff24
--- /dev/null
+++ b/ravenwood/runtime-helper-src/framework/com/android/internal/ravenwood/RavenwoodEnvironment_host.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.internal.ravenwood;
+
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
+public class RavenwoodEnvironment_host {
+    private RavenwoodEnvironment_host() {
+    }
+
+    /**
+     * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
+     */
+    public static void ensureRavenwoodInitialized() {
+        // Initialization is now done by RavenwoodAwareTestRunner.
+        // Should we remove it?
+    }
+
+    /**
+     * Called from {@link RavenwoodEnvironment#getRavenwoodRuntimePath()}.
+     */
+    public static String getRavenwoodRuntimePath(RavenwoodEnvironment env) {
+        return RavenwoodCommonUtils.getRavenwoodRuntimePath();
+    }
+
+    /**
+     * Called from {@link RavenwoodEnvironment#fromAddress(long)}.
+     */
+    public static <T> T fromAddress(RavenwoodEnvironment env, long address) {
+        return JvmWorkaround.getInstance().fromAddress(address);
+    }
+}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
deleted file mode 100644
index 5a3589d..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/ParcelFileDescriptor_host.java
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2024 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.platform.test.ravenwood.nativesubstitution;
-
-import com.android.ravenwood.common.JvmWorkaround;
-
-import java.io.FileDescriptor;
-
-public class ParcelFileDescriptor_host {
-    public static void setFdInt(FileDescriptor fd, int fdInt) {
-        JvmWorkaround.getInstance().setFdInt(fd, fdInt);
-    }
-
-    public static int getFdInt(FileDescriptor fd) {
-        return JvmWorkaround.getInstance().getFdInt(fd);
-    }
-}
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
deleted file mode 100644
index 2df93cd..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/Parcel_host.java
+++ /dev/null
@@ -1,529 +0,0 @@
-/*
- * Copyright (C) 2023 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.platform.test.ravenwood.nativesubstitution;
-
-import android.system.ErrnoException;
-import android.system.Os;
-import android.util.Log;
-
-import java.io.FileDescriptor;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.Arrays;
-import java.util.Map;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.atomic.AtomicLong;
-
-/**
- * Tentative, partial implementation of the Parcel native methods, using Java's
- * {@code byte[]}.
- * (We don't use a {@link ByteBuffer} because there's enough semantics differences between Parcel
- * and {@link ByteBuffer}, and it didn't work out.
- * e.g. Parcel seems to allow moving the data position to be beyond its size? Which
- * {@link ByteBuffer} wouldn't allow...)
- */
-public class Parcel_host {
-    private static final String TAG = "Parcel";
-
-    private Parcel_host() {
-    }
-
-    private static final AtomicLong sNextId = new AtomicLong(1);
-
-    private static final Map<Long, Parcel_host> sInstances = new ConcurrentHashMap<>();
-
-    private boolean mDeleted = false;
-
-    private byte[] mBuffer;
-    private int mSize;
-    private int mPos;
-
-    private boolean mSensitive;
-    private boolean mAllowFds;
-
-    // TODO Use the actual value from Parcel.java.
-    private static final int OK = 0;
-
-    private final Map<Integer, FileDescriptor> mFdMap = new ConcurrentHashMap<>();
-
-    private static final int FD_PLACEHOLDER = 0xDEADBEEF;
-    private static final int FD_PAYLOAD_SIZE = 8;
-
-    private void validate() {
-        if (mDeleted) {
-            // TODO: Put more info
-            throw new RuntimeException("Parcel already destroyed");
-        }
-    }
-
-    private static Parcel_host getInstance(long id) {
-        Parcel_host p = sInstances.get(id);
-        if (p == null) {
-            // TODO: Put more info
-            throw new RuntimeException("Parcel doesn't exist with id=" + id);
-        }
-        p.validate();
-        return p;
-    }
-
-    /** Native method substitution */
-    public static long nativeCreate() {
-        final long id = sNextId.getAndIncrement();
-        final Parcel_host p = new Parcel_host();
-        sInstances.put(id, p);
-        p.init();
-        return id;
-    }
-
-    private void init() {
-        mBuffer = new byte[0];
-        mSize = 0;
-        mPos = 0;
-        mSensitive = false;
-        mAllowFds = true;
-        mFdMap.clear();
-    }
-
-    private void updateSize() {
-        if (mSize < mPos) {
-            mSize = mPos;
-        }
-    }
-
-    /** Native method substitution */
-    public static void nativeDestroy(long nativePtr) {
-        getInstance(nativePtr).mDeleted = true;
-        sInstances.remove(nativePtr);
-    }
-
-    /** Native method substitution */
-    public static void nativeFreeBuffer(long nativePtr) {
-        getInstance(nativePtr).freeBuffer();
-    }
-
-    /** Native method substitution */
-    private void freeBuffer() {
-        init();
-    }
-
-    private int getCapacity() {
-        return mBuffer.length;
-    }
-
-    private void ensureMoreCapacity(int size) {
-        ensureCapacity(mPos + size);
-    }
-
-    private void ensureCapacity(int targetSize) {
-        if (targetSize <= getCapacity()) {
-            return;
-        }
-        var newSize = getCapacity() * 2;
-        if (newSize < targetSize) {
-            newSize = targetSize;
-        }
-        forceSetCapacity(newSize);
-    }
-
-    private void forceSetCapacity(int newSize) {
-        var newBuf = new byte[newSize];
-
-        // Copy
-        System.arraycopy(mBuffer, 0, newBuf, 0, Math.min(newSize, getCapacity()));
-
-        this.mBuffer = newBuf;
-    }
-
-    private void ensureDataAvailable(int requestSize) {
-        if (mSize - mPos < requestSize) {
-            throw new RuntimeException(String.format(
-                    "Pacel data underflow. size=%d, pos=%d, request=%d", mSize, mPos, requestSize));
-        }
-    }
-
-    /** Native method substitution */
-    public static void nativeMarkSensitive(long nativePtr) {
-        getInstance(nativePtr).mSensitive = true;
-    }
-
-    /** Native method substitution */
-    public static int nativeDataSize(long nativePtr) {
-        return getInstance(nativePtr).mSize;
-    }
-
-    /** Native method substitution */
-    public static int nativeDataAvail(long nativePtr) {
-        var p = getInstance(nativePtr);
-        return p.mSize - p.mPos;
-    }
-
-    /** Native method substitution */
-    public static int nativeDataPosition(long nativePtr) {
-        return getInstance(nativePtr).mPos;
-    }
-
-    /** Native method substitution */
-    public static int nativeDataCapacity(long nativePtr) {
-        return getInstance(nativePtr).mBuffer.length;
-    }
-
-    /** Native method substitution */
-    public static void nativeSetDataSize(long nativePtr, int size) {
-        var p = getInstance(nativePtr);
-        p.ensureCapacity(size);
-        getInstance(nativePtr).mSize = size;
-    }
-
-    /** Native method substitution */
-    public static void nativeSetDataPosition(long nativePtr, int pos) {
-        var p = getInstance(nativePtr);
-        // TODO: Should this change the size or the capacity??
-        p.mPos = pos;
-    }
-
-    /** Native method substitution */
-    public static void nativeSetDataCapacity(long nativePtr, int size) {
-        if (size < 0) {
-            throw new IllegalArgumentException("size < 0: size=" + size);
-        }
-        var p = getInstance(nativePtr);
-        if (p.getCapacity() < size) {
-            p.forceSetCapacity(size);
-        }
-    }
-
-    /** Native method substitution */
-    public static boolean nativePushAllowFds(long nativePtr, boolean allowFds) {
-        var p = getInstance(nativePtr);
-        var prev = p.mAllowFds;
-        p.mAllowFds = allowFds;
-        return prev;
-    }
-
-    /** Native method substitution */
-    public static void nativeRestoreAllowFds(long nativePtr, boolean lastValue) {
-        getInstance(nativePtr).mAllowFds = lastValue;
-    }
-
-    /** Native method substitution */
-    public static void nativeWriteByteArray(long nativePtr, byte[] b, int offset, int len) {
-        nativeWriteBlob(nativePtr, b, offset, len);
-    }
-
-    /** Native method substitution */
-    public static void nativeWriteBlob(long nativePtr, byte[] b, int offset, int len) {
-        var p = getInstance(nativePtr);
-
-        if (b == null) {
-            nativeWriteInt(nativePtr, -1);
-        } else {
-            final var alignedSize = align4(len);
-
-            nativeWriteInt(nativePtr, len);
-
-            p.ensureMoreCapacity(alignedSize);
-
-            System.arraycopy(b, offset, p.mBuffer,  p.mPos, len);
-            p.mPos += alignedSize;
-            p.updateSize();
-        }
-    }
-
-    /** Native method substitution */
-    public static int nativeWriteInt(long nativePtr, int value) {
-        var p = getInstance(nativePtr);
-        p.ensureMoreCapacity(Integer.BYTES);
-
-        p.mBuffer[p.mPos++] = (byte) ((value >> 24) & 0xff);
-        p.mBuffer[p.mPos++] = (byte) ((value >> 16) & 0xff);
-        p.mBuffer[p.mPos++] = (byte) ((value >>  8) & 0xff);
-        p.mBuffer[p.mPos++] = (byte) ((value >>  0) & 0xff);
-
-        p.updateSize();
-
-        return OK;
-    }
-
-    /** Native method substitution */
-    public static int nativeWriteLong(long nativePtr, long value) {
-        nativeWriteInt(nativePtr, (int) (value >>> 32));
-        nativeWriteInt(nativePtr, (int) (value));
-        return OK;
-    }
-
-    /** Native method substitution */
-    public static int nativeWriteFloat(long nativePtr, float val) {
-        return nativeWriteInt(nativePtr, Float.floatToIntBits(val));
-    }
-
-    /** Native method substitution */
-    public static int nativeWriteDouble(long nativePtr, double val) {
-        return nativeWriteLong(nativePtr, Double.doubleToLongBits(val));
-    }
-
-    private static int align4(int val) {
-        return ((val + 3) / 4) * 4;
-    }
-
-    /** Native method substitution */
-    public static void nativeWriteString8(long nativePtr, String val) {
-        if (val == null) {
-            nativeWriteBlob(nativePtr, null, 0, 0);
-        } else {
-            var bytes = val.getBytes(StandardCharsets.UTF_8);
-            nativeWriteBlob(nativePtr, bytes, 0, bytes.length);
-        }
-    }
-
-    /** Native method substitution */
-    public static void nativeWriteString16(long nativePtr, String val) {
-        // Just reuse String8
-        nativeWriteString8(nativePtr, val);
-    }
-
-    /** Native method substitution */
-    public static byte[] nativeCreateByteArray(long nativePtr) {
-        return nativeReadBlob(nativePtr);
-    }
-
-    /** Native method substitution */
-    public static boolean nativeReadByteArray(long nativePtr, byte[] dest, int destLen) {
-        if (dest == null) {
-            return false;
-        }
-        var data = nativeReadBlob(nativePtr);
-        if (data == null) {
-            System.err.println("Percel has NULL, which is unexpected."); // TODO: Is this correct?
-            return false;
-        }
-        // TODO: Make sure the check logic is correct.
-        if (data.length != destLen) {
-            System.err.println("Byte array size mismatch: expected="
-                    + data.length + " given=" + destLen);
-            return false;
-        }
-        System.arraycopy(data, 0, dest, 0, data.length);
-        return true;
-    }
-
-    /** Native method substitution */
-    public static byte[] nativeReadBlob(long nativePtr) {
-        var p = getInstance(nativePtr);
-        if (p.mSize - p.mPos < 4) {
-            // Match native impl that returns "null" when not enough data
-            return null;
-        }
-        final var size = nativeReadInt(nativePtr);
-        if (size == -1) {
-            return null;
-        }
-        try {
-            p.ensureDataAvailable(align4(size));
-        } catch (Exception e) {
-            System.err.println(e.toString());
-            return null;
-        }
-
-        var bytes = new byte[size];
-        System.arraycopy(p.mBuffer, p.mPos, bytes, 0, size);
-
-        p.mPos += align4(size);
-
-        return bytes;
-    }
-
-    /** Native method substitution */
-    public static int nativeReadInt(long nativePtr) {
-        var p = getInstance(nativePtr);
-
-        if (p.mSize - p.mPos < 4) {
-            // Match native impl that returns "0" when not enough data
-            return 0;
-        }
-
-        var ret = (((p.mBuffer[p.mPos++] & 0xff) << 24)
-                | ((p.mBuffer[p.mPos++] & 0xff) << 16)
-                | ((p.mBuffer[p.mPos++] & 0xff) <<  8)
-                | ((p.mBuffer[p.mPos++] & 0xff) <<  0));
-
-        return ret;
-    }
-
-    /** Native method substitution */
-    public static long nativeReadLong(long nativePtr) {
-        return (((long) nativeReadInt(nativePtr)) << 32)
-                | (((long) nativeReadInt(nativePtr)) & 0xffff_ffffL);
-    }
-
-    /** Native method substitution */
-    public static float nativeReadFloat(long nativePtr) {
-        return Float.intBitsToFloat(nativeReadInt(nativePtr));
-    }
-
-    /** Native method substitution */
-    public static double nativeReadDouble(long nativePtr) {
-        return Double.longBitsToDouble(nativeReadLong(nativePtr));
-    }
-
-    /** Native method substitution */
-    public static String nativeReadString8(long nativePtr) {
-        final var bytes = nativeReadBlob(nativePtr);
-        if (bytes == null) {
-            return null;
-        }
-        return new String(bytes, StandardCharsets.UTF_8);
-    }
-    public static String nativeReadString16(long nativePtr) {
-        return nativeReadString8(nativePtr);
-    }
-
-    /** Native method substitution */
-    public static byte[] nativeMarshall(long nativePtr) {
-        var p = getInstance(nativePtr);
-        return Arrays.copyOf(p.mBuffer, p.mSize);
-    }
-
-    /** Native method substitution */
-    public static void nativeUnmarshall(
-            long nativePtr, byte[] data, int offset, int length) {
-        var p = getInstance(nativePtr);
-        p.ensureMoreCapacity(length);
-        System.arraycopy(data, offset, p.mBuffer, p.mPos, length);
-        p.mPos += length;
-        p.updateSize();
-    }
-
-    /** Native method substitution */
-    public static int nativeCompareData(long thisNativePtr, long otherNativePtr) {
-        var a = getInstance(thisNativePtr);
-        var b = getInstance(otherNativePtr);
-        if ((a.mSize == b.mSize) && Arrays.equals(a.mBuffer, b.mBuffer)) {
-            return 0;
-        } else {
-            return -1;
-        }
-    }
-
-    /** Native method substitution */
-    public static boolean nativeCompareDataInRange(
-            long ptrA, int offsetA, long ptrB, int offsetB, int length) {
-        var a = getInstance(ptrA);
-        var b = getInstance(ptrB);
-        if (offsetA < 0 || offsetA + length > a.mSize) {
-            throw new IllegalArgumentException();
-        }
-        if (offsetB < 0 || offsetB + length > b.mSize) {
-            throw new IllegalArgumentException();
-        }
-        return Arrays.equals(Arrays.copyOfRange(a.mBuffer, offsetA, offsetA + length),
-                Arrays.copyOfRange(b.mBuffer, offsetB, offsetB + length));
-    }
-
-    /** Native method substitution */
-    public static void nativeAppendFrom(
-            long thisNativePtr, long otherNativePtr, int srcOffset, int length) {
-        var dst = getInstance(thisNativePtr);
-        var src = getInstance(otherNativePtr);
-
-        dst.ensureMoreCapacity(length);
-
-        System.arraycopy(src.mBuffer, srcOffset, dst.mBuffer, dst.mPos, length);
-        dst.mPos += length; // TODO: 4 byte align?
-        dst.updateSize();
-
-        // TODO: Update the other's position?
-    }
-
-    /** Native method substitution */
-    public static boolean nativeHasBinders(long nativePtr) {
-        // Assume false for now, because we don't support adding binders.
-        return false;
-    }
-
-    /** Native method substitution */
-    public static boolean nativeHasBindersInRange(
-            long nativePtr, int offset, int length) {
-        // Assume false for now, because we don't support writing FDs yet.
-        return false;
-    }
-
-    /** Native method substitution */
-    public static void nativeWriteFileDescriptor(long nativePtr, java.io.FileDescriptor val) {
-        var p = getInstance(nativePtr);
-
-        if (!p.mAllowFds) {
-            // Simulate the FDS_NOT_ALLOWED case in frameworks/base/core/jni/android_util_Binder.cpp
-            throw new RuntimeException("Not allowed to write file descriptors here");
-        }
-
-        FileDescriptor dup = null;
-        try {
-            dup = Os.dup(val);
-        } catch (ErrnoException e) {
-            throw new RuntimeException(e);
-        }
-        p.mFdMap.put(p.mPos, dup);
-
-        // Parcel.cpp writes two int32s for a FD.
-        // Make sure FD_PAYLOAD_SIZE is in sync with this code.
-        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
-        nativeWriteInt(nativePtr, FD_PLACEHOLDER);
-    }
-
-    /** Native method substitution */
-    public static java.io.FileDescriptor nativeReadFileDescriptor(long nativePtr) {
-        var p = getInstance(nativePtr);
-
-        var pos = p.mPos;
-        var fd = p.mFdMap.get(pos);
-
-        if (fd == null) {
-            Log.w(TAG, "nativeReadFileDescriptor: Not a FD at pos #" + pos);
-            return null;
-        }
-        nativeReadInt(nativePtr);
-        return fd;
-    }
-
-    /** Native method substitution */
-    public static boolean nativeHasFileDescriptors(long nativePtr) {
-        var p = getInstance(nativePtr);
-        return p.mFdMap.size() > 0;
-    }
-
-    /** Native method substitution */
-    public static boolean nativeHasFileDescriptorsInRange(long nativePtr, int offset, int length) {
-        var p = getInstance(nativePtr);
-
-        // Original code: hasFileDescriptorsInRange() in frameworks/native/libs/binder/Parcel.cpp
-        if (offset < 0 || length < 0) {
-            throw new IllegalArgumentException("Negative value not allowed: offset=" + offset
-                    + " length=" + length);
-        }
-        long limit = (long) offset + (long) length;
-        if (limit > p.mSize) {
-            throw new IllegalArgumentException("Out of range: offset=" + offset
-                    + " length=" + length + " dataSize=" + p.mSize);
-        }
-
-        for (var pos : p.mFdMap.keySet()) {
-            if (offset <= pos && (pos + FD_PAYLOAD_SIZE - 1) < (offset + length)) {
-                return true;
-            }
-        }
-        return false;
-    }
-}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
deleted file mode 100644
index b00cee0..0000000
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/nativesubstitution/RavenwoodEnvironment_host.java
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2024 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.platform.test.ravenwood.nativesubstitution;
-
-import android.platform.test.ravenwood.RavenwoodSystemProperties;
-import android.util.Log;
-
-import com.android.internal.ravenwood.RavenwoodEnvironment;
-import com.android.ravenwood.common.RavenwoodCommonUtils;
-
-public class RavenwoodEnvironment_host {
-    private static final String TAG = RavenwoodEnvironment.TAG;
-
-    private static final Object sInitializeLock = new Object();
-
-    // @GuardedBy("sInitializeLock")
-    private static boolean sInitialized;
-
-    private RavenwoodEnvironment_host() {
-    }
-
-    /**
-     * Called from {@link RavenwoodEnvironment#ensureRavenwoodInitialized()}.
-     */
-    public static void ensureRavenwoodInitializedInternal() {
-        synchronized (sInitializeLock) {
-            if (sInitialized) {
-                return;
-            }
-            Log.i(TAG, "Initializing Ravenwood environment");
-
-            // Set the default values.
-            var sysProps = RavenwoodSystemProperties.DEFAULT_VALUES;
-
-            // We have a method that does it in RavenwoodRuleImpl, but we can't use that class
-            // here, So just inline it.
-            SystemProperties_host.initializeIfNeeded(
-                    sysProps.getValues(),
-                    sysProps.getKeyReadablePredicate(),
-                    sysProps.getKeyWritablePredicate());
-
-            sInitialized = true;
-        }
-    }
-}
\ No newline at end of file
diff --git a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
index e198646..be8c443 100644
--- a/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
+++ b/ravenwood/runtime-helper-src/framework/com/android/platform/test/ravenwood/runtimehelper/ClassLoadHook.java
@@ -15,49 +15,10 @@
  */
 package com.android.platform.test.ravenwood.runtimehelper;
 
-import com.android.ravenwood.common.RavenwoodCommonUtils;
-
-import java.io.File;
-import java.lang.reflect.Modifier;
-import java.util.ArrayList;
-
 /**
  * Standard class loader hook.
- *
- * Currently, we use this class to load libandroid_runtime (if needed). In the future, we may
- * load other JNI or do other set up here.
  */
 public class ClassLoadHook {
-    /**
-     * If true, we won't load `libandroid_runtime`
-     *
-     * <p>Looks like there's some complexity in running a host test with JNI with `atest`,
-     * so we need a way to remove the dependency.
-     */
-    private static final boolean SKIP_LOADING_LIBANDROID = "1".equals(System.getenv(
-            "RAVENWOOD_SKIP_LOADING_LIBANDROID"));
-
-    public static final String CORE_NATIVE_CLASSES = "core_native_classes";
-    public static final String ICU_DATA_PATH = "icu.data.path";
-    public static final String KEYBOARD_PATHS = "keyboard_paths";
-    public static final String GRAPHICS_NATIVE_CLASSES = "graphics_native_classes";
-
-    public static final String LIBANDROID_RUNTIME_NAME = "android_runtime";
-
-    /**
-     * Extra strings needed to pass to register_android_graphics_classes().
-     *
-     * `android.graphics.Graphics` is not actually a class, so we can't use the same initialization
-     * strategy than the "normal" classes. So we just hardcode it here.
-     */
-    public static final String GRAPHICS_EXTRA_INIT_PARAMS = ",android.graphics.Graphics";
-
-    private static String sInitialDir = new File("").getAbsolutePath();
-
-    static {
-        log("Initialized. Current dir=" + sInitialDir);
-    }
-
     private ClassLoadHook() {
     }
 
@@ -70,129 +31,12 @@
     public static void onClassLoaded(Class<?> clazz) {
         System.out.println("Framework class loaded: " + clazz.getCanonicalName());
 
-        loadFrameworkNativeCode();
-    }
-
-    private static void log(String message) {
-        System.out.println("ClassLoadHook: " + message);
-    }
-
-    private static void log(String fmt, Object... args) {
-        log(String.format(fmt, args));
-    }
-
-    private static void ensurePropertyNotSet(String key) {
-        if (System.getProperty(key) != null) {
-            throw new RuntimeException("System property \"" + key + "\" is set unexpectedly");
+        // Always try to initialize the environment in case classes are loaded before
+        // RavenwoodAwareTestRunner is initialized
+        try {
+            Class.forName("android.platform.test.ravenwood.RavenwoodRuntimeEnvironmentController")
+                    .getMethod("globalInitOnce").invoke(null);
+        } catch (ReflectiveOperationException ignored) {
         }
     }
-
-    private static void setProperty(String key, String value) {
-        System.setProperty(key, value);
-        log("Property set: %s=\"%s\"", key, value);
-    }
-
-    private static void dumpSystemProperties() {
-        for (var prop : System.getProperties().entrySet()) {
-            log("  %s=\"%s\"", prop.getKey(), prop.getValue());
-        }
-    }
-
-    private static boolean sLoadFrameworkNativeCodeCalled = false;
-
-    /**
-     * Load `libandroid_runtime` if needed.
-     */
-    private static void loadFrameworkNativeCode() {
-        // This is called from class-initializers, so no synchronization is needed.
-        if (sLoadFrameworkNativeCodeCalled) {
-            return;
-        }
-        sLoadFrameworkNativeCodeCalled = true;
-
-        // libandroid_runtime uses Java's system properties to decide what JNI methods to set up.
-        // Set up these properties for host-side tests.
-
-        if ("1".equals(System.getenv("RAVENWOOD_DUMP_PROPERTIES"))) {
-            log("Java system properties:");
-            dumpSystemProperties();
-        }
-
-        if (SKIP_LOADING_LIBANDROID) {
-            log("Skip loading native runtime.");
-            return;
-        }
-
-        // Make sure these properties are not set.
-        ensurePropertyNotSet(CORE_NATIVE_CLASSES);
-        ensurePropertyNotSet(ICU_DATA_PATH);
-        ensurePropertyNotSet(KEYBOARD_PATHS);
-        ensurePropertyNotSet(GRAPHICS_NATIVE_CLASSES);
-
-        // Load the libraries, if needed.
-        final var libanrdoidClasses = getClassesWithNativeMethods(sLibandroidClasses);
-        final var libhwuiClasses = getClassesWithNativeMethods(sLibhwuiClasses);
-        if (libanrdoidClasses.isEmpty() && libhwuiClasses.isEmpty()) {
-            log("No classes require JNI methods, skip loading native runtime.");
-            return;
-        }
-        setProperty(CORE_NATIVE_CLASSES, libanrdoidClasses);
-        setProperty(GRAPHICS_NATIVE_CLASSES, libhwuiClasses + GRAPHICS_EXTRA_INIT_PARAMS);
-
-        log("Loading " + LIBANDROID_RUNTIME_NAME + " for '" + libanrdoidClasses + "' and '"
-                + libhwuiClasses + "'");
-        RavenwoodCommonUtils.loadJniLibrary(LIBANDROID_RUNTIME_NAME);
-    }
-
-    /**
-     * Classes with native methods that are backed by libandroid_runtime.
-     *
-     * See frameworks/base/core/jni/platform/host/HostRuntime.cpp
-     */
-    private static final Class<?>[] sLibandroidClasses = {
-            android.util.Log.class,
-    };
-
-    /**
-     * Classes with native methods that are backed by libhwui.
-     *
-     * See frameworks/base/libs/hwui/apex/LayoutlibLoader.cpp
-     */
-    private static final Class<?>[] sLibhwuiClasses = {
-            android.graphics.Interpolator.class,
-            android.graphics.Matrix.class,
-            android.graphics.Path.class,
-            android.graphics.Color.class,
-            android.graphics.ColorSpace.class,
-    };
-
-    /**
-     * @return if a given class and its nested classes, if any, have any native method or not.
-     */
-    private static boolean hasNativeMethod(Class<?> clazz) {
-        for (var nestedClass : clazz.getNestMembers()) {
-            for (var method : nestedClass.getDeclaredMethods()) {
-                if (Modifier.isNative(method.getModifiers())) {
-                    return true;
-                }
-            }
-        }
-        return false;
-    }
-    /**
-     * Create a list of classes as comma-separated that require JNI methods to be set up from
-     * a given class list, ignoring classes with no native methods.
-     */
-    private static String getClassesWithNativeMethods(Class<?>[] classes) {
-        final var coreNativeClassesToLoad = new ArrayList<String>();
-
-        for (var clazz : classes) {
-            if (hasNativeMethod(clazz)) {
-                log("Class %s has native methods", clazz.getCanonicalName());
-                coreNativeClassesToLoad.add(clazz.getName());
-            }
-        }
-
-        return String.join(",", coreNativeClassesToLoad);
-    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
index ecaa816..c94ef31 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/android/system/Os.java
@@ -15,11 +15,15 @@
  */
 package android.system;
 
+import com.android.ravenwood.RavenwoodRuntimeNative;
 import com.android.ravenwood.common.JvmWorkaround;
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
 
 import java.io.FileDescriptor;
+import java.io.FileInputStream;
 import java.io.IOException;
+import java.io.InterruptedIOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.AsynchronousCloseException;
 
 /**
  * OS class replacement used on Ravenwood. For now, we just implement APIs as we need them...
@@ -36,6 +40,11 @@
         return RavenwoodRuntimeNative.pipe2(flags);
     }
 
+    /** Ravenwood version of the OS API. */
+    public static FileDescriptor[] pipe() throws ErrnoException {
+        return RavenwoodRuntimeNative.pipe2(0);
+    }
+
     public static FileDescriptor dup(FileDescriptor fd) throws ErrnoException {
         return RavenwoodRuntimeNative.dup(fd);
     }
@@ -69,4 +78,23 @@
     public static FileDescriptor open(String path, int flags, int mode) throws ErrnoException {
         return RavenwoodRuntimeNative.open(path, flags, mode);
     }
+
+    /** Ravenwood version of the OS API. */
+    public static int pread(FileDescriptor fd, byte[] bytes, int byteOffset, int byteCount,
+            long offset) throws ErrnoException, InterruptedIOException {
+        var channel = new FileInputStream(fd).getChannel();
+        var buf = ByteBuffer.wrap(bytes, byteOffset, byteCount);
+        try {
+            return channel.read(buf, offset);
+        } catch (AsynchronousCloseException e) {
+            throw new InterruptedIOException(e.getMessage());
+        } catch (IOException e) {
+            // Most likely EIO
+            throw new ErrnoException("pread", OsConstants.EIO, e);
+        }
+    }
+
+    public static void setenv(String name, String value, boolean overwrite) throws ErrnoException {
+        RavenwoodRuntimeNative.setenv(name, value, overwrite);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
new file mode 100644
index 0000000..96aed4b
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodJdkPatch.java
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2024 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.ravenwood;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
+import java.io.FileDescriptor;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+/**
+ * Class to host APIs that exist in libcore, but not in standard JRE.
+ */
+public class RavenwoodJdkPatch {
+    /**
+     * Implements FileDescriptor.getInt$()
+     */
+    public static int getInt$(FileDescriptor fd) {
+        return JvmWorkaround.getInstance().getFdInt(fd);
+    }
+
+    /**
+     * Implements FileDescriptor.setInt$(int)
+     */
+    public static void setInt$(FileDescriptor fd, int rawFd) {
+        JvmWorkaround.getInstance().setFdInt(fd, rawFd);
+    }
+
+    /**
+     * Implements LinkedHashMap.eldest()
+     */
+    public static <K, V> Map.Entry<K, V> eldest(LinkedHashMap<K, V> map) {
+        final var it = map.entrySet().iterator();
+        return it.hasNext() ? it.next() : null;
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
similarity index 92%
rename from ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
rename to ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
index beba8339..ad80d92 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/common/RavenwoodRuntimeNative.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/com/android/ravenwood/RavenwoodRuntimeNative.java
@@ -13,11 +13,14 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.common;
+package com.android.ravenwood;
 
 import android.system.ErrnoException;
 import android.system.StructStat;
 
+import com.android.ravenwood.common.JvmWorkaround;
+import com.android.ravenwood.common.RavenwoodCommonUtils;
+
 import java.io.FileDescriptor;
 
 /**
@@ -50,6 +53,9 @@
 
     private static native int nOpen(String path, int flags, int mode) throws ErrnoException;
 
+    public static native void setenv(String name, String value, boolean overwrite)
+            throws ErrnoException;
+
     public static long lseek(FileDescriptor fd, long offset, int whence) throws ErrnoException {
         return nLseek(JvmWorkaround.getInstance().getFdInt(fd), offset, whence);
     }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
index 7d2b00d..ba89f71 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/dalvik/system/VMRuntime.java
@@ -19,6 +19,8 @@
 // The original is here:
 // $ANDROID_BUILD_TOP/libcore/libart/src/main/java/dalvik/system/VMRuntime.java
 
+import com.android.ravenwood.common.JvmWorkaround;
+
 import java.lang.reflect.Array;
 
 public class VMRuntime {
@@ -32,14 +34,22 @@
     }
 
     public boolean is64Bit() {
-        return true;
+        return "amd64".equals(System.getProperty("os.arch"));
     }
 
     public static boolean is64BitAbi(String abi) {
-        return true;
+        return abi.contains("64");
     }
 
     public Object newUnpaddedArray(Class<?> componentType, int minLength) {
         return Array.newInstance(componentType, minLength);
     }
+
+    public Object newNonMovableArray(Class<?> componentType, int length) {
+        return Array.newInstance(componentType, length);
+    }
+
+    public long addressOf(Object obj) {
+        return JvmWorkaround.getInstance().addressOf(obj);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
index 65c285e..2bd1ae8 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/io/IoUtils.java
@@ -16,7 +16,13 @@
 
 package libcore.io;
 
+import android.system.ErrnoException;
+import android.system.Os;
+
+import com.android.ravenwood.common.JvmWorkaround;
+
 import java.io.File;
+import java.io.FileDescriptor;
 import java.io.IOException;
 import java.net.Socket;
 
@@ -47,6 +53,13 @@
         }
     }
 
+    public static void closeQuietly(FileDescriptor fd) {
+        try {
+            Os.close(fd);
+        } catch (ErrnoException ignored) {
+        }
+    }
+
     public static void deleteContents(File dir) throws IOException {
         File[] files = dir.listFiles();
         if (files != null) {
@@ -58,4 +71,17 @@
             }
         }
     }
+
+    /**
+     * FD owners currently unsupported under Ravenwood; ignored
+     */
+    public static void setFdOwner(FileDescriptor fd, Object owner) {
+    }
+
+    /**
+     * FD owners currently unsupported under Ravenwood; return FD directly
+     */
+    public static int acquireRawFd(FileDescriptor fd) {
+        return JvmWorkaround.getInstance().getFdInt(fd);
+    }
 }
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java
new file mode 100644
index 0000000..478503b
--- /dev/null
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/FP16.java
@@ -0,0 +1,814 @@
+/*
+ * Copyright (C) 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 libcore.util;
+
+/**
+ * <p>The {@code FP16} class is a wrapper and a utility class to manipulate half-precision 16-bit
+ * <a href="https://en.wikipedia.org/wiki/Half-precision_floating-point_format">IEEE 754</a>
+ * floating point data types (also called fp16 or binary16). A half-precision float can be
+ * created from or converted to single-precision floats, and is stored in a short data type.
+ *
+ * <p>The IEEE 754 standard specifies an fp16 as having the following format:</p>
+ * <ul>
+ * <li>Sign bit: 1 bit</li>
+ * <li>Exponent width: 5 bits</li>
+ * <li>Significand: 10 bits</li>
+ * </ul>
+ *
+ * <p>The format is laid out as follows:</p>
+ * <pre>
+ * 1   11111   1111111111
+ * ^   --^--   -----^----
+ * sign  |          |_______ significand
+ *       |
+ *       -- exponent
+ * </pre>
+ *
+ * <p>Half-precision floating points can be useful to save memory and/or
+ * bandwidth at the expense of range and precision when compared to single-precision
+ * floating points (fp32).</p>
+ * <p>To help you decide whether fp16 is the right storage type for you need, please
+ * refer to the table below that shows the available precision throughout the range of
+ * possible values. The <em>precision</em> column indicates the step size between two
+ * consecutive numbers in a specific part of the range.</p>
+ *
+ * <table summary="Precision of fp16 across the range">
+ *     <tr><th>Range start</th><th>Precision</th></tr>
+ *     <tr><td>0</td><td>1 &frasl; 16,777,216</td></tr>
+ *     <tr><td>1 &frasl; 16,384</td><td>1 &frasl; 16,777,216</td></tr>
+ *     <tr><td>1 &frasl; 8,192</td><td>1 &frasl; 8,388,608</td></tr>
+ *     <tr><td>1 &frasl; 4,096</td><td>1 &frasl; 4,194,304</td></tr>
+ *     <tr><td>1 &frasl; 2,048</td><td>1 &frasl; 2,097,152</td></tr>
+ *     <tr><td>1 &frasl; 1,024</td><td>1 &frasl; 1,048,576</td></tr>
+ *     <tr><td>1 &frasl; 512</td><td>1 &frasl; 524,288</td></tr>
+ *     <tr><td>1 &frasl; 256</td><td>1 &frasl; 262,144</td></tr>
+ *     <tr><td>1 &frasl; 128</td><td>1 &frasl; 131,072</td></tr>
+ *     <tr><td>1 &frasl; 64</td><td>1 &frasl; 65,536</td></tr>
+ *     <tr><td>1 &frasl; 32</td><td>1 &frasl; 32,768</td></tr>
+ *     <tr><td>1 &frasl; 16</td><td>1 &frasl; 16,384</td></tr>
+ *     <tr><td>1 &frasl; 8</td><td>1 &frasl; 8,192</td></tr>
+ *     <tr><td>1 &frasl; 4</td><td>1 &frasl; 4,096</td></tr>
+ *     <tr><td>1 &frasl; 2</td><td>1 &frasl; 2,048</td></tr>
+ *     <tr><td>1</td><td>1 &frasl; 1,024</td></tr>
+ *     <tr><td>2</td><td>1 &frasl; 512</td></tr>
+ *     <tr><td>4</td><td>1 &frasl; 256</td></tr>
+ *     <tr><td>8</td><td>1 &frasl; 128</td></tr>
+ *     <tr><td>16</td><td>1 &frasl; 64</td></tr>
+ *     <tr><td>32</td><td>1 &frasl; 32</td></tr>
+ *     <tr><td>64</td><td>1 &frasl; 16</td></tr>
+ *     <tr><td>128</td><td>1 &frasl; 8</td></tr>
+ *     <tr><td>256</td><td>1 &frasl; 4</td></tr>
+ *     <tr><td>512</td><td>1 &frasl; 2</td></tr>
+ *     <tr><td>1,024</td><td>1</td></tr>
+ *     <tr><td>2,048</td><td>2</td></tr>
+ *     <tr><td>4,096</td><td>4</td></tr>
+ *     <tr><td>8,192</td><td>8</td></tr>
+ *     <tr><td>16,384</td><td>16</td></tr>
+ *     <tr><td>32,768</td><td>32</td></tr>
+ * </table>
+ *
+ * <p>This table shows that numbers higher than 1024 lose all fractional precision.</p>
+ *
+ * @hide
+ */
+
+public final class FP16 {
+    /**
+     * The number of bits used to represent a half-precision float value.
+     *
+     * @hide
+     */
+    public static final int SIZE = 16;
+
+    /**
+     * Epsilon is the difference between 1.0 and the next value representable
+     * by a half-precision floating-point.
+     *
+     * @hide
+     */
+    public static final short EPSILON = (short) 0x1400;
+
+    /**
+     * Maximum exponent a finite half-precision float may have.
+     *
+     * @hide
+     */
+    public static final int MAX_EXPONENT = 15;
+    /**
+     * Minimum exponent a normalized half-precision float may have.
+     *
+     * @hide
+     */
+    public static final int MIN_EXPONENT = -14;
+
+    /**
+     * Smallest negative value a half-precision float may have.
+     *
+     * @hide
+     */
+    public static final short LOWEST_VALUE = (short) 0xfbff;
+    /**
+     * Maximum positive finite value a half-precision float may have.
+     *
+     * @hide
+     */
+    public static final short MAX_VALUE = (short) 0x7bff;
+    /**
+     * Smallest positive normal value a half-precision float may have.
+     *
+     * @hide
+     */
+    public static final short MIN_NORMAL = (short) 0x0400;
+    /**
+     * Smallest positive non-zero value a half-precision float may have.
+     *
+     * @hide
+     */
+    public static final short MIN_VALUE = (short) 0x0001;
+    /**
+     * A Not-a-Number representation of a half-precision float.
+     *
+     * @hide
+     */
+    public static final short NaN = (short) 0x7e00;
+    /**
+     * Negative infinity of type half-precision float.
+     *
+     * @hide
+     */
+    public static final short NEGATIVE_INFINITY = (short) 0xfc00;
+    /**
+     * Negative 0 of type half-precision float.
+     *
+     * @hide
+     */
+    public static final short NEGATIVE_ZERO = (short) 0x8000;
+    /**
+     * Positive infinity of type half-precision float.
+     *
+     * @hide
+     */
+    public static final short POSITIVE_INFINITY = (short) 0x7c00;
+    /**
+     * Positive 0 of type half-precision float.
+     *
+     * @hide
+     */
+    public static final short POSITIVE_ZERO = (short) 0x0000;
+
+    /**
+     * The offset to shift by to obtain the sign bit.
+     *
+     * @hide
+     */
+    public static final int SIGN_SHIFT                = 15;
+
+    /**
+     * The offset to shift by to obtain the exponent bits.
+     *
+     * @hide
+     */
+    public static final int EXPONENT_SHIFT            = 10;
+
+    /**
+     * The bitmask to AND a number with to obtain the sign bit.
+     *
+     * @hide
+     */
+    public static final int SIGN_MASK                 = 0x8000;
+
+    /**
+     * The bitmask to AND a number shifted by {@link #EXPONENT_SHIFT} right, to obtain exponent bits.
+     *
+     * @hide
+     */
+    public static final int SHIFTED_EXPONENT_MASK     = 0x1f;
+
+    /**
+     * The bitmask to AND a number with to obtain significand bits.
+     *
+     * @hide
+     */
+    public static final int SIGNIFICAND_MASK          = 0x3ff;
+
+    /**
+     * The bitmask to AND with to obtain exponent and significand bits.
+     *
+     * @hide
+     */
+    public static final int EXPONENT_SIGNIFICAND_MASK = 0x7fff;
+
+    /**
+     * The offset of the exponent from the actual value.
+     *
+     * @hide
+     */
+    public static final int EXPONENT_BIAS             = 15;
+
+    private static final int FP32_SIGN_SHIFT            = 31;
+    private static final int FP32_EXPONENT_SHIFT        = 23;
+    private static final int FP32_SHIFTED_EXPONENT_MASK = 0xff;
+    private static final int FP32_SIGNIFICAND_MASK      = 0x7fffff;
+    private static final int FP32_EXPONENT_BIAS         = 127;
+    private static final int FP32_QNAN_MASK             = 0x400000;
+    private static final int FP32_DENORMAL_MAGIC = 126 << 23;
+    private static final float FP32_DENORMAL_FLOAT = Float.intBitsToFloat(FP32_DENORMAL_MAGIC);
+
+    /** Hidden constructor to prevent instantiation. */
+    private FP16() {}
+
+    /**
+     * <p>Compares the two specified half-precision float values. The following
+     * conditions apply during the comparison:</p>
+     *
+     * <ul>
+     * <li>{@link #NaN} is considered by this method to be equal to itself and greater
+     * than all other half-precision float values (including {@code #POSITIVE_INFINITY})</li>
+     * <li>{@link #POSITIVE_ZERO} is considered by this method to be greater than
+     * {@link #NEGATIVE_ZERO}.</li>
+     * </ul>
+     *
+     * @param x The first half-precision float value to compare.
+     * @param y The second half-precision float value to compare
+     *
+     * @return  The value {@code 0} if {@code x} is numerically equal to {@code y}, a
+     *          value less than {@code 0} if {@code x} is numerically less than {@code y},
+     *          and a value greater than {@code 0} if {@code x} is numerically greater
+     *          than {@code y}
+     *
+     * @hide
+     */
+    public static int compare(short x, short y) {
+        if (less(x, y)) return -1;
+        if (greater(x, y)) return 1;
+
+        // Collapse NaNs, akin to halfToIntBits(), but we want to keep
+        // (signed) short value types to preserve the ordering of -0.0
+        // and +0.0
+        short xBits = isNaN(x) ? NaN : x;
+        short yBits = isNaN(y) ? NaN : y;
+
+        return (xBits == yBits ? 0 : (xBits < yBits ? -1 : 1));
+    }
+
+    /**
+     * Returns the closest integral half-precision float value to the specified
+     * half-precision float value. Special values are handled in the
+     * following ways:
+     * <ul>
+     * <li>If the specified half-precision float is NaN, the result is NaN</li>
+     * <li>If the specified half-precision float is infinity (negative or positive),
+     * the result is infinity (with the same sign)</li>
+     * <li>If the specified half-precision float is zero (negative or positive),
+     * the result is zero (with the same sign)</li>
+     * </ul>
+     *
+     * @param h A half-precision float value
+     * @return The value of the specified half-precision float rounded to the nearest
+     *         half-precision float value
+     *
+     * @hide
+     */
+    public static short rint(short h) {
+        int bits = h & 0xffff;
+        int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+        int result = bits;
+
+        if (abs < 0x3c00) {
+            result &= SIGN_MASK;
+            if (abs > 0x3800){
+                result |= 0x3c00;
+            }
+        } else if (abs < 0x6400) {
+            int exp = 25 - (abs >> 10);
+            int mask = (1 << exp) - 1;
+            result += ((1 << (exp - 1)) - (~(abs >> exp) & 1));
+            result &= ~mask;
+        }
+        if (isNaN((short) result)) {
+            // if result is NaN mask with qNaN
+            // (i.e. mask the most significant mantissa bit with 1)
+            // to comply with hardware implementations (ARM64, Intel, etc).
+            result |= NaN;
+        }
+
+        return (short) result;
+    }
+
+    /**
+     * Returns the smallest half-precision float value toward negative infinity
+     * greater than or equal to the specified half-precision float value.
+     * Special values are handled in the following ways:
+     * <ul>
+     * <li>If the specified half-precision float is NaN, the result is NaN</li>
+     * <li>If the specified half-precision float is infinity (negative or positive),
+     * the result is infinity (with the same sign)</li>
+     * <li>If the specified half-precision float is zero (negative or positive),
+     * the result is zero (with the same sign)</li>
+     * </ul>
+     *
+     * @param h A half-precision float value
+     * @return The smallest half-precision float value toward negative infinity
+     *         greater than or equal to the specified half-precision float value
+     *
+     * @hide
+     */
+    public static short ceil(short h) {
+        int bits = h & 0xffff;
+        int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+        int result = bits;
+
+        if (abs < 0x3c00) {
+            result &= SIGN_MASK;
+            result |= 0x3c00 & -(~(bits >> 15) & (abs != 0 ? 1 : 0));
+        } else if (abs < 0x6400) {
+            abs = 25 - (abs >> 10);
+            int mask = (1 << abs) - 1;
+            result += mask & ((bits >> 15) - 1);
+            result &= ~mask;
+        }
+        if (isNaN((short) result)) {
+            // if result is NaN mask with qNaN
+            // (i.e. mask the most significant mantissa bit with 1)
+            // to comply with hardware implementations (ARM64, Intel, etc).
+            result |= NaN;
+        }
+
+        return (short) result;
+    }
+
+    /**
+     * Returns the largest half-precision float value toward positive infinity
+     * less than or equal to the specified half-precision float value.
+     * Special values are handled in the following ways:
+     * <ul>
+     * <li>If the specified half-precision float is NaN, the result is NaN</li>
+     * <li>If the specified half-precision float is infinity (negative or positive),
+     * the result is infinity (with the same sign)</li>
+     * <li>If the specified half-precision float is zero (negative or positive),
+     * the result is zero (with the same sign)</li>
+     * </ul>
+     *
+     * @param h A half-precision float value
+     * @return The largest half-precision float value toward positive infinity
+     *         less than or equal to the specified half-precision float value
+     *
+     * @hide
+     */
+    public static short floor(short h) {
+        int bits = h & 0xffff;
+        int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+        int result = bits;
+
+        if (abs < 0x3c00) {
+            result &= SIGN_MASK;
+            result |= 0x3c00 & (bits > 0x8000 ? 0xffff : 0x0);
+        } else if (abs < 0x6400) {
+            abs = 25 - (abs >> 10);
+            int mask = (1 << abs) - 1;
+            result += mask & -(bits >> 15);
+            result &= ~mask;
+        }
+        if (isNaN((short) result)) {
+            // if result is NaN mask with qNaN
+            // i.e. (Mask the most significant mantissa bit with 1)
+            result |= NaN;
+        }
+
+        return (short) result;
+    }
+
+    /**
+     * Returns the truncated half-precision float value of the specified
+     * half-precision float value. Special values are handled in the following ways:
+     * <ul>
+     * <li>If the specified half-precision float is NaN, the result is NaN</li>
+     * <li>If the specified half-precision float is infinity (negative or positive),
+     * the result is infinity (with the same sign)</li>
+     * <li>If the specified half-precision float is zero (negative or positive),
+     * the result is zero (with the same sign)</li>
+     * </ul>
+     *
+     * @param h A half-precision float value
+     * @return The truncated half-precision float value of the specified
+     *         half-precision float value
+     *
+     * @hide
+     */
+    public static short trunc(short h) {
+        int bits = h & 0xffff;
+        int abs = bits & EXPONENT_SIGNIFICAND_MASK;
+        int result = bits;
+
+        if (abs < 0x3c00) {
+            result &= SIGN_MASK;
+        } else if (abs < 0x6400) {
+            abs = 25 - (abs >> 10);
+            int mask = (1 << abs) - 1;
+            result &= ~mask;
+        }
+
+        return (short) result;
+    }
+
+    /**
+     * Returns the smaller of two half-precision float values (the value closest
+     * to negative infinity). Special values are handled in the following ways:
+     * <ul>
+     * <li>If either value is NaN, the result is NaN</li>
+     * <li>{@link #NEGATIVE_ZERO} is smaller than {@link #POSITIVE_ZERO}</li>
+     * </ul>
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     * @return The smaller of the two specified half-precision values
+     *
+     * @hide
+     */
+    public static short min(short x, short y) {
+        if (isNaN(x)) return NaN;
+        if (isNaN(y)) return NaN;
+
+        if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+            return (x & SIGN_MASK) != 0 ? x : y;
+        }
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+    }
+
+    /**
+     * Returns the larger of two half-precision float values (the value closest
+     * to positive infinity). Special values are handled in the following ways:
+     * <ul>
+     * <li>If either value is NaN, the result is NaN</li>
+     * <li>{@link #POSITIVE_ZERO} is greater than {@link #NEGATIVE_ZERO}</li>
+     * </ul>
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return The larger of the two specified half-precision values
+     *
+     * @hide
+     */
+    public static short max(short x, short y) {
+        if (isNaN(x)) return NaN;
+        if (isNaN(y)) return NaN;
+
+        if ((x & EXPONENT_SIGNIFICAND_MASK) == 0 && (y & EXPONENT_SIGNIFICAND_MASK) == 0) {
+            return (x & SIGN_MASK) != 0 ? y : x;
+        }
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff) ? x : y;
+    }
+
+    /**
+     * Returns true if the first half-precision float value is less (smaller
+     * toward negative infinity) than the second half-precision float value.
+     * If either of the values is NaN, the result is false.
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return True if x is less than y, false otherwise
+     *
+     * @hide
+     */
+    public static boolean less(short x, short y) {
+        if (isNaN(x)) return false;
+        if (isNaN(y)) return false;
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+    }
+
+    /**
+     * Returns true if the first half-precision float value is less (smaller
+     * toward negative infinity) than or equal to the second half-precision
+     * float value. If either of the values is NaN, the result is false.
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return True if x is less than or equal to y, false otherwise
+     *
+     * @hide
+     */
+    public static boolean lessEquals(short x, short y) {
+        if (isNaN(x)) return false;
+        if (isNaN(y)) return false;
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) <=
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+    }
+
+    /**
+     * Returns true if the first half-precision float value is greater (larger
+     * toward positive infinity) than the second half-precision float value.
+     * If either of the values is NaN, the result is false.
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return True if x is greater than y, false otherwise
+     *
+     * @hide
+     */
+    public static boolean greater(short x, short y) {
+        if (isNaN(x)) return false;
+        if (isNaN(y)) return false;
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+    }
+
+    /**
+     * Returns true if the first half-precision float value is greater (larger
+     * toward positive infinity) than or equal to the second half-precision float
+     * value. If either of the values is NaN, the result is false.
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return True if x is greater than y, false otherwise
+     *
+     * @hide
+     */
+    public static boolean greaterEquals(short x, short y) {
+        if (isNaN(x)) return false;
+        if (isNaN(y)) return false;
+
+        return ((x & SIGN_MASK) != 0 ? 0x8000 - (x & 0xffff) : x & 0xffff) >=
+               ((y & SIGN_MASK) != 0 ? 0x8000 - (y & 0xffff) : y & 0xffff);
+    }
+
+    /**
+     * Returns true if the two half-precision float values are equal.
+     * If either of the values is NaN, the result is false. {@link #POSITIVE_ZERO}
+     * and {@link #NEGATIVE_ZERO} are considered equal.
+     *
+     * @param x The first half-precision value
+     * @param y The second half-precision value
+     *
+     * @return True if x is equal to y, false otherwise
+     *
+     * @hide
+     */
+    public static boolean equals(short x, short y) {
+        if (isNaN(x)) return false;
+        if (isNaN(y)) return false;
+
+        return x == y || ((x | y) & EXPONENT_SIGNIFICAND_MASK) == 0;
+    }
+
+    /**
+     * Returns true if the specified half-precision float value represents
+     * infinity, false otherwise.
+     *
+     * @param h A half-precision float value
+     * @return True if the value is positive infinity or negative infinity,
+     *         false otherwise
+     *
+     * @hide
+     */
+    public static boolean isInfinite(short h) {
+        return (h & EXPONENT_SIGNIFICAND_MASK) == POSITIVE_INFINITY;
+    }
+
+    /**
+     * Returns true if the specified half-precision float value represents
+     * a Not-a-Number, false otherwise.
+     *
+     * @param h A half-precision float value
+     * @return True if the value is a NaN, false otherwise
+     *
+     * @hide
+     */
+    public static boolean isNaN(short h) {
+        return (h & EXPONENT_SIGNIFICAND_MASK) > POSITIVE_INFINITY;
+    }
+
+    /**
+     * Returns true if the specified half-precision float value is normalized
+     * (does not have a subnormal representation). If the specified value is
+     * {@link #POSITIVE_INFINITY}, {@link #NEGATIVE_INFINITY},
+     * {@link #POSITIVE_ZERO}, {@link #NEGATIVE_ZERO}, NaN or any subnormal
+     * number, this method returns false.
+     *
+     * @param h A half-precision float value
+     * @return True if the value is normalized, false otherwise
+     *
+     * @hide
+     */
+    public static boolean isNormalized(short h) {
+        return (h & POSITIVE_INFINITY) != 0 && (h & POSITIVE_INFINITY) != POSITIVE_INFINITY;
+    }
+
+    /**
+     * <p>Converts the specified half-precision float value into a
+     * single-precision float value. The following special cases are handled:</p>
+     * <ul>
+     * <li>If the input is {@link #NaN}, the returned value is {@link Float#NaN}</li>
+     * <li>If the input is {@link #POSITIVE_INFINITY} or
+     * {@link #NEGATIVE_INFINITY}, the returned value is respectively
+     * {@link Float#POSITIVE_INFINITY} or {@link Float#NEGATIVE_INFINITY}</li>
+     * <li>If the input is 0 (positive or negative), the returned value is +/-0.0f</li>
+     * <li>Otherwise, the returned value is a normalized single-precision float value</li>
+     * </ul>
+     *
+     * @param h The half-precision float value to convert to single-precision
+     * @return A normalized single-precision float value
+     *
+     * @hide
+     */
+    public static float toFloat(short h) {
+        int bits = h & 0xffff;
+        int s = bits & SIGN_MASK;
+        int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+        int m = (bits                        ) & SIGNIFICAND_MASK;
+
+        int outE = 0;
+        int outM = 0;
+
+        if (e == 0) { // Denormal or 0
+            if (m != 0) {
+                // Convert denorm fp16 into normalized fp32
+                float o = Float.intBitsToFloat(FP32_DENORMAL_MAGIC + m);
+                o -= FP32_DENORMAL_FLOAT;
+                return s == 0 ? o : -o;
+            }
+        } else {
+            outM = m << 13;
+            if (e == 0x1f) { // Infinite or NaN
+                outE = 0xff;
+                if (outM != 0) { // SNaNs are quieted
+                    outM |= FP32_QNAN_MASK;
+                }
+            } else {
+                outE = e - EXPONENT_BIAS + FP32_EXPONENT_BIAS;
+            }
+        }
+
+        int out = (s << 16) | (outE << FP32_EXPONENT_SHIFT) | outM;
+        return Float.intBitsToFloat(out);
+    }
+
+    /**
+     * <p>Converts the specified single-precision float value into a
+     * half-precision float value. The following special cases are handled:</p>
+     * <ul>
+     * <li>If the input is NaN (see {@link Float#isNaN(float)}), the returned
+     * value is {@link #NaN}</li>
+     * <li>If the input is {@link Float#POSITIVE_INFINITY} or
+     * {@link Float#NEGATIVE_INFINITY}, the returned value is respectively
+     * {@link #POSITIVE_INFINITY} or {@link #NEGATIVE_INFINITY}</li>
+     * <li>If the input is 0 (positive or negative), the returned value is
+     * {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+     * <li>If the input is a less than {@link #MIN_VALUE}, the returned value
+     * is flushed to {@link #POSITIVE_ZERO} or {@link #NEGATIVE_ZERO}</li>
+     * <li>If the input is a less than {@link #MIN_NORMAL}, the returned value
+     * is a denorm half-precision float</li>
+     * <li>Otherwise, the returned value is rounded to the nearest
+     * representable half-precision float value</li>
+     * </ul>
+     *
+     * @param f The single-precision float value to convert to half-precision
+     * @return A half-precision float value
+     *
+     * @hide
+     */
+    public static short toHalf(float f) {
+        int bits = Float.floatToRawIntBits(f);
+        int s = (bits >>> FP32_SIGN_SHIFT    );
+        int e = (bits >>> FP32_EXPONENT_SHIFT) & FP32_SHIFTED_EXPONENT_MASK;
+        int m = (bits                        ) & FP32_SIGNIFICAND_MASK;
+
+        int outE = 0;
+        int outM = 0;
+
+        if (e == 0xff) { // Infinite or NaN
+            outE = 0x1f;
+            outM = m != 0 ? 0x200 : 0;
+        } else {
+            e = e - FP32_EXPONENT_BIAS + EXPONENT_BIAS;
+            if (e >= 0x1f) { // Overflow
+                outE = 0x1f;
+            } else if (e <= 0) { // Underflow
+                if (e < -10) {
+                    // The absolute fp32 value is less than MIN_VALUE, flush to +/-0
+                } else {
+                    // The fp32 value is a normalized float less than MIN_NORMAL,
+                    // we convert to a denorm fp16
+                    m = m | 0x800000;
+                    int shift = 14 - e;
+                    outM = m >> shift;
+
+                    int lowm = m & ((1 << shift) - 1);
+                    int hway = 1 << (shift - 1);
+                    // if above halfway or exactly halfway and outM is odd
+                    if (lowm + (outM & 1) > hway){
+                        // Round to nearest even
+                        // Can overflow into exponent bit, which surprisingly is OK.
+                        // This increment relies on the +outM in the return statement below
+                        outM++;
+                    }
+                }
+            } else {
+                outE = e;
+                outM = m >> 13;
+                // if above halfway or exactly halfway and outM is odd
+                if ((m & 0x1fff) + (outM & 0x1) > 0x1000) {
+                    // Round to nearest even
+                    // Can overflow into exponent bit, which surprisingly is OK.
+                    // This increment relies on the +outM in the return statement below
+                    outM++;
+                }
+            }
+        }
+        // The outM is added here as the +1 increments for outM above can
+        // cause an overflow in the exponent bit which is OK.
+        return (short) ((s << SIGN_SHIFT) | (outE << EXPONENT_SHIFT) + outM);
+    }
+
+    /**
+     * <p>Returns a hexadecimal string representation of the specified half-precision
+     * float value. If the value is a NaN, the result is <code>"NaN"</code>,
+     * otherwise the result follows this format:</p>
+     * <ul>
+     * <li>If the sign is positive, no sign character appears in the result</li>
+     * <li>If the sign is negative, the first character is <code>'-'</code></li>
+     * <li>If the value is inifinity, the string is <code>"Infinity"</code></li>
+     * <li>If the value is 0, the string is <code>"0x0.0p0"</code></li>
+     * <li>If the value has a normalized representation, the exponent and
+     * significand are represented in the string in two fields. The significand
+     * starts with <code>"0x1."</code> followed by its lowercase hexadecimal
+     * representation. Trailing zeroes are removed unless all digits are 0, then
+     * a single zero is used. The significand representation is followed by the
+     * exponent, represented by <code>"p"</code>, itself followed by a decimal
+     * string of the unbiased exponent</li>
+     * <li>If the value has a subnormal representation, the significand starts
+     * with <code>"0x0."</code> followed by its lowercase hexadecimal
+     * representation. Trailing zeroes are removed unless all digits are 0, then
+     * a single zero is used. The significand representation is followed by the
+     * exponent, represented by <code>"p-14"</code></li>
+     * </ul>
+     *
+     * @param h A half-precision float value
+     * @return A hexadecimal string representation of the specified value
+     *
+     * @hide
+     */
+    public static String toHexString(short h) {
+        StringBuilder o = new StringBuilder();
+
+        int bits = h & 0xffff;
+        int s = (bits >>> SIGN_SHIFT    );
+        int e = (bits >>> EXPONENT_SHIFT) & SHIFTED_EXPONENT_MASK;
+        int m = (bits                   ) & SIGNIFICAND_MASK;
+
+        if (e == 0x1f) { // Infinite or NaN
+            if (m == 0) {
+                if (s != 0) o.append('-');
+                o.append("Infinity");
+            } else {
+                o.append("NaN");
+            }
+        } else {
+            if (s == 1) o.append('-');
+            if (e == 0) {
+                if (m == 0) {
+                    o.append("0x0.0p0");
+                } else {
+                    o.append("0x0.");
+                    String significand = Integer.toHexString(m);
+                    o.append(significand.replaceFirst("0{2,}$", ""));
+                    o.append("p-14");
+                }
+            } else {
+                o.append("0x1.");
+                String significand = Integer.toHexString(m);
+                o.append(significand.replaceFirst("0{2,}$", ""));
+                o.append('p');
+                o.append(Integer.toString(e - EXPONENT_BIAS));
+            }
+        }
+
+        return o.toString();
+    }
+}
diff --git a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
index 14b5a4f..4e7dc5d 100644
--- a/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
+++ b/ravenwood/runtime-helper-src/libcore-fake/libcore/util/NativeAllocationRegistry.java
@@ -15,7 +15,7 @@
  */
 package libcore.util;
 
-import com.android.ravenwood.common.RavenwoodRuntimeNative;
+import com.android.ravenwood.RavenwoodRuntimeNative;
 
 import java.lang.ref.Cleaner;
 import java.lang.ref.Reference;
diff --git a/ravenwood/runtime-jni/ravenwood_runtime.cpp b/ravenwood/runtime-jni/ravenwood_runtime.cpp
index c804928..c255be5 100644
--- a/ravenwood/runtime-jni/ravenwood_runtime.cpp
+++ b/ravenwood/runtime-jni/ravenwood_runtime.cpp
@@ -214,6 +214,19 @@
     return throwIfMinusOne(env, "open", TEMP_FAILURE_RETRY(open(path.c_str(), flags, mode)));
 }
 
+static void Linux_setenv(JNIEnv* env, jobject, jstring javaName, jstring javaValue,
+        jboolean overwrite) {
+    ScopedRealUtf8Chars name(env, javaName);
+    if (name.c_str() == NULL) {
+        jniThrowNullPointerException(env);
+    }
+    ScopedRealUtf8Chars value(env, javaValue);
+    if (value.c_str() == NULL) {
+        jniThrowNullPointerException(env);
+    }
+    throwIfMinusOne(env, "setenv", setenv(name.c_str(), value.c_str(), overwrite ? 1 : 0));
+}
+
 // ---- Registration ----
 
 static const JNINativeMethod sMethods[] =
@@ -227,6 +240,7 @@
     { "lstat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_lstat },
     { "stat", "(Ljava/lang/String;)Landroid/system/StructStat;", (void*)Linux_stat },
     { "nOpen", "(Ljava/lang/String;II)I", (void*)Linux_open },
+    { "setenv", "(Ljava/lang/String;Ljava/lang/String;Z)V", (void*)Linux_setenv },
 };
 
 extern "C" jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
@@ -245,7 +259,7 @@
     g_StructStat = findClass(env, "android/system/StructStat");
     g_StructTimespecClass = findClass(env, "android/system/StructTimespec");
 
-    jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/common/RavenwoodRuntimeNative",
+    jint res = jniRegisterNativeMethods(env, "com/android/ravenwood/RavenwoodRuntimeNative",
             sMethods, NELEM(sMethods));
     if (res < 0) {
         return res;
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
index 3332e24..633ed4e 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsConstantsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsConstantsTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
 
 // Copied from libcore/luni/src/test/java/libcore/android/system/OsConstantsTest.java
 
diff --git a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
similarity index 99%
rename from ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
rename to ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
index 05275b2..c2230c7 100644
--- a/ravenwood/runtime-test/test/com/android/ravenwood/runtimetest/OsTest.java
+++ b/ravenwood/runtime-test/test/com/android/ravenwoodtest/runtimetest/OsTest.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package com.android.ravenwood.runtimetest;
+package com.android.ravenwoodtest.runtimetest;
 
 import static android.system.OsConstants.S_ISBLK;
 import static android.system.OsConstants.S_ISCHR;
diff --git a/ravenwood/scripts/list-ravenwood-tests.sh b/ravenwood/scripts/list-ravenwood-tests.sh
index fb9b823..05f3fdf 100755
--- a/ravenwood/scripts/list-ravenwood-tests.sh
+++ b/ravenwood/scripts/list-ravenwood-tests.sh
@@ -15,4 +15,4 @@
 
 # List all the ravenwood test modules.
 
-jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json"
+jq -r 'to_entries[] | select( .value.compatibility_suites | index("ravenwood-tests") ) | .key' "$OUT/module-info.json" | sort
diff --git a/ravenwood/tools/ravenizer-fake/ravenizer b/ravenwood/scripts/remove-ravenizer-output.sh
similarity index 60%
rename from ravenwood/tools/ravenizer-fake/ravenizer
rename to ravenwood/scripts/remove-ravenizer-output.sh
index 84b3c8e..be15b71 100755
--- a/ravenwood/tools/ravenizer-fake/ravenizer
+++ b/ravenwood/scripts/remove-ravenizer-output.sh
@@ -13,19 +13,13 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-# "Fake" ravenizer, which just copies the file.
-# We need it to add ravenizer support to Soong on AOSP,
-# when the actual ravenizer is not in AOSP yet.
+# Delete all the ravenizer output jar files from Soong's intermediate directory.
 
-invalid_arg() {
-    echo "Ravenizer(fake): invalid args" 1>&2
-    exit 1
-}
+# `-a -prune` is needed because otherwise find would be confused if the directory disappears.
 
-(( $# >= 4 )) || invalid_arg
-[[ "$1" == "--in-jar" ]] || invalid_arg
-[[ "$3" == "--out-jar" ]] || invalid_arg
-
-echo "Ravenizer(fake): copiyng $2 to $4"
-
-cp "$2" "$4"
+find "${ANDROID_BUILD_TOP:?}/out/soong/.intermediates/" \
+    -type d \
+    -name 'ravenizer' \
+    -print \
+    -exec rm -fr \{\} \; \
+    -a -prune
diff --git a/ravenwood/scripts/shrink-systemui-test b/ravenwood/scripts/shrink-systemui-test
new file mode 100755
index 0000000..8589c1d
--- /dev/null
+++ b/ravenwood/scripts/shrink-systemui-test
@@ -0,0 +1,131 @@
+#!/bin/bash
+# Copyright (C) 2024 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.
+
+set -e
+
+SCRIPT_NAME="${0##*/}"
+
+usage() {
+    cat <<"EOF"
+
+$SCRIPT_NAME: Shrink / unshrink SystemUiRavenTests.
+
+    SystemUiRavenTests has a lot of kotlin source files, so it's slow to build,
+    which is painful when you want to run it after updating ravenwood code
+    that SystemUiRavenTests depends on. (example: junit-src/)
+
+    This script basically removes the test files in SystemUI/multivalentTests
+    that don't have @EnabledOnRavenwood. But if we actaully remove them,
+    soong would re-generate the ninja file, which will take a long time,
+    so instead it'll truncate them.
+
+    This script will also tell git to ignore these files, so they won't shw up
+    in `git status`.
+    (Use `git ls-files -v | sed -ne "s/^[a-zS] //p"` to show ignored filse.)
+
+Usage:
+    $SCRIPT_NAME -s # Shrink the test files.
+
+    $SCRIPT_NAME -u # Undo it.
+
+EOF
+}
+
+TEST_PATH=${ANDROID_BUILD_TOP}/frameworks/base/packages/SystemUI/multivalentTests
+cd "$TEST_PATH"
+
+command=""
+case "$1" in
+    "-s") command=shrink ;;
+    "-u") command=unshrink ;;
+    *) usage ; exit 1 ;;
+esac
+
+
+echo "Listing test files...."
+files=( $(find . -name '*Test.kt' -o -name '*Test.java') )
+
+exemption='(BaseHeadsUpManagerTest)'
+
+shrink() {
+    local target=()
+    for file in ${files[@]}; do
+        # Check for exemption
+        if echo $file | egrep -q "$exemption"; then
+            echo "  Skip exempted file"
+            continue
+        fi
+
+        echo "Checking $file"
+        if ! [[ -f $file ]] ; then
+            echo "  Skip non regular file"
+            continue
+        fi
+
+        if ! [[ -s $file ]] ; then
+            echo "  Skip empty file"
+            continue
+        fi
+
+        if grep -q '@EnabledOnRavenwood' $file ; then
+            echo "  Skip ravenwood test file".
+            continue
+        fi
+
+        # It's a non ravenwood test file. Empty it.
+        : > $file
+
+        # Tell git to ignore the file
+
+        target+=($file)
+
+        echo "  Emptied"
+
+    done
+    if (( ${#target[@]} == 0 )) ; then
+        echo "No files emptied."
+        return 0
+    fi
+
+    git update-index --skip-worktree ${target[@]}
+
+    echo "Emptied ${#target[@]} files"
+    return 0
+}
+
+unshrink() {
+    local target=()
+
+    # Collect empty files
+    for file in ${files[@]}; do
+        if [[ -s $file ]] ; then
+            continue
+        fi
+
+        target+=($file)
+        : > $file
+    done
+    if (( ${#target[@]} == 0 )) ; then
+        echo "No files to restore."
+        return 0
+    fi
+    # Un-ignore the files, and check out the original files
+    echo "Restoring ${#target[@]} files..."
+    git update-index --no-skip-worktree ${target[@]}
+    git checkout goog/main ${target[@]}
+    return 0
+}
+
+$command
diff --git a/ravenwood/scripts/update-test-mapping.sh b/ravenwood/scripts/update-test-mapping.sh
new file mode 100755
index 0000000..e478b50
--- /dev/null
+++ b/ravenwood/scripts/update-test-mapping.sh
@@ -0,0 +1,85 @@
+#!/bin/bash
+# Copyright (C) 2024 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.
+
+# Update f/b/r/TEST_MAPPING with all the ravenwood tests as presubmit.
+#
+# Note, before running it, make sure module-info.json is up-to-date by running
+# (any) build.
+
+set -e
+
+# Tests that shouldn't be in presubmit.
+EXEMPT='^(SystemUiRavenTests)$'
+
+main() {
+    local script_name="${0##*/}"
+    local script_dir="${0%/*}"
+    local test_mapping="$script_dir/../TEST_MAPPING"
+    local test_mapping_bak="$script_dir/../TEST_MAPPING.bak"
+
+    local header="$(sed -ne '1,/AUTO-GENERATED-START/p' "$test_mapping")"
+    local footer="$(sed -ne '/AUTO-GENERATED-END/,$p' "$test_mapping")"
+
+    echo "Getting all tests"
+    local tests=( $("$script_dir/list-ravenwood-tests.sh" | grep -vP "$EXEMPT") )
+
+    local num_tests="${#tests[@]}"
+
+    if (( $num_tests == 0 )) ; then
+        echo "Something went wrong. No ravenwood tests detected." 1>&2
+        return 1
+    fi
+
+    echo "Tests: ${tests[@]}"
+
+    echo "Creating backup at $test_mapping_bak"
+    cp "$test_mapping" "$test_mapping_bak"
+
+    echo "Updating $test_mapping"
+    {
+        echo "$header"
+
+        echo "    // DO NOT MODIFY MANUALLY"
+        echo "    // Use scripts/$script_name to update it."
+
+        local i=0
+        while (( $i < $num_tests )) ; do
+            local comma=","
+            if (( $i == ($num_tests - 1) )); then
+                comma=""
+            fi
+            echo "    {"
+            echo "      \"name\": \"${tests[$i]}\","
+            echo "      \"host\": true"
+            echo "    }$comma"
+
+            i=$(( $i + 1 ))
+        done
+
+        echo "$footer"
+    } >"$test_mapping"
+
+    if cmp "$test_mapping_bak" "$test_mapping" ; then
+        echo "No change detecetd."
+        return 0
+    fi
+    echo "Updated $test_mapping"
+
+    # `|| true` is needed because of `set -e`.
+    diff -u "$test_mapping_bak" "$test_mapping" || true
+    return 0
+}
+
+main
diff --git a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
index 044239f..8ce15f0 100644
--- a/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
+++ b/ravenwood/services-test/test/com/android/ravenwoodtest/servicestest/RavenwoodServicesTest.java
@@ -16,12 +16,14 @@
 
 package com.android.ravenwoodtest.servicestest;
 
+import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertNotNull;
 
 import android.content.Context;
 import android.hardware.SerialManager;
 import android.hardware.SerialManagerInternal;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -56,13 +58,16 @@
     }
 
     @Test
+    @DisabledOnRavenwood(reason="AOSP is missing resources support")
     public void testSimple() {
         // Verify that we can obtain a manager, and talk to the backend service, and that no
         // serial ports are configured by default
         final SerialManager service = (SerialManager)
                 mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
         final String[] ports = service.getSerialPorts();
-        assertEquals(0, ports.length);
+        final String[] refPorts = mRavenwood.getContext().getResources().getStringArray(
+                com.android.internal.R.array.config_serialPorts);
+        assertArrayEquals(refPorts, ports);
     }
 
     @Test
diff --git a/ravenwood/test-authors.md b/ravenwood/test-authors.md
index 0a0b200..c29fb7f 100644
--- a/ravenwood/test-authors.md
+++ b/ravenwood/test-authors.md
@@ -46,7 +46,7 @@
 * Write your unit test just like you would for an Android device:
 
 ```
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.ext.junit.runners.AndroidJUnit4;
@@ -66,7 +66,7 @@
 * APIs available under Ravenwood are stateless by default.  If your test requires explicit states (such as defining the UID you’re running under, or requiring a main `Looper` thread), add a `RavenwoodRule` to declare that:
 
 ```
-import android.platform.test.annotations.IgnoreUnderRavenwood;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.ravenwood.RavenwoodRule;
 
 import androidx.test.runner.AndroidJUnit4;
@@ -165,7 +165,7 @@
     }
 
     @Test
-    @IgnoreUnderRavenwood(blockedBy = PackageManager.class)
+    @DisabledOnRavenwood(blockedBy = PackageManager.class)
     public void testComplex() {
         // Complex test that runs on devices, but is ignored under Ravenwood
     }
diff --git a/ravenwood/tests/bivalentinst/Android.bp b/ravenwood/tests/bivalentinst/Android.bp
new file mode 100644
index 0000000..41e45e5
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/Android.bp
@@ -0,0 +1,148 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_ravenwood_test {
+    name: "RavenwoodBivalentInstTest_self_inst",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    exclude_srcs: [
+        "test/**/*_nonself.java",
+    ],
+
+    static_libs: [
+        "RavenwoodBivalentInstTest_self_inst_device_R",
+
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+
+        "junit",
+        "truth",
+    ],
+    resource_apk: "RavenwoodBivalentInstTest_self_inst_device",
+    auto_gen_config: true,
+}
+
+android_ravenwood_test {
+    name: "RavenwoodBivalentInstTest_nonself_inst",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    exclude_srcs: [
+        "test/**/*_self.java",
+    ],
+
+    static_libs: [
+        "RavenwoodBivalentInstTestTarget_R",
+        "RavenwoodBivalentInstTest_nonself_inst_device_R",
+
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+
+        "junit",
+        "truth",
+    ],
+    resource_apk: "RavenwoodBivalentInstTestTarget",
+    inst_resource_apk: "RavenwoodBivalentInstTest_nonself_inst_device",
+    auto_gen_config: true,
+}
+
+// We have 3 R.javas from the 3 packages (2 test apks below, and 1 target APK)
+// RavenwoodBivalentInstTest needs to use all of them, but we can't add all the
+// {.aapt.srcjar}'s together because that'd cause
+// "duplicate declaration of androidx.test.core.R$string."
+// So we build them as separate libraries, and include them as static_libs.
+java_library {
+    name: "RavenwoodBivalentInstTestTarget_R",
+    srcs: [
+        ":RavenwoodBivalentInstTestTarget{.aapt.srcjar}",
+    ],
+}
+
+java_library {
+    name: "RavenwoodBivalentInstTest_self_inst_device_R",
+    srcs: [
+        ":RavenwoodBivalentInstTest_self_inst_device{.aapt.srcjar}",
+    ],
+}
+
+java_library {
+    name: "RavenwoodBivalentInstTest_nonself_inst_device_R",
+    srcs: [
+        ":RavenwoodBivalentInstTest_nonself_inst_device{.aapt.srcjar}",
+    ],
+}
+
+android_test {
+    name: "RavenwoodBivalentInstTest_self_inst_device",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    exclude_srcs: [
+        "test/**/*_nonself.java",
+    ],
+    static_libs: [
+        "junit",
+        "truth",
+
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+
+        "ravenwood-junit",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    use_resource_processor: false,
+    manifest: "AndroidManifest-self-inst.xml",
+    test_config: "AndroidTest-self-inst.xml",
+    optimize: {
+        enabled: false,
+    },
+}
+
+android_test {
+    name: "RavenwoodBivalentInstTest_nonself_inst_device",
+
+    srcs: [
+        "test/**/*.java",
+    ],
+    exclude_srcs: [
+        "test/**/*_self.java",
+    ],
+    static_libs: [
+        "junit",
+        "truth",
+
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+
+        "ravenwood-junit",
+    ],
+    data: [
+        ":RavenwoodBivalentInstTestTarget",
+    ],
+    test_suites: [
+        "device-tests",
+    ],
+    use_resource_processor: false,
+    manifest: "AndroidManifest-nonself-inst.xml",
+    test_config: "AndroidTest-nonself-inst.xml",
+    instrumentation_for: "RavenwoodBivalentInstTestTarget",
+    optimize: {
+        enabled: false,
+    },
+}
diff --git a/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml b/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml
new file mode 100644
index 0000000..a5a1f17
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidManifest-nonself-inst.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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 xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.bivalentinsttest_nonself_inst">
+
+    <application android:debuggable="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.ravenwood.bivalentinst_target_app"
+        />
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml b/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml
new file mode 100644
index 0000000..3dc4c56
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidManifest-self-inst.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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 xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.bivalentinsttest_self_inst">
+
+    <application android:debuggable="true" >
+        <uses-library android:name="android.test.runner" />
+    </application>
+
+    <instrumentation
+        android:name="androidx.test.runner.AndroidJUnitRunner"
+        android:targetPackage="com.android.ravenwood.bivalentinsttest_self_inst"
+        />
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml b/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml
new file mode 100644
index 0000000..9491c53
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidTest-nonself-inst.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<configuration>
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="RavenwoodBivalentInstTestTarget.apk" />
+        <option name="test-file-name" value="RavenwoodBivalentInstTest_nonself_inst_device.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.ravenwood.bivalentinsttest_nonself_inst" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml b/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml
new file mode 100644
index 0000000..3079c06
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/AndroidTest-self-inst.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+<configuration>
+    <option name="test-suite-tag" value="apct" />
+    <option name="test-suite-tag" value="apct-instrumentation" />
+
+    <target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
+        <option name="cleanup-apks" value="true" />
+        <option name="test-file-name" value="RavenwoodBivalentInstTest_self_inst_device.apk" />
+    </target_preparer>
+
+    <test class="com.android.tradefed.testtype.AndroidJUnitTest" >
+        <option name="package" value="com.android.ravenwood.bivalentinsttest_self_inst" />
+        <option name="runner" value="androidx.test.runner.AndroidJUnitRunner" />
+    </test>
+</configuration>
diff --git a/ravenwood/tests/bivalentinst/res/values/strings.xml b/ravenwood/tests/bivalentinst/res/values/strings.xml
new file mode 100644
index 0000000..73ef650
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string translatable="false" name="test_string_in_test">String in test APK</string>
+</resources>
diff --git a/ravenwood/tools/ravenizer-fake/Android.bp b/ravenwood/tests/bivalentinst/targetapp/Android.bp
similarity index 61%
rename from ravenwood/tools/ravenizer-fake/Android.bp
rename to ravenwood/tests/bivalentinst/targetapp/Android.bp
index 7e2c407..7528a62 100644
--- a/ravenwood/tools/ravenizer-fake/Android.bp
+++ b/ravenwood/tests/bivalentinst/targetapp/Android.bp
@@ -7,8 +7,14 @@
     default_applicable_licenses: ["frameworks_base_license"],
 }
 
-sh_binary_host {
-    name: "ravenizer",
-    src: "ravenizer",
-    visibility: ["//visibility:public"],
+android_app {
+    name: "RavenwoodBivalentInstTestTarget",
+    srcs: [
+        "src/**/*.java",
+    ],
+    sdk_version: "current",
+    optimize: {
+        enabled: false,
+    },
+    use_resource_processor: false,
 }
diff --git a/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml b/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml
new file mode 100644
index 0000000..0715f5d
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/AndroidManifest.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2024 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 xmlns:android="http://schemas.android.com/apk/res/android"
+        package="com.android.ravenwood.bivalentinst_target_app">
+    <application>
+    </application>
+</manifest>
diff --git a/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml b/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml
new file mode 100644
index 0000000..395bc2a
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/res/values/strings.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright (C) 2024 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.
+-->
+
+<resources xmlns:android="http://schemas.android.com/apk/res/android"
+           xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string translatable="false" name="test_string_in_target">Test string in target APK</string>
+</resources>
diff --git a/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java b/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java
new file mode 100644
index 0000000..15e50ec
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/targetapp/src/com/android/ravenwoodtest/bivalentinst/Empty.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalentinst;
+
+/**
+ * Empty class. We need it because an instrumentation target APK must have code.
+ */
+public class Empty {
+}
diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java
new file mode 100644
index 0000000..ed1992c
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_nonself.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalentinst;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodConfig.Config;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the case where the instrumentation target is _not_ the test APK itself.
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(reason="AOSP is missing resources support")
+public class RavenwoodInstrumentationTest_nonself {
+    private static final String TARGET_PACKAGE_NAME =
+            "com.android.ravenwood.bivalentinst_target_app";
+    private static final String TEST_PACKAGE_NAME =
+            "com.android.ravenwood.bivalentinsttest_nonself_inst";
+
+    @Config
+    public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+            .setPackageName(TEST_PACKAGE_NAME)
+            .setTargetPackageName(TARGET_PACKAGE_NAME)
+            .build();
+
+    private static Instrumentation sInstrumentation;
+    private static Context sTestContext;
+    private static Context sTargetContext;
+
+    @BeforeClass
+    public static void beforeClass() {
+        sInstrumentation = InstrumentationRegistry.getInstrumentation();
+        sTestContext = sInstrumentation.getContext();
+        sTargetContext = sInstrumentation.getTargetContext();
+    }
+
+    @Test
+    public void testTestContextPackageName() {
+        assertThat(sTestContext.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTargetContextPackageName() {
+        assertThat(sTargetContext.getPackageName()).isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTestAppContext() {
+        // Test context doesn't have an app context.
+        assertThat(sTestContext.getApplicationContext()).isNull();
+    }
+
+    @Test
+    public void testTargetAppContextPackageName() {
+        assertThat(sTargetContext.getApplicationContext().getPackageName())
+                .isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTargetAppAppContextPackageName() {
+        assertThat(sTargetContext.getApplicationContext()
+                .getApplicationContext().getPackageName())
+                .isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testContextSameness() {
+        assertThat(sTargetContext).isNotSameInstanceAs(sTestContext);
+
+        assertThat(sTargetContext).isNotSameInstanceAs(sTargetContext.getApplicationContext());
+
+        assertThat(sTargetContext.getApplicationContext()).isSameInstanceAs(
+                sTargetContext.getApplicationContext().getApplicationContext());
+    }
+
+    @Test
+    public void testTargetAppResource() {
+        assertThat(sTargetContext.getString(
+                com.android.ravenwood.bivalentinst_target_app.R.string.test_string_in_target))
+                .isEqualTo("Test string in target APK");
+    }
+
+    @Test
+    public void testTestAppResource() {
+        assertThat(sTestContext.getString(
+                com.android.ravenwood.bivalentinsttest_nonself_inst.R.string.test_string_in_test))
+                .isEqualTo("String in test APK");
+    }
+}
diff --git a/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java
new file mode 100644
index 0000000..b5bafc4
--- /dev/null
+++ b/ravenwood/tests/bivalentinst/test/com/android/ravenwoodtest/bivalentinst/RavenwoodInstrumentationTest_self.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.bivalentinst;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.app.Instrumentation;
+import android.content.Context;
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodConfig.Config;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * Tests for the case where the instrumentation target is the test APK itself.
+ */
+@RunWith(AndroidJUnit4.class)
+@DisabledOnRavenwood(reason="AOSP is missing resources support")
+public class RavenwoodInstrumentationTest_self {
+
+    private static final String TARGET_PACKAGE_NAME =
+            "com.android.ravenwood.bivalentinsttest_self_inst";
+    private static final String TEST_PACKAGE_NAME =
+            "com.android.ravenwood.bivalentinsttest_self_inst";
+
+    @Config
+    public static final RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+            .setPackageName(TEST_PACKAGE_NAME)
+            .setTargetPackageName(TARGET_PACKAGE_NAME)
+            .build();
+
+
+    private static Instrumentation sInstrumentation;
+    private static Context sTestContext;
+    private static Context sTargetContext;
+
+    @BeforeClass
+    public static void beforeClass() {
+        sInstrumentation = InstrumentationRegistry.getInstrumentation();
+        sTestContext = sInstrumentation.getContext();
+        sTargetContext = sInstrumentation.getTargetContext();
+    }
+
+    @Test
+    public void testTestContextPackageName() {
+        assertThat(sTestContext.getPackageName()).isEqualTo(TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTargetContextPackageName() {
+        assertThat(sTargetContext.getPackageName()).isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTestAppContextPackageName() {
+        assertThat(sTestContext.getApplicationContext().getPackageName())
+                .isEqualTo(TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTestAppAppContextPackageName() {
+        assertThat(sTestContext.getApplicationContext().getPackageName())
+                .isEqualTo(TEST_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTargetAppContextPackageName() {
+        assertThat(sTargetContext.getApplicationContext()
+                .getApplicationContext().getPackageName())
+                .isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testTargetAppAppContextPackageName() {
+        assertThat(sTargetContext.getApplicationContext()
+                .getApplicationContext().getPackageName())
+                .isEqualTo(TARGET_PACKAGE_NAME);
+    }
+
+    @Test
+    public void testContextSameness() {
+        assertThat(sTargetContext).isNotSameInstanceAs(sTestContext);
+
+        assertThat(sTestContext).isNotSameInstanceAs(sTestContext.getApplicationContext());
+        assertThat(sTargetContext).isNotSameInstanceAs(sTargetContext.getApplicationContext());
+
+        assertThat(sTestContext.getApplicationContext()).isSameInstanceAs(
+                sTestContext.getApplicationContext().getApplicationContext());
+        assertThat(sTargetContext.getApplicationContext()).isSameInstanceAs(
+                sTargetContext.getApplicationContext().getApplicationContext());
+    }
+
+    @Test
+    public void testTargetAppResource() {
+        assertThat(sTargetContext.getString(
+                com.android.ravenwood.bivalentinsttest_self_inst.R.string.test_string_in_test))
+                .isEqualTo("String in test APK");
+    }
+
+    @Test
+    public void testTestAppResource() {
+        assertThat(sTestContext.getString(
+                com.android.ravenwood.bivalentinsttest_self_inst.R.string.test_string_in_test))
+                .isEqualTo("String in test APK");
+    }
+}
diff --git a/ravenwood/coretest/Android.bp b/ravenwood/tests/coretest/Android.bp
similarity index 87%
rename from ravenwood/coretest/Android.bp
rename to ravenwood/tests/coretest/Android.bp
index a78c5c1..d94475c 100644
--- a/ravenwood/coretest/Android.bp
+++ b/ravenwood/tests/coretest/Android.bp
@@ -14,10 +14,12 @@
         "androidx.annotation_annotation",
         "androidx.test.ext.junit",
         "androidx.test.rules",
+        "junit-params",
+        "platform-parametric-runner-lib",
+        "truth",
     ],
     srcs: [
         "test/**/*.java",
     ],
-    sdk_version: "test_current",
     auto_gen_config: true,
 }
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
new file mode 100644
index 0000000..bd01313
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerCallbackTest.java
@@ -0,0 +1,458 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.runnercallbacktests;
+
+import static org.junit.Assume.assumeTrue;
+
+import android.platform.test.annotations.DisabledOnRavenwood;
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.Assume;
+import org.junit.BeforeClass;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.junit.runners.BlockJUnit4ClassRunner;
+import org.junit.runners.model.InitializationError;
+import org.junit.runners.model.Statement;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
+
+/**
+ * Tests to make sure {@link RavenwoodAwareTestRunner} produces expected callbacks in various
+ * error situations in places such as @BeforeClass / @AfterClass / Constructors, which are
+ * out of test method bodies.
+ */
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public class RavenwoodRunnerCallbackTest extends RavenwoodRunnerTestBase {
+
+    /**
+     * Throws an exception in @AfterClass. This should produce a critical error.
+     */
+    @RunWith(BlockJUnit4ClassRunner.class)
+    // CHECKSTYLE:OFF Generated code
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest
+    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+    testStarted: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+    testFinished: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$AfterClassFailureTest
+    criticalError: Failures detected in @AfterClass, which would be swallowed by tradefed: FAILURE
+    testSuiteFinished: classes
+    testRunFinished: 2,0,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class AfterClassFailureTest {
+        public AfterClassFailureTest() {
+        }
+
+        @AfterClass
+        public static void afterClass() {
+            throw new RuntimeException("FAILURE");
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+
+    }
+
+    /**
+     * Assumption failure in @BeforeClass.
+     */
+    @RunWith(ParameterizedAndroidJunit4.class)
+    // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+    // the whole class is executed twice.
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest
+    testSuiteStarted: [0]
+    testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testSuiteFinished: [0]
+    testSuiteStarted: [1]
+    testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassAssumptionFailureTest)
+    testSuiteFinished: [1]
+    testSuiteFinished: classes
+    testRunFinished: 4,0,4,0
+    """)
+    // CHECKSTYLE:ON
+    public static class BeforeClassAssumptionFailureTest {
+        public BeforeClassAssumptionFailureTest(String param) {
+        }
+
+        @BeforeClass
+        public static void beforeClass() {
+            Assume.assumeTrue(false);
+        }
+
+        @Parameters
+        public static List<String> getParams() {
+            var params =  new ArrayList<String>();
+            params.add("foo");
+            params.add("bar");
+            return params;
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * General exception in @BeforeClass.
+     */
+    @RunWith(ParameterizedAndroidJunit4.class)
+    // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+    // the whole class is executed twice.
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest
+    testSuiteStarted: [0]
+    testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testFailure: FAILURE
+    testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testFailure: FAILURE
+    testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testSuiteFinished: [0]
+    testSuiteStarted: [1]
+    testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testFailure: FAILURE
+    testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testFailure: FAILURE
+    testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BeforeClassExceptionTest)
+    testSuiteFinished: [1]
+    testSuiteFinished: classes
+    testRunFinished: 4,4,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class BeforeClassExceptionTest {
+        public BeforeClassExceptionTest(String param) {
+        }
+
+        @BeforeClass
+        public static void beforeClass() {
+            throw new RuntimeException("FAILURE");
+        }
+
+        @Parameters
+        public static List<String> getParams() {
+            var params =  new ArrayList<String>();
+            params.add("foo");
+            params.add("bar");
+            return params;
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * Assumption failure from a @ClassRule.
+     */
+    @RunWith(ParameterizedAndroidJunit4.class)
+    // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+    // the whole class is executed twice.
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest
+    testSuiteStarted: [0]
+    testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testSuiteFinished: [0]
+    testSuiteStarted: [1]
+    testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleAssumptionFailureTest)
+    testSuiteFinished: [1]
+    testSuiteFinished: classes
+    testRunFinished: 4,0,4,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ClassRuleAssumptionFailureTest {
+        @ClassRule
+        public static final TestRule sClassRule = new TestRule() {
+            @Override
+            public Statement apply(Statement base, Description description) {
+                assumeTrue(false);
+                return null; // unreachable
+            }
+        };
+
+        public ClassRuleAssumptionFailureTest(String param) {
+        }
+
+        @Parameters
+        public static List<String> getParams() {
+            var params = new ArrayList<String>();
+            params.add("foo");
+            params.add("bar");
+            return params;
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * General exception from a @ClassRule.
+     */
+    @RunWith(ParameterizedAndroidJunit4.class)
+    // Because the test uses ParameterizedAndroidJunit4 with two parameters,
+    // the whole class is executed twice.
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest
+    testSuiteStarted: [0]
+    testStarted: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testStarted: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[0](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testSuiteFinished: [0]
+    testSuiteStarted: [1]
+    testStarted: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test1[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testStarted: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testAssumptionFailure: got: <false>, expected: is <true>
+    testFinished: test2[1](com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassRuleExceptionTest)
+    testSuiteFinished: [1]
+    testSuiteFinished: classes
+    testRunFinished: 4,0,4,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ClassRuleExceptionTest {
+        @ClassRule
+        public static final TestRule sClassRule = new TestRule() {
+            @Override
+            public Statement apply(Statement base, Description description) {
+                assumeTrue(false);
+                return null; // unreachable
+            }
+        };
+
+        public ClassRuleExceptionTest(String param) {
+        }
+
+        @Parameters
+        public static List<String> getParams() {
+            var params = new ArrayList<String>();
+            params.add("foo");
+            params.add("bar");
+            return params;
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * General exception from a @ClassRule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+    testFailure: Exception detected in constructor
+    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ExceptionFromInnerRunnerConstructorTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ExceptionFromInnerRunnerConstructorTest {
+        public ExceptionFromInnerRunnerConstructorTest(String arg1, String arg2) {
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * The test class is unloadable, but has a @DisabledOnRavenwood.
+     */
+    @RunWith(AndroidJUnit4.class)
+    @DisabledOnRavenwood
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+    testIgnored: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+    testSuiteFinished: ClassUnloadbleAndDisabledTest(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndDisabledTest)
+    testSuiteFinished: classes
+    testRunFinished: 0,0,0,1
+    """)
+    // CHECKSTYLE:ON
+    public static class ClassUnloadbleAndDisabledTest {
+        static {
+            Assert.fail("Class unloadable!");
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    /**
+     * The test class is unloadable, but has a @DisabledOnRavenwood.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest
+    testStarted: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+    testFailure: Class unloadable!
+    testFinished: test1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+    testStarted: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+    testFailure: Class unloadable!
+    testFinished: test2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$ClassUnloadbleAndEnabledTest)
+    testSuiteFinished: classes
+    testRunFinished: 2,2,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ClassUnloadbleAndEnabledTest {
+        static {
+            Assert.fail("Class unloadable!");
+        }
+
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+
+    public static class BrokenTestRunner extends BlockJUnit4ClassRunner {
+        public BrokenTestRunner(Class<?> testClass) throws InitializationError {
+            super(testClass);
+
+            if (true)  {
+                throw new RuntimeException("This is a broken test runner!");
+            }
+        }
+    }
+
+    /**
+     * The test runner throws an exception from the ctor.
+     */
+    @RunWith(BrokenTestRunner.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+    testFailure: Exception detected in constructor
+    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerCallbackTest$BrokenRunnerTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class BrokenRunnerTest {
+        @Test
+        public void test1() {
+        }
+
+        @Test
+        public void test2() {
+        }
+    }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
new file mode 100644
index 0000000..73ea64f
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerConfigValidationTest.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.runnercallbacktests;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodConfig;
+import android.platform.test.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestRule;
+import org.junit.runner.RunWith;
+
+
+/**
+ * Test for @Config field extraction and validation.
+ */
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public class RavenwoodRunnerConfigValidationTest extends RavenwoodRunnerTestBase {
+    public abstract static class ConfigInBaseClass {
+        static String PACKAGE_NAME = "com.ConfigInBaseClass";
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+                .setPackageName(PACKAGE_NAME).build();
+    }
+
+    /**
+     * Make sure a config in the base class is detected.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest
+    testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest)
+    testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigInBaseClassTest
+    testSuiteFinished: classes
+    testRunFinished: 1,0,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ConfigInBaseClassTest extends ConfigInBaseClass {
+        @Test
+        public void test() {
+            assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+                    .isEqualTo(PACKAGE_NAME);
+        }
+    }
+
+    /**
+     * Make sure a config in the base class is detected.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest
+    testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest)
+    testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigOverridingTest
+    testSuiteFinished: classes
+    testRunFinished: 1,0,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ConfigOverridingTest extends ConfigInBaseClass {
+        static String PACKAGE_NAME_OVERRIDE = "com.ConfigOverridingTest";
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder()
+                .setPackageName(PACKAGE_NAME_OVERRIDE).build();
+
+        @Test
+        public void test() {
+            assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+                    .isEqualTo(PACKAGE_NAME_OVERRIDE);
+        }
+    }
+
+    /**
+     * Test to make sure that if a test has a config error, the failure would be reported from
+     * each test method.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+    testFinished: testMethod1(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testStarted: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+    testFinished: testMethod2(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testStarted: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest.sConfig expected to be public static
+    testFinished: testMethod3(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ErrorMustBeReportedFromEachTest)
+    testSuiteFinished: classes
+    testRunFinished: 3,3,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ErrorMustBeReportedFromEachTest {
+        @RavenwoodConfig.Config
+        private static RavenwoodConfig sConfig = // Invalid because it's private.
+                new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void testMethod1() {
+        }
+
+        @Test
+        public void testMethod2() {
+        }
+
+        @Test
+        public void testMethod3() {
+        }
+    }
+
+    /**
+     * Invalid because there are two @Config's.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+    testFailure: Class com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.DuplicateConfigTest has multiple fields with @RavenwoodConfig.Config
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateConfigTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class DuplicateConfigTest {
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig1 =
+                new RavenwoodConfig.Builder().build();
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig2 =
+                new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+    }
+
+    /**
+     * @Config's must be static.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest.sConfig expected to be public static
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonStaticConfigTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class NonStaticConfigTest {
+
+        @RavenwoodConfig.Config
+        public RavenwoodConfig sConfig =
+                new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+    }
+
+    /**
+     * @Config's must be public.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+    testFailure: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest.sConfig expected to be public static
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$NonPublicConfigTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class NonPublicConfigTest {
+
+        @RavenwoodConfig.Config
+        RavenwoodConfig sConfig =
+                new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+    }
+
+    /**
+     * @Config's must be of type RavenwoodConfig.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+    testFailure: Field com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest.WrongTypeConfigTest.sConfig has @RavenwoodConfig.Config but type is not RavenwoodConfig
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeConfigTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class WrongTypeConfigTest {
+
+        @RavenwoodConfig.Config
+        public static Object sConfig =
+                new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+
+    }
+
+    /**
+     * @Rule must be of type RavenwoodRule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest)
+    testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WrongTypeRuleTest
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class WrongTypeRuleTest {
+
+        @Rule
+        public TestRule mRule = new RavenwoodRule.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+
+    }
+
+    /**
+     * Config can't be used with a (instance) Rule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+    testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
+    testFinished: testConfig(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithInstanceRuleTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class WithInstanceRuleTest {
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig =
+                new RavenwoodConfig.Builder().build();
+
+        @Rule
+        public RavenwoodRule mRule = new RavenwoodRule.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+    }
+
+    /**
+     * Config can't be used with a (static) Rule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+    testFailure: Exception detected in constructor
+    testFinished: Constructor(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$WithStaticRuleTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class WithStaticRuleTest {
+
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig =
+                new RavenwoodConfig.Builder().build();
+
+        @Rule
+        public static RavenwoodRule sRule = new RavenwoodRule.Builder().build();
+
+        @Test
+        public void testConfig() {
+        }
+    }
+
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest
+    testStarted: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest)
+    testFailure: Multiple nesting RavenwoodRule's are detected in the same class, which is not supported.
+    testFinished: testMultipleRulesNotAllowed(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$DuplicateRulesTest
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class DuplicateRulesTest {
+
+        @Rule
+        public final RavenwoodRule mRavenwood1 = new RavenwoodRule();
+
+        @Rule
+        public final RavenwoodRule mRavenwood2 = new RavenwoodRule();
+
+        @Test
+        public void testMultipleRulesNotAllowed() {
+        }
+    }
+
+    public static class RuleInBaseClass {
+        static String PACKAGE_NAME = "com.RuleInBaseClass";
+        @Rule
+        public final RavenwoodRule mRavenwood1 = new RavenwoodRule.Builder()
+                .setPackageName(PACKAGE_NAME).build();
+    }
+
+    /**
+     * Make sure that RavenwoodRule in a base class takes effect.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest
+    testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest)
+    testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleInBaseClassSuccessTest
+    testSuiteFinished: classes
+    testRunFinished: 1,0,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class RuleInBaseClassSuccessTest extends RuleInBaseClass {
+
+        @Test
+        public void testRuleInBaseClass() {
+            assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+                    .isEqualTo(PACKAGE_NAME);
+        }
+    }
+
+    /**
+     * Make sure that having a config and a rule in a base class should fail.
+     * RavenwoodRule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+    testFailure: RavenwoodConfig and RavenwoodRule cannot be used in the same class. Suggest migrating to RavenwoodConfig.
+    testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleInBaseClassTest)
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ConfigWithRuleInBaseClassTest extends RuleInBaseClass {
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void test() {
+        }
+    }
+
+    /**
+     * Same as {@link RuleInBaseClass}, but the type of the rule field is not {@link RavenwoodRule}.
+     */
+    public abstract static class RuleWithDifferentTypeInBaseClass {
+        static String PACKAGE_NAME = "com.RuleWithDifferentTypeInBaseClass";
+        @Rule
+        public final TestRule mRavenwood1 = new RavenwoodRule.Builder()
+                .setPackageName(PACKAGE_NAME).build();
+    }
+
+    /**
+     * Make sure that RavenwoodRule in a base class takes effect, even if the field type is not
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest
+    testStarted: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest)
+    testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+    testFinished: testRuleInBaseClass(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$RuleWithDifferentTypeInBaseClassSuccessTest
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class RuleWithDifferentTypeInBaseClassSuccessTest extends RuleWithDifferentTypeInBaseClass {
+
+        @Test
+        public void testRuleInBaseClass() {
+            assertThat(InstrumentationRegistry.getInstrumentation().getContext().getPackageName())
+                    .isEqualTo(PACKAGE_NAME);
+        }
+    }
+
+    /**
+     * Make sure that having a config and a rule in a base class should fail, even if the field type is not
+     * RavenwoodRule.
+     */
+    @RunWith(AndroidJUnit4.class)
+    // CHECKSTYLE:OFF
+    @Expected("""
+    testRunStarted: classes
+    testSuiteStarted: classes
+    testSuiteStarted: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest
+    testStarted: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest)
+    testFailure: If you have a RavenwoodRule in your test, make sure the field type is RavenwoodRule so Ravenwood can detect it.
+    testFinished: test(com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest)
+    testSuiteFinished: com.android.ravenwoodtest.runnercallbacktests.RavenwoodRunnerConfigValidationTest$ConfigWithRuleWithDifferentTypeInBaseClassTest
+    testSuiteFinished: classes
+    testRunFinished: 1,1,0,0
+    """)
+    // CHECKSTYLE:ON
+    public static class ConfigWithRuleWithDifferentTypeInBaseClassTest extends RuleWithDifferentTypeInBaseClass {
+        @RavenwoodConfig.Config
+        public static RavenwoodConfig sConfig = new RavenwoodConfig.Builder().build();
+
+        @Test
+        public void test() {
+        }
+    }
+}
diff --git a/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java
new file mode 100644
index 0000000..9a6934bf
--- /dev/null
+++ b/ravenwood/tests/coretest/test/com/android/ravenwoodtest/runnercallbacktests/RavenwoodRunnerTestBase.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2024 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.ravenwoodtest.runnercallbacktests;
+
+import static com.google.common.truth.Truth.assertThat;
+import static com.google.common.truth.Truth.assertWithMessage;
+
+import android.platform.test.annotations.NoRavenizer;
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner;
+import android.util.Log;
+
+import junitparams.JUnitParamsRunner;
+import junitparams.Parameters;
+import org.junit.Test;
+import org.junit.runner.Description;
+import org.junit.runner.JUnitCore;
+import org.junit.runner.Result;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.Failure;
+import org.junit.runner.notification.RunListener;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.function.BiConsumer;
+
+
+/**
+ * Base class for tests to make sure {@link RavenwoodAwareTestRunner} produces expected callbacks
+ * in various situations. (most of them are error situations.)
+ *
+ * Subclasses must contain test classes as static inner classes with an {@link Expected} annotation.
+ * This class finds them using reflections and run them one by one directly using {@link JUnitCore},
+ * and check the callbacks.
+ *
+ * Subclasses do no need to have any test methods.
+ *
+ * The {@link Expected} annotation must contain the expected result as a string.
+ *
+ * This test abuses the fact that atest + tradefed + junit won't run nested classes automatically.
+ * (So atest won't show any results directly from the nested classes.)
+ *
+ * The actual test method is {@link #doTest}, which is executed for each target test class, using
+ * junit-params.
+ */
+@RunWith(JUnitParamsRunner.class)
+@NoRavenizer // This class shouldn't be executed with RavenwoodAwareTestRunner.
+public abstract class RavenwoodRunnerTestBase {
+    private static final String TAG = "RavenwoodRunnerTestBase";
+
+    /**
+     * Annotation to specify the expected result for a class.
+     */
+    @Target({ElementType.TYPE})
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface Expected {
+        String value();
+    }
+
+    /**
+     * Take a multiline string, strip all of them, remove empty lines, and return it.
+     */
+    private static String stripMultiLines(String resultString) {
+        var list = new ArrayList<String>();
+        for (var line : resultString.split("\n")) {
+            var s = line.strip();
+            if (s.length() > 0) {
+                list.add(s);
+            }
+        }
+        return String.join("\n", list);
+    }
+
+    /**
+     * Extract the expected result from @Expected.
+     */
+    private String getExpectedResult(Class<?> testClazz) {
+        var expect = testClazz.getAnnotation(Expected.class);
+        return stripMultiLines(expect.value());
+    }
+
+    /**
+     * List all the nested classrs with an {@link Expected} annotation in a given class.
+     */
+    public Class<?>[] getTestClasses() {
+        var thisClass = this.getClass();
+        var ret = Arrays.stream(thisClass.getNestMembers())
+                .filter((c) -> c.getAnnotation(Expected.class) != null)
+                .toArray(Class[]::new);
+
+        assertThat(ret.length).isGreaterThan(0);
+
+        return ret;
+    }
+
+    /**
+     * This is the actual test method. We use junit-params to run this method for each target
+     * test class, which are returned by {@link #getTestClasses}.
+     *
+     * It runs each test class, and compare the result collected with
+     * {@link ResultCollectingListener} to expected results (as strings).
+     */
+    @Test
+    @Parameters(method = "getTestClasses")
+    public void doTest(Class<?> testClazz) {
+        doTest(testClazz, getExpectedResult(testClazz));
+    }
+
+    /**
+     * Run a given test class, and compare the result collected with
+     * {@link ResultCollectingListener} to expected results (as a string).
+     */
+    private void doTest(Class<?> testClazz, String expectedResult) {
+        Log.i(TAG, "Running test for " + testClazz);
+        var junitCore = new JUnitCore();
+
+        // Create a listener.
+        var listener = new ResultCollectingListener();
+        junitCore.addListener(listener);
+
+        // Set a listener to critical errors. This will also prevent
+        // {@link RavenwoodAwareTestRunner} from calling System.exit() when there's
+        // a critical error.
+        RavenwoodAwareTestRunner.private$ravenwood().setCriticalErrorHandler(
+                listener.sCriticalErrorListener);
+
+        try {
+            // Run the test class.
+            junitCore.run(testClazz);
+        } finally {
+            // Clear the critical error listener.
+            RavenwoodAwareTestRunner.private$ravenwood().setCriticalErrorHandler(null);
+        }
+
+        // Check the result.
+        assertWithMessage("Failure in test class: " + testClazz.getCanonicalName() + "]")
+                .that(listener.getResult())
+                .isEqualTo(expectedResult);
+    }
+
+    /**
+     * A JUnit RunListener that collects all the callbacks as a single string.
+     */
+    private static class ResultCollectingListener extends RunListener {
+        private final ArrayList<String> mResult = new ArrayList<>();
+
+        public final BiConsumer<String, Throwable> sCriticalErrorListener = (message, th) -> {
+            mResult.add("criticalError: " + message + ": " + th.getMessage());
+        };
+
+        @Override
+        public void testRunStarted(Description description) throws Exception {
+            mResult.add("testRunStarted: " + description);
+        }
+
+        @Override
+        public void testRunFinished(Result result) throws Exception {
+            mResult.add("testRunFinished: "
+                    + result.getRunCount() + ","
+                    + result.getFailureCount() + ","
+                    + result.getAssumptionFailureCount() + ","
+                    + result.getIgnoreCount());
+        }
+
+        @Override
+        public void testSuiteStarted(Description description) throws Exception {
+            mResult.add("testSuiteStarted: " + description);
+        }
+
+        @Override
+        public void testSuiteFinished(Description description) throws Exception {
+            mResult.add("testSuiteFinished: " + description);
+        }
+
+        @Override
+        public void testStarted(Description description) throws Exception {
+            mResult.add("testStarted: " + description);
+        }
+
+        @Override
+        public void testFinished(Description description) throws Exception {
+            mResult.add("testFinished: " + description);
+        }
+
+        @Override
+        public void testFailure(Failure failure) throws Exception {
+            mResult.add("testFailure: " + failure.getException().getMessage());
+        }
+
+        @Override
+        public void testAssumptionFailure(Failure failure) {
+            mResult.add("testAssumptionFailure: " + failure.getException().getMessage());
+        }
+
+        @Override
+        public void testIgnored(Description description) throws Exception {
+            mResult.add("testIgnored: " + description);
+        }
+
+        public String getResult() {
+            return String.join("\n", mResult);
+        }
+    }
+}
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index f3172ae..9c86389 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -9,17 +9,13 @@
 com.android.internal.logging.testing.UiEventLoggerFake
 com.android.internal.os.AndroidPrintStream
 com.android.internal.os.BatteryStatsHistory
-com.android.internal.os.BatteryStatsHistory$TraceDelegate
-com.android.internal.os.BatteryStatsHistory$VarintParceler
 com.android.internal.os.BatteryStatsHistoryIterator
 com.android.internal.os.Clock
 com.android.internal.os.LongArrayMultiStateCounter
-com.android.internal.os.LongArrayMultiStateCounter$LongArrayContainer
 com.android.internal.os.LongMultiStateCounter
 com.android.internal.os.MonotonicClock
 com.android.internal.os.PowerProfile
 com.android.internal.os.PowerStats
-com.android.internal.os.PowerStats$Descriptor
 com.android.internal.os.RuntimeInit
 com.android.internal.power.EnergyConsumerStats
 com.android.internal.power.ModemPowerProfile
@@ -39,12 +35,14 @@
 android.util.DataUnit
 android.util.DayOfMonthCursor
 android.util.DebugUtils
+android.util.DisplayMetrics
 android.util.Dumpable
 android.util.DumpableContainer
 android.util.EmptyArray
 android.util.EventLog
 android.util.FloatProperty
 android.util.FloatMath
+android.util.Half
 android.util.IndentingPrintWriter
 android.util.IntArray
 android.util.IntProperty
@@ -98,10 +96,12 @@
 android.util.SparseIntArray
 android.util.SparseLongArray
 android.util.SparseSetArray
+android.util.StateSet
 android.util.StringBuilderPrinter
 android.util.TeeWriter
 android.util.TimeUtils
 android.util.TimingsTraceLog
+android.util.TypedValue
 android.util.UtilConfig
 android.util.Xml
 
@@ -119,15 +119,9 @@
 android.os.BaseBundle
 android.os.BatteryConsumer
 android.os.BatteryStats
-android.os.BatteryStats$HistoryItem
-android.os.BatteryStats$HistoryStepDetails
-android.os.BatteryStats$HistoryTag
-android.os.BatteryStats$LongCounter
-android.os.BatteryStats$ProcessStateChange
 android.os.BatteryUsageStats
 android.os.BatteryUsageStatsQuery
 android.os.Binder
-android.os.Binder$IdentitySupplier
 android.os.BluetoothBatteryStats
 android.os.Broadcaster
 android.os.Build
@@ -138,19 +132,17 @@
 android.os.DeadObjectException
 android.os.DeadSystemException
 android.os.FileUtils
-android.os.FileUtils$MemoryPipe
 android.os.Handler
 android.os.HandlerExecutor
 android.os.HandlerThread
 android.os.IBinder
+android.os.LocaleList
 android.os.Looper
 android.os.Message
 android.os.MessageQueue
 android.os.PackageTagsList
 android.os.Parcel
 android.os.ParcelFileDescriptor
-android.os.ParcelFileDescriptor$AutoCloseInputStream
-android.os.ParcelFileDescriptor$AutoCloseOutputStream
 android.os.ParcelFormatException
 android.os.ParcelUuid
 android.os.Parcelable
@@ -164,7 +156,6 @@
 android.os.RemoteException
 android.os.ResultReceiver
 android.os.ServiceManager
-android.os.ServiceManager$ServiceNotFoundException
 android.os.ServiceSpecificException
 android.os.StrictMode
 android.os.SystemClock
@@ -175,23 +166,20 @@
 android.os.Trace
 android.os.TransactionTooLargeException
 android.os.UserBatteryConsumer
-android.os.UserBatteryConsumer$Builder
 android.os.UidBatteryConsumer
-android.os.UidBatteryConsumer$Builder
 android.os.UserHandle
 android.os.UserManager
 android.os.VibrationAttributes
-android.os.VibrationAttributes$Builder
 android.os.WakeLockStats
 android.os.WorkSource
 
 android.content.ClipData
-android.content.ClipData$Item
 android.content.ClipDescription
 android.content.ClipboardManager
 android.content.ComponentName
 android.content.ContentUris
 android.content.ContentValues
+android.content.Context
 android.content.ContextWrapper
 android.content.Intent
 android.content.IntentFilter
@@ -202,11 +190,7 @@
 android.content.pm.ComponentInfo
 android.content.pm.PackageInfo
 android.content.pm.PackageItemInfo
-android.content.pm.PackageManager$Flags
-android.content.pm.PackageManager$PackageInfoFlags
-android.content.pm.PackageManager$ApplicationInfoFlags
-android.content.pm.PackageManager$ComponentInfoFlags
-android.content.pm.PackageManager$ResolveInfoFlags
+android.content.pm.PackageManager
 android.content.pm.PathPermission
 android.content.pm.ProviderInfo
 android.content.pm.ResolveInfo
@@ -214,6 +198,32 @@
 android.content.pm.Signature
 android.content.pm.UserInfo
 
+android.content.res.ApkAssets
+android.content.res.AssetFileDescriptor
+android.content.res.AssetManager
+android.content.res.ColorStateList
+android.content.res.ConfigurationBoundResourceCache
+android.content.res.Configuration
+android.content.res.CompatibilityInfo
+android.content.res.ComplexColor
+android.content.res.ConstantState
+android.content.res.DrawableCache
+android.content.res.Element
+android.content.res.FontResourcesParser
+android.content.res.FontScaleConverter
+android.content.res.FontScaleConverterImpl
+android.content.res.FontScaleConverterFactory
+android.content.res.Resources
+android.content.res.ResourceId
+android.content.res.ResourcesImpl
+android.content.res.ResourcesKey
+android.content.res.StringBlock
+android.content.res.TagCounter
+android.content.res.ThemedResourceCache
+android.content.res.TypedArray
+android.content.res.Validator
+android.content.res.XmlBlock
+
 android.database.AbstractCursor
 android.database.CharArrayBuffer
 android.database.ContentObservable
@@ -226,7 +236,6 @@
 android.database.DataSetObservable
 android.database.DataSetObserver
 android.database.MatrixCursor
-android.database.MatrixCursor$RowBuilder
 android.database.MergeCursor
 android.database.Observable
 android.database.SQLException
@@ -234,7 +243,6 @@
 android.database.sqlite.SQLiteException
 
 android.text.TextUtils
-android.text.TextUtils$SimpleStringSplitter
 
 android.accounts.Account
 
@@ -244,7 +252,11 @@
 android.graphics.Insets
 android.graphics.Interpolator
 android.graphics.Matrix
+android.graphics.Matrix44
+android.graphics.Outline
+android.graphics.ParcelableColorSpace
 android.graphics.Path
+android.graphics.PixelFormat
 android.graphics.Point
 android.graphics.PointF
 android.graphics.Rect
@@ -254,15 +266,18 @@
 
 android.app.ActivityManager
 android.app.ActivityOptions
+android.app.ApplicationPackageManager
 android.app.BroadcastOptions
 android.app.ComponentOptions
 android.app.Instrumentation
+android.app.LocaleConfig
+android.app.ResourcesManager
+android.app.WindowConfiguration
 
 android.metrics.LogMaker
 
 android.view.Display
-android.view.Display$HdrCapabilities
-android.view.Display$Mode
+android.view.DisplayAdjustments
 android.view.DisplayInfo
 android.view.inputmethod.InputBinding
 
@@ -274,10 +289,18 @@
 android.telephony.ModemActivityInfo
 android.telephony.ServiceState
 
+android.os.connectivity.CellularBatteryStats
 android.os.connectivity.WifiActivityEnergyInfo
+android.os.connectivity.WifiBatteryStats
 
 com.android.server.LocalServices
 
+com.android.internal.graphics.cam.Cam
+com.android.internal.graphics.cam.CamUtils
+com.android.internal.graphics.cam.Frame
+com.android.internal.graphics.cam.HctSolver
+com.android.internal.graphics.ColorUtils
+
 com.android.internal.util.BitUtils
 com.android.internal.util.BitwiseInputStream
 com.android.internal.util.BitwiseOutputStream
@@ -302,6 +325,7 @@
 com.android.internal.util.QuickSelect
 com.android.internal.util.RingBuffer
 com.android.internal.util.SizedInputStream
+com.android.internal.util.RateLimitingCache
 com.android.internal.util.StringPool
 com.android.internal.util.TokenBucket
 com.android.internal.util.XmlPullParserWrapper
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index 4012bdc..d962c82 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -9,11 +9,21 @@
 # Keep all sysprops generated code implementations
 class :sysprops keepclass
 
+# Keep all resource R classes
+class :r keepclass
+
 # To avoid VerifyError on nano proto files (b/324063814), we rename nano proto classes.
-# Note: The "rename" directive must use shashes (/) as a package name separator.
+# Note: The "rename" directive must use slashes (/) as a package name separator.
 rename com/.*/nano/   devicenano/
 rename android/.*/nano/   devicenano/
 
+# Support APIs not available in standard JRE
+class java.io.FileDescriptor keep
+    method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
+    method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
+class java.util.LinkedHashMap keep
+    method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
+
 # Exported to Mainline modules; cannot use annotations
 class com.android.internal.util.FastXmlSerializer keepclass
 class com.android.internal.util.FileRotator keepclass
@@ -62,3 +72,7 @@
     method <init> ()V keep
 class android.text.ClipboardManager keep
     method <init> ()V keep
+
+# Just enough to allow ResourcesManager to run
+class android.hardware.display.DisplayManagerGlobal keep
+    method getInstance ()Landroid/hardware/display/DisplayManagerGlobal; ignore
diff --git a/ravenwood/texts/ravenwood-standard-options.txt b/ravenwood/texts/ravenwood-standard-options.txt
index f64f26d..3ec3e3c 100644
--- a/ravenwood/texts/ravenwood-standard-options.txt
+++ b/ravenwood/texts/ravenwood-standard-options.txt
@@ -6,8 +6,6 @@
 --default-throw
 
 # Uncomment below lines to enable each feature.
-# --enable-non-stub-method-check
---no-non-stub-method-check
 
 #--default-method-call-hook
 #    com.android.hoststubgen.hosthelper.HostTestUtils.logMethodCall
@@ -34,8 +32,11 @@
 --substitute-annotation
     android.ravenwood.annotation.RavenwoodReplace
 
---native-substitute-annotation
-    android.ravenwood.annotation.RavenwoodNativeSubstitutionClass
+--redirect-annotation
+    android.ravenwood.annotation.RavenwoodRedirect
+
+--redirection-class-annotation
+    android.ravenwood.annotation.RavenwoodRedirectionClass
 
 --class-load-hook-annotation
     android.ravenwood.annotation.RavenwoodClassLoadHook
diff --git a/ravenwood/tools/ravenizer/Android.bp b/ravenwood/tools/ravenizer/Android.bp
new file mode 100644
index 0000000..2892d07
--- /dev/null
+++ b/ravenwood/tools/ravenizer/Android.bp
@@ -0,0 +1,25 @@
+package {
+    // See: http://go/android-license-faq
+    // A large-scale-change added 'default_applicable_licenses' to import
+    // all of the 'license_kinds' from "frameworks_base_license"
+    // to get the below license kinds:
+    //   SPDX-license-identifier-Apache-2.0
+    default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_binary_host {
+    name: "ravenizer",
+    main_class: "com.android.platform.test.ravenwood.ravenizer.RavenizerMain",
+    srcs: ["src/**/*.kt"],
+    static_libs: [
+        "hoststubgen-lib",
+        "ow2-asm",
+        "ow2-asm-analysis",
+        "ow2-asm-commons",
+        "ow2-asm-tree",
+        "ow2-asm-util",
+        "junit",
+        "ravenwood-junit-impl-for-ravenizer",
+    ],
+    visibility: ["//visibility:public"],
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
new file mode 100644
index 0000000..0dcd271
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Exceptions.kt
@@ -0,0 +1,30 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+@file:Suppress("ktlint:standard:filename")
+
+package com.android.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.UserErrorException
+
+/**
+ * Use it for internal exception that really shouldn't happen.
+ */
+class RavenizerInternalException(message: String) : Exception(message)
+
+/**
+ * Thrown when an invalid test is detected in the target jar. (e.g. JUni3 tests)
+ */
+class RavenizerInvalidTestException(message: String) : Exception(message), UserErrorException
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
new file mode 100644
index 0000000..f7f9a85
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Ravenizer.kt
@@ -0,0 +1,249 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.GeneralUserErrorException
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.zipEntryNameToClassName
+import com.android.hoststubgen.executableName
+import com.android.hoststubgen.log
+import com.android.platform.test.ravenwood.ravenizer.adapter.RunnerRewritingAdapter
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.ClassWriter
+import org.objectweb.asm.util.CheckClassAdapter
+import java.io.BufferedInputStream
+import java.io.BufferedOutputStream
+import java.io.FileOutputStream
+import java.io.InputStream
+import java.io.OutputStream
+import java.util.zip.ZipEntry
+import java.util.zip.ZipFile
+import java.util.zip.ZipOutputStream
+
+/**
+ * Various stats on Ravenizer.
+ */
+data class RavenizerStats(
+    /** Total end-to-end time. */
+    var totalTime: Double = .0,
+
+    /** Time took to build [ClasNodes] */
+    var loadStructureTime: Double = .0,
+
+    /** Time took to validate the classes */
+    var validationTime: Double = .0,
+
+    /** Total real time spent for converting the jar file */
+    var totalProcessTime: Double = .0,
+
+    /** Total real time spent for converting class files (except for I/O time). */
+    var totalConversionTime: Double = .0,
+
+    /** Total real time spent for copying class files without modification. */
+    var totalCopyTime: Double = .0,
+
+    /** # of entries in the input jar file */
+    var totalEntiries: Int = 0,
+
+    /** # of *.class files in the input jar file */
+    var totalClasses: Int = 0,
+
+    /** # of *.class files that have been processed. */
+    var processedClasses: Int = 0,
+) {
+    override fun toString(): String {
+        return """
+            RavenizerStats{
+              totalTime=$totalTime,
+              loadStructureTime=$loadStructureTime,
+              validationTime=$validationTime,
+              totalProcessTime=$totalProcessTime,
+              totalConversionTime=$totalConversionTime,
+              totalCopyTime=$totalCopyTime,
+              totalEntiries=$totalEntiries,
+              totalClasses=$totalClasses,
+              processedClasses=$processedClasses,
+            }
+            """.trimIndent()
+    }
+}
+
+/**
+ * Main class.
+ */
+class Ravenizer(val options: RavenizerOptions) {
+    fun run() {
+        val stats = RavenizerStats()
+
+        val fatalValidation = options.fatalValidation.get
+
+        stats.totalTime = log.nTime {
+            process(
+                options.inJar.get,
+                options.outJar.get,
+                options.enableValidation.get,
+                fatalValidation,
+                stats,
+            )
+        }
+        log.i(stats.toString())
+    }
+
+    private fun process(
+        inJar: String,
+        outJar: String,
+        enableValidation: Boolean,
+        fatalValidation: Boolean,
+        stats: RavenizerStats,
+    ) {
+        var allClasses = ClassNodes.loadClassStructures(inJar) {
+            time -> stats.loadStructureTime = time
+        }
+        if (enableValidation) {
+            stats.validationTime = log.iTime("Validating classes") {
+                if (!validateClasses(allClasses)) {
+                    var message = "Invalid test class(es) detected." +
+                            " See error log for details."
+                    if (fatalValidation) {
+                        throw RavenizerInvalidTestException(message)
+                    } else {
+                        log.w("Warning: $message")
+                    }
+                }
+            }
+        }
+
+        stats.totalProcessTime = log.vTime("$executableName processing $inJar") {
+            ZipFile(inJar).use { inZip ->
+                val inEntries = inZip.entries()
+
+                stats.totalEntiries = inZip.size()
+
+                ZipOutputStream(BufferedOutputStream(FileOutputStream(outJar))).use { outZip ->
+                    while (inEntries.hasMoreElements()) {
+                        val entry = inEntries.nextElement()
+
+                        if (entry.name.endsWith(".dex")) {
+                            // Seems like it's an ART jar file. We can't process it.
+                            // It's a fatal error.
+                            throw GeneralUserErrorException(
+                                "$inJar is not a desktop jar file. It contains a *.dex file."
+                            )
+                        }
+
+                        val className = zipEntryNameToClassName(entry.name)
+
+                        if (className != null) {
+                            stats.totalClasses += 1
+                        }
+
+                        if (className != null && shouldProcessClass(allClasses, className)) {
+                            stats.processedClasses += 1
+                            processSingleClass(inZip, entry, outZip, allClasses, stats)
+                        } else {
+                            // Too slow, let's use merge_zips to bring back the original classes.
+                            copyZipEntry(inZip, entry, outZip, stats)
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Copy a single ZIP entry to the output.
+     */
+    private fun copyZipEntry(
+        inZip: ZipFile,
+        entry: ZipEntry,
+        out: ZipOutputStream,
+        stats: RavenizerStats,
+    ) {
+        stats.totalCopyTime += log.nTime {
+            inZip.getInputStream(entry).use { ins ->
+                // Copy unknown entries as is to the impl out. (but not to the stub out.)
+                val outEntry = ZipEntry(entry.name)
+                outEntry.method = 0
+                outEntry.size = entry.size
+                outEntry.crc = entry.crc
+                out.putNextEntry(outEntry)
+
+                ins.transferTo(out)
+
+                out.closeEntry()
+            }
+        }
+    }
+
+    private fun processSingleClass(
+        inZip: ZipFile,
+        entry: ZipEntry,
+        outZip: ZipOutputStream,
+        allClasses: ClassNodes,
+        stats: RavenizerStats,
+    ) {
+        val newEntry = ZipEntry(entry.name)
+        outZip.putNextEntry(newEntry)
+
+        BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
+            processSingleClass(entry, bis, outZip, allClasses, stats)
+        }
+        outZip.closeEntry()
+    }
+
+    /**
+     * Whether a class needs to be processed. This must be kept in sync with [processSingleClass].
+     */
+    private fun shouldProcessClass(classes: ClassNodes, classInternalName: String): Boolean {
+        return !classInternalName.shouldByBypassed()
+                && RunnerRewritingAdapter.shouldProcess(classes, classInternalName)
+    }
+
+    private fun processSingleClass(
+        entry: ZipEntry,
+        input: InputStream,
+        output: OutputStream,
+        allClasses: ClassNodes,
+        stats: RavenizerStats,
+    ) {
+        val cr = ClassReader(input)
+
+        lateinit var data: ByteArray
+        stats.totalConversionTime += log.vTime("Modify ${entry.name}") {
+
+            val classInternalName = zipEntryNameToClassName(entry.name)
+                ?: throw RavenizerInternalException("Unexpected zip entry name: ${entry.name}")
+            val flags = ClassWriter.COMPUTE_MAXS
+            val cw = ClassWriter(flags)
+            var outVisitor: ClassVisitor = cw
+
+            val enableChecker = false
+            if (enableChecker) {
+                outVisitor = CheckClassAdapter(outVisitor)
+            }
+
+            // This must be kept in sync with shouldProcessClass.
+            outVisitor = RunnerRewritingAdapter.maybeApply(
+                classInternalName, allClasses, outVisitor)
+
+            cr.accept(outVisitor, ClassReader.EXPAND_FRAMES)
+
+            data = cw.toByteArray()
+        }
+        output.write(data)
+    }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
new file mode 100644
index 0000000..ff41818
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerMain.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 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.
+ */
+@file:JvmName("RavenizerMain")
+
+package com.android.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.LogLevel
+import com.android.hoststubgen.executableName
+import com.android.hoststubgen.log
+import com.android.hoststubgen.runMainWithBoilerplate
+
+/**
+ * Entry point.
+ */
+fun main(args: Array<String>) {
+    executableName = "Ravenizer"
+    log.setConsoleLogLevel(LogLevel.Info)
+
+    runMainWithBoilerplate {
+        val options = RavenizerOptions.parseArgs(args)
+
+        log.i("$executableName started")
+        log.v("Options: $options")
+
+        // Run.
+        Ravenizer(options).run()
+    }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
new file mode 100644
index 0000000..10fe0a3
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/RavenizerOptions.kt
@@ -0,0 +1,115 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.ArgIterator
+import com.android.hoststubgen.ArgumentsException
+import com.android.hoststubgen.SetOnce
+import com.android.hoststubgen.ensureFileExists
+import com.android.hoststubgen.log
+import java.nio.file.Paths
+import kotlin.io.path.exists
+
+/**
+ * If this file exits, we also read options from it. This is "unsafe" because it could break
+ * incremental builds, if it sets any flag that affects the output file.
+ * (however, for now, there's no such options.)
+ *
+ * For example, to enable verbose logging, do `echo '-v' > ~/.raveniezr-unsafe`
+ *
+ * (but even the content of this file changes, soong won't rerun the command, so you need to
+ * remove the output first and then do a build again.)
+ */
+private val RAVENIZER_DOTFILE = System.getenv("HOME") + "/.raveniezr-unsafe"
+
+class RavenizerOptions(
+    /** Input jar file*/
+    var inJar: SetOnce<String> = SetOnce(""),
+
+    /** Output jar file */
+    var outJar: SetOnce<String> = SetOnce(""),
+
+    /** Whether to enable test validation. */
+    var enableValidation: SetOnce<Boolean> = SetOnce(true),
+
+    /** Whether the validation failure is fatal or not. */
+    var fatalValidation: SetOnce<Boolean> = SetOnce(false),
+) {
+    companion object {
+
+        fun parseArgs(origArgs: Array<String>): RavenizerOptions {
+            val args = origArgs.toMutableList()
+            if (Paths.get(RAVENIZER_DOTFILE).exists()) {
+                log.i("Reading options from $RAVENIZER_DOTFILE")
+                args.add(0, "@$RAVENIZER_DOTFILE")
+            }
+
+            val ret = RavenizerOptions()
+            val ai = ArgIterator.withAtFiles(args.toTypedArray())
+
+            while (true) {
+                val arg = ai.nextArgOptional()
+                if (arg == null) {
+                    break
+                }
+
+                fun nextArg(): String = ai.nextArgRequired(arg)
+
+                if (log.maybeHandleCommandLineArg(arg) { nextArg() }) {
+                    continue
+                }
+                try {
+                    when (arg) {
+                        // TODO: Write help
+                        "-h", "--help" -> TODO("Help is not implemented yet")
+
+                        "--in-jar" -> ret.inJar.set(nextArg()).ensureFileExists()
+                        "--out-jar" -> ret.outJar.set(nextArg())
+
+                        "--enable-validation" -> ret.enableValidation.set(true)
+                        "--disable-validation" -> ret.enableValidation.set(false)
+
+                        "--fatal-validation" -> ret.fatalValidation.set(true)
+                        "--no-fatal-validation" -> ret.fatalValidation.set(false)
+
+                        else -> throw ArgumentsException("Unknown option: $arg")
+                    }
+                } catch (e: SetOnce.SetMoreThanOnceException) {
+                    throw ArgumentsException("Duplicate or conflicting argument found: $arg")
+                }
+            }
+
+            if (!ret.inJar.isSet) {
+                throw ArgumentsException("Required option missing: --in-jar")
+            }
+            if (!ret.outJar.isSet) {
+                throw ArgumentsException("Required option missing: --out-jar")
+            }
+           return ret
+        }
+    }
+
+    override fun toString(): String {
+        return """
+            RavenizerOptions{
+              inJar=$inJar,
+              outJar=$outJar,
+              enableValidation=$enableValidation,
+              fatalValidation=$fatalValidation,
+            }
+            """.trimIndent()
+    }
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
new file mode 100644
index 0000000..1aa70c08
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Utils.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.ravenizer
+
+import android.platform.test.annotations.NoRavenizer
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.startsWithAny
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import org.junit.rules.TestRule
+import org.junit.runner.RunWith
+import org.objectweb.asm.Type
+
+data class TypeHolder(
+    val clazz: Class<*>,
+) {
+    val type = Type.getType(clazz)
+    val desc = type.descriptor
+    val descAsSet = setOf<String>(desc)
+    val internlName = type.internalName
+    val humanReadableName = type.internalName.toHumanReadableClassName()
+}
+
+val testAnotType = TypeHolder(org.junit.Test::class.java)
+val ruleAnotType = TypeHolder(org.junit.Rule::class.java)
+val classRuleAnotType = TypeHolder(org.junit.ClassRule::class.java)
+val runWithAnotType = TypeHolder(RunWith::class.java)
+val innerRunnerAnotType = TypeHolder(RavenwoodAwareTestRunner.InnerRunner::class.java)
+val noRavenizerAnotType = TypeHolder(NoRavenizer::class.java)
+
+val testRuleType = TypeHolder(TestRule::class.java)
+val ravenwoodTestRunnerType = TypeHolder(RavenwoodAwareTestRunner::class.java)
+
+/**
+ * Returns true, if a test looks like it's a test class which needs to be processed.
+ */
+fun isTestLookingClass(classes: ClassNodes, className: String): Boolean {
+    // Similar to  com.android.tradefed.lite.HostUtils.testLoadClass(), except it's more lenient,
+    // and accept non-public and/or abstract classes.
+    // HostUtils also checks "Suppress" or "SuiteClasses" but this one doesn't.
+    // TODO: SuiteClasses may need to be supported.
+
+    val cn = classes.findClass(className) ?: return false
+
+    if (cn.findAnyAnnotation(runWithAnotType.descAsSet) != null) {
+        return true
+    }
+    cn.methods?.forEach { method ->
+        if (method.findAnyAnnotation(testAnotType.descAsSet) != null) {
+            return true
+        }
+    }
+
+    // Check the super class.
+    if (cn.superName == null) {
+        return false
+    }
+    return isTestLookingClass(classes, cn.superName)
+}
+
+fun String.isRavenwoodClass(): Boolean {
+    return this.startsWithAny(
+        "com/android/hoststubgen/",
+        "android/platform/test/ravenwood",
+        "com/android/ravenwood/",
+        "com/android/platform/test/ravenwood/",
+    )
+}
+
+/**
+ * Classes that should never be modified.
+ */
+fun String.shouldByBypassed(): Boolean {
+    if (this.isRavenwoodClass()) {
+        return true
+    }
+    return this.startsWithAny(
+        "java/", // just in case...
+        "javax/",
+        "junit/",
+        "org/junit/",
+        "org/mockito/",
+        "kotlin/",
+        "androidx/",
+        "android/support/",
+        // TODO -- anything else?
+    )
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
new file mode 100644
index 0000000..27092d2
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/Validator.kt
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.ravenizer
+
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.startsWithAny
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+import org.objectweb.asm.tree.ClassNode
+
+fun validateClasses(classes: ClassNodes): Boolean {
+    var allOk = true
+    classes.forEach { allOk = checkClass(it, classes) && allOk }
+
+    return allOk
+}
+
+/**
+ * Validate a class.
+ *
+ * - A test class shouldn't extend
+ *
+ */
+fun checkClass(cn: ClassNode, classes: ClassNodes): Boolean {
+    if (cn.name.shouldByBypassed()) {
+        // Class doesn't need to be checked.
+        return true
+    }
+    var allOk = true
+
+    // See if there's any class that extends a legacy base class.
+    // But ignore the base classes in android.test.
+    if (!cn.name.startsWithAny("android/test/")) {
+        allOk = checkSuperClass(cn, cn, classes) && allOk
+    }
+    return allOk
+}
+
+fun checkSuperClass(targetClass: ClassNode, currentClass: ClassNode, classes: ClassNodes): Boolean {
+    if (currentClass.superName == null || currentClass.superName == "java/lang/Object") {
+        return true // No parent class
+    }
+    if (currentClass.superName.isLegacyTestBaseClass()) {
+        log.e("Error: Class ${targetClass.name.toHumanReadableClassName()} extends"
+                + " a legacy test class ${currentClass.superName.toHumanReadableClassName()}.")
+        return false
+    }
+    classes.findClass(currentClass.superName)?.let {
+        return checkSuperClass(targetClass, it, classes)
+    }
+    // Super class not found.
+    // log.w("Class ${currentClass.superName} not found.")
+    return true
+}
+
+/**
+ * Check if a class internal name is a known legacy test base class.
+ */
+fun String.isLegacyTestBaseClass(): Boolean {
+    return this.startsWithAny(
+        "junit/framework/TestCase",
+
+        // In case the test doesn't statically include JUnit, we need
+        "android/test/AndroidTestCase",
+        "android/test/InstrumentationTestCase",
+        "android/test/InstrumentationTestSuite",
+    )
+}
diff --git a/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
new file mode 100644
index 0000000..cf6d6f6
--- /dev/null
+++ b/ravenwood/tools/ravenizer/src/com/android/platform/test/ravenwood/ravenizer/adapter/RunnerRewritingAdapter.kt
@@ -0,0 +1,466 @@
+/*
+ * Copyright (C) 2024 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.platform.test.ravenwood.ravenizer.adapter
+
+import android.platform.test.ravenwood.RavenwoodAwareTestRunner
+import com.android.hoststubgen.ClassParseException
+import com.android.hoststubgen.asm.CLASS_INITIALIZER_DESC
+import com.android.hoststubgen.asm.CLASS_INITIALIZER_NAME
+import com.android.hoststubgen.asm.CTOR_NAME
+import com.android.hoststubgen.asm.ClassNodes
+import com.android.hoststubgen.asm.findAnnotationValueAsType
+import com.android.hoststubgen.asm.findAnyAnnotation
+import com.android.hoststubgen.asm.toHumanReadableClassName
+import com.android.hoststubgen.log
+import com.android.hoststubgen.visitors.OPCODE_VERSION
+import com.android.platform.test.ravenwood.ravenizer.RavenizerInternalException
+import com.android.platform.test.ravenwood.ravenizer.classRuleAnotType
+import com.android.platform.test.ravenwood.ravenizer.isTestLookingClass
+import com.android.platform.test.ravenwood.ravenizer.innerRunnerAnotType
+import com.android.platform.test.ravenwood.ravenizer.noRavenizerAnotType
+import com.android.platform.test.ravenwood.ravenizer.ravenwoodTestRunnerType
+import com.android.platform.test.ravenwood.ravenizer.ruleAnotType
+import com.android.platform.test.ravenwood.ravenizer.runWithAnotType
+import com.android.platform.test.ravenwood.ravenizer.testRuleType
+import org.objectweb.asm.AnnotationVisitor
+import org.objectweb.asm.ClassVisitor
+import org.objectweb.asm.FieldVisitor
+import org.objectweb.asm.MethodVisitor
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Opcodes.ACC_FINAL
+import org.objectweb.asm.Opcodes.ACC_PUBLIC
+import org.objectweb.asm.Opcodes.ACC_STATIC
+import org.objectweb.asm.commons.AdviceAdapter
+import org.objectweb.asm.tree.ClassNode
+
+/**
+ * Class visitor to update the RunWith and inject some necessary rules.
+ *
+ * - Change the @RunWith(RavenwoodAwareTestRunner.class).
+ * - If the original class has a @RunWith(...), then change it to an @OrigRunWith(...).
+ * - Add RavenwoodAwareTestRunner's member rules as junit rules.
+ * - Update the order of the existing JUnit rules to make sure they don't use the MIN or MAX.
+ */
+class RunnerRewritingAdapter private constructor(
+    protected val classes: ClassNodes,
+    nextVisitor: ClassVisitor,
+) : ClassVisitor(OPCODE_VERSION, nextVisitor) {
+    /** Arbitrary cut-off point when deciding whether to change the order or an existing rule.*/
+    val RULE_ORDER_TWEAK_CUTOFF = 1973020500
+
+    /** Current class's internal name */
+    lateinit var classInternalName: String
+
+    /** [ClassNode] for the current class */
+    lateinit var classNode: ClassNode
+
+    /** True if this visitor is generating code. */
+    var isGeneratingCode = false
+
+    /** Run a [block] with [isGeneratingCode] set to true. */
+    private inline fun <T> generateCode(block: () -> T): T {
+        isGeneratingCode = true
+        try {
+            return block()
+        } finally {
+            isGeneratingCode = false
+        }
+    }
+
+    override fun visit(
+        version: Int,
+        access: Int,
+        name: String?,
+        signature: String?,
+        superName: String?,
+        interfaces: Array<out String>?,
+        ) {
+        classInternalName = name!!
+        classNode = classes.getClass(name)
+        if (!isTestLookingClass(classes, name)) {
+            throw RavenizerInternalException("This adapter shouldn't be used for non-test class")
+        }
+        super.visit(version, access, name, signature, superName, interfaces)
+
+        generateCode {
+            injectRunWithAnnotation()
+            if (!classes.hasClassInitializer(classInternalName)) {
+                injectStaticInitializer()
+            }
+            injectRules()
+        }
+    }
+
+    /**
+     * Remove the original @RunWith annotation.
+     */
+    override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor? {
+        if (!isGeneratingCode && runWithAnotType.desc == descriptor) {
+            return null
+        }
+        return super.visitAnnotation(descriptor, visible)
+    }
+
+    override fun visitField(
+        access: Int,
+        name: String,
+        descriptor: String,
+        signature: String?,
+        value: Any?
+    ): FieldVisitor {
+        val fallback = super.visitField(access, name, descriptor, signature, value)
+        if (isGeneratingCode) {
+            return fallback
+        }
+        return FieldRuleOrderRewriter(name, fallback)
+    }
+
+    /** Inject an empty <clinit>. The body will be injected by [visitMethod]. */
+    private fun injectStaticInitializer() {
+        visitMethod(
+            Opcodes.ACC_PRIVATE or Opcodes.ACC_STATIC,
+            CLASS_INITIALIZER_NAME,
+            CLASS_INITIALIZER_DESC,
+            null,
+            null
+        )!!.let { mv ->
+            mv.visitCode()
+            mv.visitInsn(Opcodes.RETURN)
+            mv.visitMaxs(0, 0)
+            mv.visitEnd()
+        }
+    }
+
+    /**
+     * Inject `@RunWith(RavenwoodAwareTestRunner.class)`. If the class already has
+     * a `@RunWith`, then change it to add a `@OrigRunWith`.
+     */
+    private fun injectRunWithAnnotation() {
+        // Extract the original RunWith annotation and its value.
+        val runWith = classNode.findAnyAnnotation(runWithAnotType.descAsSet)
+        val runWithClass = runWith?.let { an ->
+            findAnnotationValueAsType(an, "value")
+        }
+
+        if (runWith != null) {
+            if (runWithClass == ravenwoodTestRunnerType.type) {
+                // It already uses RavenwoodTestRunner. We'll just keep it, but we need to
+                // inject it again because the original one is removed by visitAnnotation().
+                log.d("Class ${classInternalName.toHumanReadableClassName()}" +
+                        " already uses RavenwoodTestRunner.")
+                visitAnnotation(runWithAnotType.desc, true)!!.let { av ->
+                    av.visit("value", ravenwoodTestRunnerType)
+                    av.visitEnd()
+                }
+                return
+            }
+            if (runWithClass == null) {
+                throw ClassParseException("@RunWith annotation doesn't have a property \"value\""
+                        + " in class ${classInternalName.toHumanReadableClassName()}")
+            }
+
+            // Inject an @OrigRunWith.
+            visitAnnotation(innerRunnerAnotType.desc, true)!!.let { av ->
+                av.visit("value", runWithClass)
+                av.visitEnd()
+            }
+        }
+
+        // Inject a @RunWith(RavenwoodAwareTestRunner.class).
+        visitAnnotation(runWithAnotType.desc, true)!!.let { av ->
+            av.visit("value", ravenwoodTestRunnerType.type)
+            av.visitEnd()
+        }
+        log.v("Update the @RunWith: ${classInternalName.toHumanReadableClassName()}")
+    }
+
+    /*
+     Generate the fields and the ctor, which should looks like  this:
+
+  public static final org.junit.rules.TestRule sRavenwoodImplicitClassMinRule;
+    descriptor: Lorg/junit/rules/TestRule;
+    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
+    RuntimeVisibleAnnotations:
+      0: #49(#50=I#51)
+        org.junit.ClassRule(
+          order=-2147483648
+        )
+
+  public static final org.junit.rules.TestRule sRavenwoodImplicitClassMaxRule;
+    descriptor: Lorg/junit/rules/TestRule;
+    flags: (0x0019) ACC_PUBLIC, ACC_STATIC, ACC_FINAL
+    RuntimeVisibleAnnotations:
+      0: #49(#50=I#52)
+        org.junit.ClassRule(
+          order=2147483647
+        )
+
+  public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMinRule;
+    descriptor: Lorg/junit/rules/TestRule;
+    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
+    RuntimeVisibleAnnotations:
+      0: #53(#50=I#51)
+        org.junit.Rule(
+          order=-2147483648
+        )
+
+  public final org.junit.rules.TestRule sRavenwoodImplicitInstanceMaxRule;
+    descriptor: Lorg/junit/rules/TestRule;
+    flags: (0x0011) ACC_PUBLIC, ACC_FINAL
+    RuntimeVisibleAnnotations:
+      0: #53(#50=I#52)
+        org.junit.Rule(
+          order=2147483647
+        )
+     */
+
+    val sRavenwood_ClassRuleMin = "sRavenwood_ClassRuleMin"
+    val sRavenwood_ClassRuleMax = "sRavenwood_ClassRuleMax"
+    val mRavenwood_InstRuleMin = "mRavenwood_InstRuleMin"
+    val mRavenwood_InstRuleMax = "mRavenwood_InstRuleMax"
+
+    private fun injectRules() {
+        injectRule(sRavenwood_ClassRuleMin, true, Integer.MIN_VALUE)
+        injectRule(sRavenwood_ClassRuleMax, true, Integer.MAX_VALUE)
+        injectRule(mRavenwood_InstRuleMin, false, Integer.MIN_VALUE)
+        injectRule(mRavenwood_InstRuleMax, false, Integer.MAX_VALUE)
+    }
+
+    private fun injectRule(fieldName: String, isStatic: Boolean, order: Int) {
+        visitField(
+            ACC_PUBLIC or ACC_FINAL or (if (isStatic) ACC_STATIC else 0),
+            fieldName,
+            testRuleType.desc,
+            null,
+            null,
+        ).let { fv ->
+            val anot = if (isStatic) { classRuleAnotType } else { ruleAnotType }
+            fv.visitAnnotation(anot.desc, true).let {
+                it.visit("order", order)
+                it.visitEnd()
+            }
+            fv.visitEnd()
+        }
+    }
+
+    override fun visitMethod(
+        access: Int,
+        name: String,
+        descriptor: String,
+        signature: String?,
+        exceptions: Array<String>?,
+    ): MethodVisitor {
+        val next = super.visitMethod(access, name, descriptor, signature, exceptions)
+        if (name == CLASS_INITIALIZER_NAME && descriptor == CLASS_INITIALIZER_DESC) {
+            return ClassInitializerVisitor(
+                access, name, descriptor, signature, exceptions, next)
+        }
+        if (name == CTOR_NAME) {
+            return ConstructorVisitor(
+                access, name, descriptor, signature, exceptions, next)
+        }
+        return next
+    }
+
+    /*
+
+  static {};
+    descriptor: ()V
+    flags: (0x0008) ACC_STATIC
+    Code:
+      stack=1, locals=0, args_size=0
+         0: getstatic     #36                 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule;
+         3: putstatic     #39                 // Field sRavenwoodImplicitClassMinRule:Lorg/junit/rules/TestRule;
+         6: getstatic     #42                 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule;
+         9: putstatic     #45                 // Field sRavenwoodImplicitClassMaxRule:Lorg/junit/rules/TestRule;
+        12: return
+      LineNumberTable:
+        line 33: 0
+        line 36: 6
+     */
+    private inner class ClassInitializerVisitor(
+        access: Int,
+        val name: String,
+        val descriptor: String,
+        signature: String?,
+        exceptions: Array<String>?,
+        next: MethodVisitor?,
+    ) : MethodVisitor(OPCODE_VERSION, next) {
+        override fun visitCode() {
+            visitFieldInsn(Opcodes.GETSTATIC,
+                ravenwoodTestRunnerType.internlName,
+                RavenwoodAwareTestRunner.IMPLICIT_CLASS_OUTER_RULE_NAME,
+                testRuleType.desc
+            )
+            visitFieldInsn(Opcodes.PUTSTATIC,
+                classInternalName,
+                sRavenwood_ClassRuleMin,
+                testRuleType.desc
+            )
+
+            visitFieldInsn(Opcodes.GETSTATIC,
+                ravenwoodTestRunnerType.internlName,
+                RavenwoodAwareTestRunner.IMPLICIT_CLASS_INNER_RULE_NAME,
+                testRuleType.desc
+            )
+            visitFieldInsn(Opcodes.PUTSTATIC,
+                classInternalName,
+                sRavenwood_ClassRuleMax,
+                testRuleType.desc
+            )
+
+            super.visitCode()
+        }
+    }
+
+    /*
+  public com.android.ravenwoodtest.bivalenttest.runnertest.RavenwoodRunnerTest();
+    descriptor: ()V
+    flags: (0x0001) ACC_PUBLIC
+    Code:
+      stack=2, locals=1, args_size=1
+         0: aload_0
+         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
+         4: aload_0
+         5: getstatic     #7                  // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule;
+         8: putfield      #13                 // Field sRavenwoodImplicitInstanceMinRule:Lorg/junit/rules/TestRule;
+        11: aload_0
+        12: getstatic     #18                 // Field android/platform/test/ravenwood/RavenwoodAwareTestRunner.RavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule;
+        15: putfield      #21                 // Field sRavenwoodImplicitInstanceMaxRule:Lorg/junit/rules/TestRule;
+        18: return
+      LineNumberTable:
+        line 31: 0
+        line 38: 4
+        line 41: 11
+      LocalVariableTable:
+        Start  Length  Slot  Name   Signature
+            0      19     0  this   Lcom/android/ravenwoodtest/bivalenttest/runnertest/RavenwoodRunnerTest;
+     */
+    private inner class ConstructorVisitor(
+        access: Int,
+        name: String,
+        descriptor: String,
+        signature: String?,
+        exceptions: Array<String>?,
+        next: MethodVisitor?,
+    ) : AdviceAdapter(OPCODE_VERSION, next, ACC_ENUM, name, descriptor) {
+        override fun onMethodEnter() {
+            visitVarInsn(ALOAD, 0)
+            visitFieldInsn(Opcodes.GETSTATIC,
+                ravenwoodTestRunnerType.internlName,
+                RavenwoodAwareTestRunner.IMPLICIT_INST_OUTER_RULE_NAME,
+                testRuleType.desc
+            )
+            visitFieldInsn(Opcodes.PUTFIELD,
+                classInternalName,
+                mRavenwood_InstRuleMin,
+                testRuleType.desc
+            )
+
+            visitVarInsn(ALOAD, 0)
+            visitFieldInsn(Opcodes.GETSTATIC,
+                ravenwoodTestRunnerType.internlName,
+                RavenwoodAwareTestRunner.IMPLICIT_INST_INNER_RULE_NAME,
+                testRuleType.desc
+            )
+            visitFieldInsn(Opcodes.PUTFIELD,
+                classInternalName,
+                mRavenwood_InstRuleMax,
+                testRuleType.desc
+            )
+        }
+    }
+
+    /**
+     * Rewrite "order" of the existing junit rules to make sure no rules use a MAX or MIN order.
+     *
+     * Currently, we do it a hacky way -- use an arbitrary cut-off point, and if the order
+     * is larger than that, decrement by 1, and if it's smaller than the negative cut-off point,
+     * increment it by 1.
+     *
+     * (or the arbitrary number is already used.... then we're unlucky, let's change the cut-off
+     * point.)
+     */
+    private inner class FieldRuleOrderRewriter(
+        val fieldName: String,
+        next: FieldVisitor,
+    ) : FieldVisitor(OPCODE_VERSION, next) {
+        override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor {
+            val fallback = super.visitAnnotation(descriptor, visible)
+            if (descriptor != ruleAnotType.desc && descriptor != classRuleAnotType.desc) {
+                return fallback
+            }
+            return RuleOrderRewriter(fallback)
+        }
+
+        private inner class RuleOrderRewriter(
+            next: AnnotationVisitor,
+        ) : AnnotationVisitor(OPCODE_VERSION, next) {
+            override fun visit(name: String?, origValue: Any?) {
+                if (name != "order") {
+                    return super.visit(name, origValue)
+                }
+                var order = origValue as Int
+                if (order == RULE_ORDER_TWEAK_CUTOFF || order == -RULE_ORDER_TWEAK_CUTOFF) {
+                    // Oops. If this happens, we'll need to change RULE_ORDER_TWEAK_CUTOFF.
+                    // Or, we could scan all the rules in the target jar and find an unused number.
+                    // Because rules propagate to subclasses, we'll at least check all the
+                    // super classes of the current class.
+                    throw RavenizerInternalException(
+                        "OOPS: Field $classInternalName.$fieldName uses $order."
+                                + " We can't update it.")
+                }
+                if (order > RULE_ORDER_TWEAK_CUTOFF) {
+                    order -= 1
+                }
+                if (order < -RULE_ORDER_TWEAK_CUTOFF) {
+                    order += 1
+                }
+                super.visit(name, order)
+            }
+        }
+    }
+
+    companion object {
+        fun shouldProcess(classes: ClassNodes, className: String): Boolean {
+            if (!isTestLookingClass(classes, className)) {
+                return false
+            }
+            // Don't process a class if it has a @NoRavenizer annotation.
+            classes.findClass(className)?.let { cn ->
+                if (cn.findAnyAnnotation(noRavenizerAnotType.descAsSet) != null) {
+                    log.i("Class ${className.toHumanReadableClassName()} has" +
+                        " @${noRavenizerAnotType.humanReadableName}. Skipping."
+                    )
+                    return false
+                }
+            }
+            return true
+        }
+
+        fun maybeApply(
+            className: String,
+            classes: ClassNodes,
+            nextVisitor: ClassVisitor,
+        ): ClassVisitor {
+            if (!shouldProcess(classes, className)) {
+                return nextVisitor
+            } else {
+                return RunnerRewritingAdapter(classes, nextVisitor)
+            }
+        }
+    }
+}
\ No newline at end of file