First pass of "real" services on Ravenwood.

One of our eventual goals with Ravenwood is to support usage of
system services from test code.  Robolectric takes the approach of
publishing "shadows" which are effectively fakes of the Manager
objects visible to app processes, and it unfortunately doesn't offer
a mechanism to run "real" services code.

In contrast, Ravenwood aims to support API owners progressively
offering their system services either via a "fake" approach, or by
using various levels of the "real" code that would run on a device.

This change wires up the foundational support and uses the simple
`SerialManager` example to demonstrate using the same "real" code
on both Ravenwood and devices.  It also demonstrates the `Internal`
pattern being used to customize behavior for tests.

To offer as hermetic as a test environment as possible, we start
new instances of each requested service for each test.  Requiring
developers to be explicit about the services they need will help
keep overhead low, especially for tests that don't need services.

Bug: 325506297
Test: atest RavenwoodServicesTest
Change-Id: Ie22436b38f2176f91dfce746b899ebab7752bbb8
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 6337022..2df6d58 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -78,6 +78,73 @@
 }
 
 java_library {
+    name: "services.core-for-hoststubgen",
+    installable: false, // host only jar.
+    static_libs: [
+        "services.core",
+    ],
+    sdk_version: "core_platform",
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "services.core.ravenwood-base",
+    tools: ["hoststubgen"],
+    cmd: "$(location hoststubgen) " +
+        "@$(location ravenwood/ravenwood-standard-options.txt) " +
+
+        "--debug-log $(location hoststubgen_services.core.log) " +
+        "--stats-file $(location hoststubgen_services.core_stats.csv) " +
+
+        "--out-impl-jar $(location ravenwood.jar) " +
+
+        "--gen-keep-all-file $(location hoststubgen_keep_all.txt) " +
+        "--gen-input-dump-file $(location hoststubgen_dump.txt) " +
+
+        "--in-jar $(location :services.core-for-hoststubgen) " +
+        "--policy-override-file $(location ravenwood/services.core-ravenwood-policies.txt) " +
+        "--annotation-allowed-classes-file $(location ravenwood/ravenwood-annotation-allowed-classes.txt) ",
+    srcs: [
+        ":services.core-for-hoststubgen",
+        "ravenwood/services.core-ravenwood-policies.txt",
+        "ravenwood/ravenwood-standard-options.txt",
+        "ravenwood/ravenwood-annotation-allowed-classes.txt",
+    ],
+    out: [
+        "ravenwood.jar",
+
+        // Following files are created just as FYI.
+        "hoststubgen_keep_all.txt",
+        "hoststubgen_dump.txt",
+
+        "hoststubgen_services.core.log",
+        "hoststubgen_services.core_stats.csv",
+    ],
+    visibility: ["//visibility:private"],
+}
+
+java_genrule {
+    name: "services.core.ravenwood",
+    defaults: ["ravenwood-internal-only-visibility-genrule"],
+    cmd: "cp $(in) $(out)",
+    srcs: [
+        ":services.core.ravenwood-base{ravenwood.jar}",
+    ],
+    out: [
+        "services.core.ravenwood.jar",
+    ],
+}
+
+java_library {
+    name: "services.core.ravenwood-jarjar",
+    installable: false,
+    static_libs: [
+        "services.core.ravenwood",
+    ],
+    jarjar_rules: ":ravenwood-services-jarjar-rules",
+}
+
+java_library {
     name: "mockito-ravenwood-prebuilt",
     installable: false,
     static_libs: [
@@ -121,6 +188,7 @@
         "android.test.mock.ravenwood",
         "ravenwood-helper-runtime",
         "hoststubgen-helper-runtime.ravenwood",
+        "services.core.ravenwood-jarjar",
 
         // Provide runtime versions of utils linked in below
         "junit",
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index be7199b..db216b1 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -145,7 +145,7 @@
      * reflection, but it will serve as noticeable discouragement from
      * doing such a thing.
      */
-    @android.ravenwood.annotation.RavenwoodReplace
+    @android.ravenwood.annotation.RavenwoodKeep
     private void checkInstrumenting(String method) {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -155,16 +155,12 @@
         }
     }
 
-    private void checkInstrumenting$ravenwood(String method) {
-        // At the moment, Ravenwood doesn't attach a Context, but we're only ever
-        // running code as part of tests, so we continue quietly
-    }
-
     /**
      * Returns if it is being called in an instrumentation environment.
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean isInstrumenting() {
         // Check if we have an instrumentation context, as init should only get called by
         // the system in startup processes that are being instrumented.
@@ -328,6 +324,7 @@
      * 
      * @see #getTargetContext
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public Context getContext() {
         return mInstrContext;
     }
@@ -352,6 +349,7 @@
      * 
      * @see #getContext
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public Context getTargetContext() {
         return mAppContext;
     }
@@ -2402,6 +2400,17 @@
         mThread = thread;
     }
 
+    /**
+     * Only sets the Context up, keeps everything else null.
+     *
+     * @hide
+     */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public final void basicInit(Context context) {
+        mInstrContext = context;
+        mAppContext = context;
+    }
+
     /** @hide */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
     public static void checkStartActivityResult(int res, Object intent) {
diff --git a/core/java/android/hardware/SerialManager.java b/core/java/android/hardware/SerialManager.java
index 26e5129..e2ce723 100644
--- a/core/java/android/hardware/SerialManager.java
+++ b/core/java/android/hardware/SerialManager.java
@@ -29,6 +29,7 @@
  * @hide
  */
 @SystemService(Context.SERIAL_SERVICE)
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class SerialManager {
     private static final String TAG = "SerialManager";
 
@@ -69,6 +70,8 @@
      * @return the serial port
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = ParcelFileDescriptor.class, reason =
+            "Needs socketpair() to offer accurate emulation")
     public SerialPort openSerialPort(String name, int speed) throws IOException {
         try {
             ParcelFileDescriptor pfd = mService.openSerialPort(name);
diff --git a/core/java/android/hardware/SerialManagerInternal.java b/core/java/android/hardware/SerialManagerInternal.java
new file mode 100644
index 0000000..9132da0
--- /dev/null
+++ b/core/java/android/hardware/SerialManagerInternal.java
@@ -0,0 +1,35 @@
+/*
+ * 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.hardware;
+
+import android.annotation.NonNull;
+import android.os.ParcelFileDescriptor;
+
+import java.util.function.Supplier;
+
+/**
+ * Internal interactions with {@link SerialManager}.
+ *
+ * @hide
+ */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
+public abstract class SerialManagerInternal {
+    public abstract void addVirtualSerialPortForTest(@NonNull String name,
+            @NonNull Supplier<ParcelFileDescriptor> supplier);
+
+    public abstract void removeVirtualSerialPortForTest(@NonNull String name);
+}
diff --git a/core/java/android/os/PermissionEnforcer.java b/core/java/android/os/PermissionEnforcer.java
index 91d2269..3cc6fb5a 100644
--- a/core/java/android/os/PermissionEnforcer.java
+++ b/core/java/android/os/PermissionEnforcer.java
@@ -24,6 +24,7 @@
 import android.content.PermissionChecker;
 import android.content.pm.PackageManager;
 import android.permission.PermissionCheckerManager;
+import android.permission.PermissionManager;
 
 /**
  * PermissionEnforcer check permissions for AIDL-generated services which use
@@ -71,6 +72,7 @@
  * @hide
  */
 @SystemService(Context.PERMISSION_ENFORCER_SERVICE)
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class PermissionEnforcer {
 
     private final Context mContext;
@@ -84,6 +86,8 @@
     }
 
     /** Constructor, prefer using the fromContext static method when possible */
+    @android.ravenwood.annotation.RavenwoodThrow(blockedBy = PermissionManager.class,
+            reason = "Use subclass for unit tests, such as FakePermissionEnforcer")
     public PermissionEnforcer(@NonNull Context context) {
         mContext = context;
     }
@@ -103,9 +107,19 @@
         return PermissionCheckerManager.PERMISSION_HARD_DENIED;
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace(blockedBy = AppOpsManager.class,
+            reason = "Blocked on Mainline dependencies")
+    private static int permissionToOpCode(String permission) {
+        return AppOpsManager.permissionToOpCode(permission);
+    }
+
+    private static int permissionToOpCode$ravenwood(String permission) {
+        return AppOpsManager.OP_NONE;
+    }
+
     private boolean anyAppOps(@NonNull String[] permissions) {
         for (String permission : permissions) {
-            if (AppOpsManager.permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
+            if (permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
                 return true;
             }
         }
@@ -122,7 +136,7 @@
 
     public void enforcePermission(@NonNull String permission, int pid, int uid)
             throws SecurityException {
-        if (AppOpsManager.permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
+        if (permissionToOpCode(permission) != AppOpsManager.OP_NONE) {
             AttributionSource source = new AttributionSource(uid, null, null);
             enforcePermission(permission, source);
             return;
diff --git a/core/java/android/os/ServiceManager.java b/core/java/android/os/ServiceManager.java
index e96c24d..0be2d3e3 100644
--- a/core/java/android/os/ServiceManager.java
+++ b/core/java/android/os/ServiceManager.java
@@ -25,6 +25,7 @@
 
 import com.android.internal.annotations.GuardedBy;
 import com.android.internal.os.BinderInternal;
+import com.android.internal.util.Preconditions;
 import com.android.internal.util.StatLogger;
 
 import java.util.Map;
@@ -38,6 +39,7 @@
  * @hide
  **/
 @SystemApi(client = SystemApi.Client.MODULE_LIBRARIES)
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
 public final class ServiceManager {
     private static final String TAG = "ServiceManager";
     private static final Object sLock = new Object();
@@ -48,9 +50,16 @@
     /**
      * Cache for the "well known" services, such as WM and AM.
      */
+    // NOTE: this cache is designed to be populated exactly once at process
+    // start to avoid any overhead from locking
     @UnsupportedAppUsage
     private static Map<String, IBinder> sCache = new ArrayMap<String, IBinder>();
 
+    @GuardedBy("ServiceManager.class")
+    // NOTE: this cache is designed to support mutation by tests, so we require
+    // a lock to be held for all accesses
+    private static Map<String, IBinder> sCache$ravenwood;
+
     /**
      * We do the "slow log" at most once every this interval.
      */
@@ -115,9 +124,27 @@
 
     /** @hide */
     @UnsupportedAppUsage
+    @android.ravenwood.annotation.RavenwoodKeep
     public ServiceManager() {
     }
 
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void init$ravenwood() {
+        synchronized (ServiceManager.class) {
+            sCache$ravenwood = new ArrayMap<>();
+        }
+    }
+
+    /** @hide */
+    @android.ravenwood.annotation.RavenwoodKeep
+    public static void reset$ravenwood() {
+        synchronized (ServiceManager.class) {
+            sCache$ravenwood.clear();
+            sCache$ravenwood = null;
+        }
+    }
+
     @UnsupportedAppUsage
     private static IServiceManager getIServiceManager() {
         if (sServiceManager != null) {
@@ -138,6 +165,7 @@
      * @hide
      */
     @UnsupportedAppUsage
+    @android.ravenwood.annotation.RavenwoodReplace
     public static IBinder getService(String name) {
         try {
             IBinder service = sCache.get(name);
@@ -152,12 +180,21 @@
         return null;
     }
 
+    /** @hide */
+    public static IBinder getService$ravenwood(String name) {
+        synchronized (ServiceManager.class) {
+            // Ravenwood is a single-process environment, so it only needs to store locally
+            return Preconditions.requireNonNullViaRavenwoodRule(sCache$ravenwood).get(name);
+        }
+    }
+
     /**
      * Returns a reference to a service with the given name, or throws
      * {@link ServiceNotFoundException} if none is found.
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public static IBinder getServiceOrThrow(String name) throws ServiceNotFoundException {
         final IBinder binder = getService(name);
         if (binder != null) {
@@ -176,6 +213,7 @@
      * @hide
      */
     @UnsupportedAppUsage
+    @android.ravenwood.annotation.RavenwoodKeep
     public static void addService(String name, IBinder service) {
         addService(name, service, false, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
@@ -191,6 +229,7 @@
      * @hide
      */
     @UnsupportedAppUsage
+    @android.ravenwood.annotation.RavenwoodKeep
     public static void addService(String name, IBinder service, boolean allowIsolated) {
         addService(name, service, allowIsolated, IServiceManager.DUMP_FLAG_PRIORITY_DEFAULT);
     }
@@ -207,6 +246,7 @@
      * @hide
      */
     @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
+    @android.ravenwood.annotation.RavenwoodReplace
     public static void addService(String name, IBinder service, boolean allowIsolated,
             int dumpPriority) {
         try {
@@ -216,6 +256,15 @@
         }
     }
 
+    /** @hide */
+    public static void addService$ravenwood(String name, IBinder service, boolean allowIsolated,
+            int dumpPriority) {
+        synchronized (ServiceManager.class) {
+            // Ravenwood is a single-process environment, so it only needs to store locally
+            Preconditions.requireNonNullViaRavenwoodRule(sCache$ravenwood).put(name, service);
+        }
+    }
+
     /**
      * Retrieve an existing service called @a name from the
      * service manager.  Non-blocking.
@@ -366,6 +415,7 @@
      *
      * @hide
      */
+    @android.ravenwood.annotation.RavenwoodKeepWholeClass
     public static class ServiceNotFoundException extends Exception {
         public ServiceNotFoundException(String name) {
             super("No service published for: " + name);
diff --git a/core/java/android/util/TimingsTraceLog.java b/core/java/android/util/TimingsTraceLog.java
index 48a5cea..b4f4729 100644
--- a/core/java/android/util/TimingsTraceLog.java
+++ b/core/java/android/util/TimingsTraceLog.java
@@ -34,6 +34,7 @@
  *
  * @hide
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class TimingsTraceLog {
     // Debug boot time for every step if it's non-user build.
     private static final boolean DEBUG_BOOT_TIME = !Build.IS_USER;
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 861f719..37e6780 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -211,6 +211,7 @@
     ],
     srcs: [
         "src/android/app/ActivityManagerTest.java",
+        "src/android/content/ContextTest.java",
         "src/android/content/pm/PackageManagerTest.java",
         "src/android/content/pm/UserInfoTest.java",
         "src/android/database/CursorWindowTest.java",
diff --git a/core/tests/coretests/src/android/content/ContextTest.java b/core/tests/coretests/src/android/content/ContextTest.java
index d478437..a02af78 100644
--- a/core/tests/coretests/src/android/content/ContextTest.java
+++ b/core/tests/coretests/src/android/content/ContextTest.java
@@ -26,6 +26,7 @@
 
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertTrue;
 
 import android.app.ActivityThread;
@@ -35,7 +36,9 @@
 import android.hardware.display.VirtualDisplay;
 import android.media.ImageReader;
 import android.os.UserHandle;
+import android.platform.test.annotations.DisabledOnRavenwood;
 import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
 import android.view.Display;
 
 import androidx.test.core.app.ApplicationProvider;
@@ -43,6 +46,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.platform.app.InstrumentationRegistry;
 
+import org.junit.Rule;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -54,7 +58,23 @@
 @SmallTest
 @RunWith(AndroidJUnit4.class)
 public class ContextTest {
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder().build();
+
     @Test
+    public void testInstrumentationContext() {
+        // Confirm that we have a valid Context
+        assertNotNull(InstrumentationRegistry.getInstrumentation().getContext());
+    }
+
+    @Test
+    public void testInstrumentationTargetContext() {
+        // Confirm that we have a valid Context
+        assertNotNull(InstrumentationRegistry.getInstrumentation().getTargetContext());
+    }
+
+    @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDisplayIdForSystemContext() {
         final Context systemContext =
                 ActivityThread.currentActivityThread().getSystemContext();
@@ -63,6 +83,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDisplayIdForSystemUiContext() {
         final Context systemUiContext =
                 ActivityThread.currentActivityThread().getSystemUiContext();
@@ -71,6 +92,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDisplayIdForTestContext() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -79,6 +101,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDisplayIdForDefaultDisplayContext() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -91,6 +114,7 @@
     }
 
     @Test(expected = NullPointerException.class)
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testStartActivityAsUserNullIntentNullUser() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -98,6 +122,7 @@
     }
 
     @Test(expected = NullPointerException.class)
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testStartActivityAsUserNullIntentNonNullUser() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -105,6 +130,7 @@
     }
 
     @Test(expected = NullPointerException.class)
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testStartActivityAsUserNonNullIntentNullUser() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -112,6 +138,7 @@
     }
 
     @Test(expected = RuntimeException.class)
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testStartActivityAsUserNonNullIntentNonNullUser() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
@@ -119,6 +146,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_appContext_returnsFalse() {
         final Context appContext = ApplicationProvider.getApplicationContext();
 
@@ -126,6 +154,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_systemContext_returnsTrue() {
         final Context systemContext =
                 ActivityThread.currentActivityThread().getSystemContext();
@@ -134,6 +163,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_systemUiContext_returnsTrue() {
         final Context systemUiContext =
                 ActivityThread.currentActivityThread().getSystemUiContext();
@@ -142,11 +172,13 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testGetDisplayFromDisplayContextDerivedContextOnPrimaryDisplay() {
         verifyGetDisplayFromDisplayContextDerivedContext(false /* onSecondaryDisplay */);
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testGetDisplayFromDisplayContextDerivedContextOnSecondaryDisplay() {
         verifyGetDisplayFromDisplayContextDerivedContext(true /* onSecondaryDisplay */);
     }
@@ -179,6 +211,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_ContextWrapper() {
         ContextWrapper wrapper = new ContextWrapper(null /* base */);
 
@@ -190,6 +223,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_UiContextDerivedContext() {
         final Context uiContext = createUiContext();
         Context context = uiContext.createAttributionContext(null /* attributionTag */);
@@ -202,6 +236,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testIsUiContext_UiContextDerivedDisplayContext() {
         final Context uiContext = createUiContext();
         final Display secondaryDisplay =
@@ -212,6 +247,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDeviceIdForSystemContext() {
         final Context systemContext =
                 ActivityThread.currentActivityThread().getSystemContext();
@@ -220,6 +256,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDeviceIdForSystemUiContext() {
         final Context systemUiContext =
                 ActivityThread.currentActivityThread().getSystemUiContext();
@@ -228,6 +265,7 @@
     }
 
     @Test
+    @DisabledOnRavenwood(blockedBy = Context.class)
     public void testDeviceIdForTestContext() {
         final Context testContext =
                 InstrumentationRegistry.getInstrumentation().getTargetContext();
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 132804f..53897e1 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -74,11 +74,14 @@
         "androidx.test.monitor-for-device",
     ],
     libs: [
+        "android.test.mock",
         "framework-minus-apex.ravenwood",
+        "services.core.ravenwood",
         "junit",
     ],
     sdk_version: "core_current",
     visibility: ["//frameworks/base"],
+    jarjar_rules: ":ravenwood-services-jarjar-rules",
 }
 
 // Carefully compiles against only test_current to support tests that
@@ -111,3 +114,9 @@
         "androidx.test.monitor",
     ],
 }
+
+filegroup {
+    name: "ravenwood-services-jarjar-rules",
+    srcs: ["ravenwood-services-jarjar-rules.txt"],
+    visibility: ["//frameworks/base"],
+}
diff --git a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
index a920f63..83a7b6e 100644
--- a/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
+++ b/ravenwood/annotations-src/android/ravenwood/annotation/RavenwoodReplace.java
@@ -32,4 +32,14 @@
 @Target({METHOD})
 @Retention(RetentionPolicy.CLASS)
 public @interface RavenwoodReplace {
+    /**
+     * One or more classes that aren't yet supported by Ravenwood, which is why this method is
+     * being replaced.
+     */
+    Class<?>[] blockedBy() default {};
+
+    /**
+     * General free-form description of why this method is being replaced.
+     */
+    String reason() default "";
 }
diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt
index 49cef07..6b67364 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -52,5 +52,6 @@
     method <init> ()V stub
 class android.content.Context stub
     method <init> ()V stub
+    method getSystemService (Ljava/lang/Class;)Ljava/lang/Object; stub
 class android.content.pm.PackageManager stub
     method <init> ()V stub
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
new file mode 100644
index 0000000..3668b03
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -0,0 +1,90 @@
+/*
+ * 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.content.Context;
+import android.hardware.ISerialManager;
+import android.hardware.SerialManager;
+import android.os.PermissionEnforcer;
+import android.os.ServiceManager;
+import android.test.mock.MockContext;
+import android.util.ArrayMap;
+import android.util.Singleton;
+
+import java.util.function.Supplier;
+
+public class RavenwoodContext extends MockContext {
+    private final RavenwoodPermissionEnforcer mEnforcer = new RavenwoodPermissionEnforcer();
+
+    private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>();
+    private final ArrayMap<String, Supplier<?>> mNameToFactory = new ArrayMap<>();
+
+    private void registerService(Class<?> serviceClass, String serviceName,
+            Supplier<?> serviceSupplier) {
+        mClassToName.put(serviceClass, serviceName);
+        mNameToFactory.put(serviceName, serviceSupplier);
+    }
+
+    public RavenwoodContext() {
+        registerService(PermissionEnforcer.class,
+                Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer);
+        registerService(SerialManager.class,
+                Context.SERIAL_SERVICE, asSingleton(() ->
+                        new SerialManager(this, ISerialManager.Stub.asInterface(
+                                ServiceManager.getService(Context.SERIAL_SERVICE)))
+                ));
+    }
+
+    @Override
+    public Object getSystemService(String serviceName) {
+        // TODO: pivot to using SystemServiceRegistry
+        final Supplier<?> serviceSupplier = mNameToFactory.get(serviceName);
+        if (serviceSupplier != null) {
+            return serviceSupplier.get();
+        } else {
+            throw new UnsupportedOperationException(
+                    "Service " + serviceName + " not yet supported under Ravenwood");
+        }
+    }
+
+    @Override
+    public String getSystemServiceName(Class<?> serviceClass) {
+        // TODO: pivot to using SystemServiceRegistry
+        final String serviceName = mClassToName.get(serviceClass);
+        if (serviceName != null) {
+            return serviceName;
+        } else {
+            throw new UnsupportedOperationException(
+                    "Service " + serviceClass + " not yet supported under Ravenwood");
+        }
+    }
+
+    /**
+     * Wrap the given {@link Supplier} to become a memoized singleton.
+     */
+    private static <T> Supplier<T> asSingleton(Supplier<T> supplier) {
+        final Singleton<T> singleton = new Singleton<>() {
+            @Override
+            protected T create() {
+                return supplier.get();
+            }
+        };
+        return () -> {
+            return singleton.get();
+        };
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java
new file mode 100644
index 0000000..4244135
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodPermissionEnforcer.java
@@ -0,0 +1,38 @@
+/*
+ * 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.permission.PermissionManager.PERMISSION_GRANTED;
+
+import android.content.AttributionSource;
+import android.os.PermissionEnforcer;
+
+public class RavenwoodPermissionEnforcer extends PermissionEnforcer {
+    @Override
+    protected int checkPermission(String permission, AttributionSource source) {
+        // For the moment, since Ravenwood doesn't offer cross-process capabilities, assume all
+        // permissions are granted during tests
+        return PERMISSION_GRANTED;
+    }
+
+    @Override
+    protected int checkPermission(String permission, int pid, int uid) {
+        // For the moment, since Ravenwood doesn't offer cross-process capabilities, assume all
+        // permissions are granted during tests
+        return PERMISSION_GRANTED;
+    }
+}
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
index 1d5c79c..231cce9 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java
@@ -24,11 +24,13 @@
 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.Assert;
@@ -103,9 +105,10 @@
                 rule.mSystemProperties.getKeyReadablePredicate(),
                 rule.mSystemProperties.getKeyWritablePredicate());
 
-        ActivityManager.init$ravenwood(rule.mCurrentUser);
+        ServiceManager.init$ravenwood();
+        LocalServices.removeAllServicesForTest();
 
-        com.android.server.LocalServices.removeAllServicesForTest();
+        ActivityManager.init$ravenwood(rule.mCurrentUser);
 
         if (rule.mProvideMainThread) {
             final HandlerThread main = new HandlerThread(MAIN_THREAD_NAME);
@@ -113,7 +116,12 @@
             Looper.setMainLooperForTest(main.getLooper());
         }
 
-        InstrumentationRegistry.registerInstance(new Instrumentation(), Bundle.EMPTY);
+        rule.mContext = new RavenwoodContext();
+        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,
@@ -121,7 +129,7 @@
         }
 
         // Touch some references early to ensure they're <clinit>'ed
-        Objects.requireNonNull(Build.IS_USERDEBUG);
+        Objects.requireNonNull(Build.TYPE);
         Objects.requireNonNull(Build.VERSION.SDK);
     }
 
@@ -130,17 +138,22 @@
             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();
         }
 
-        com.android.server.LocalServices.removeAllServicesForTest();
-
         ActivityManager.reset$ravenwood();
 
+        LocalServices.removeAllServicesForTest();
+        ServiceManager.reset$ravenwood();
+
         android.os.SystemProperties.reset$ravenwood();
         android.os.Binder.reset$ravenwood();
         android.os.Process.reset$ravenwood();
diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
new file mode 100644
index 0000000..bb280f4
--- /dev/null
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -0,0 +1,85 @@
+/*
+ * 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.hardware.SerialManager;
+import android.os.SystemClock;
+import android.util.ArrayMap;
+
+import com.android.server.LocalServices;
+import com.android.server.SystemService;
+import com.android.server.SystemServiceManager;
+import com.android.server.utils.TimingsTraceAndSlog;
+
+public class RavenwoodSystemServer {
+    /**
+     * Set of services that we know how to provide under Ravenwood. We keep this set distinct
+     * from {@code com.android.server.SystemServer} to give us the ability to choose either
+     * "real" or "fake" implementations based on the commitments of the service owner.
+     *
+     * Map from {@code FooManager.class} to the {@code com.android.server.SystemService}
+     * lifecycle class name used to instantiate and drive that service.
+     */
+    private static final ArrayMap<Class<?>, String> sKnownServices = new ArrayMap<>();
+
+    // TODO: expand SystemService API to support dependency expression, so we don't need test
+    // authors to exhaustively declare all transitive services
+
+    static {
+        sKnownServices.put(SerialManager.class, "com.android.server.SerialService$Lifecycle");
+    }
+
+    private static TimingsTraceAndSlog sTimings;
+    private static SystemServiceManager sServiceManager;
+
+    public static void init(RavenwoodRule rule) {
+        // Avoid overhead if no services required
+        if (rule.mServicesRequired.isEmpty()) return;
+
+        sTimings = new TimingsTraceAndSlog();
+        sServiceManager = new SystemServiceManager(rule.mContext);
+        sServiceManager.setStartInfo(false,
+                SystemClock.elapsedRealtime(),
+                SystemClock.uptimeMillis());
+        LocalServices.addService(SystemServiceManager.class, sServiceManager);
+
+        for (Class<?> service : rule.mServicesRequired) {
+            final String target = sKnownServices.get(service);
+            if (target == null) {
+                throw new RuntimeException("The requested service " + service
+                        + " is not yet supported under the Ravenwood deviceless testing "
+                        + "environment; consider requesting support from the API owner or "
+                        + "consider using Mockito; more details at go/ravenwood-docs");
+            } else {
+                sServiceManager.startService(target);
+            }
+        }
+        sServiceManager.sealStartedServices();
+
+        // TODO: expand to include additional boot phases when relevant
+        sServiceManager.startBootPhase(sTimings, SystemService.PHASE_SYSTEM_SERVICES_READY);
+        sServiceManager.startBootPhase(sTimings, SystemService.PHASE_BOOT_COMPLETED);
+    }
+
+    public static void reset(RavenwoodRule rule) {
+        // TODO: consider introducing shutdown boot phases
+
+        LocalServices.removeServiceForTest(SystemServiceManager.class);
+        sServiceManager = null;
+        sTimings = null;
+    }
+}
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index b90f112..a8c24fc 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -22,10 +22,13 @@
 
 import static org.junit.Assert.fail;
 
+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 android.util.ArraySet;
 
 import org.junit.Assume;
 import org.junit.rules.TestRule;
@@ -122,6 +125,11 @@
 
     final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
 
+    final ArraySet<Class<?>> mServicesRequired = new ArraySet<>();
+
+    volatile Context mContext;
+    volatile Instrumentation mInstrumentation;
+
     public RavenwoodRule() {
     }
 
@@ -192,6 +200,23 @@
             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(Class<?>... services) {
+            mRule.mServicesRequired.clear();
+            for (Class<?> service : services) {
+                mRule.mServicesRequired.add(service);
+            }
+            return this;
+        }
+
         public RavenwoodRule build() {
             return mRule;
         }
@@ -212,6 +237,28 @@
         return IS_ON_RAVENWOOD;
     }
 
+    /**
+     * 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.
+     */
+    public Context getContext() {
+        return Objects.requireNonNull(mContext,
+                "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.
+     */
+    public Instrumentation getInstrumentation() {
+        return Objects.requireNonNull(mInstrumentation,
+                "Instrumentation is only available during @Test execution");
+    }
+
     static boolean shouldEnableOnDevice(Description description) {
         if (description.isTest()) {
             if (description.getAnnotation(DisabledOnNonRavenwood.class) != null) {
diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt
index b5baef6..4a4c290 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -99,6 +99,7 @@
 android.util.StringBuilderPrinter
 android.util.TeeWriter
 android.util.TimeUtils
+android.util.TimingsTraceLog
 android.util.UtilConfig
 android.util.Xml
 
@@ -152,6 +153,7 @@
 android.os.ParcelUuid
 android.os.Parcelable
 android.os.PatternMatcher
+android.os.PermissionEnforcer
 android.os.PersistableBundle
 android.os.PowerComponents
 android.os.Process
@@ -159,6 +161,8 @@
 android.os.RemoteCallbackList
 android.os.RemoteException
 android.os.ResultReceiver
+android.os.ServiceManager
+android.os.ServiceManager$ServiceNotFoundException
 android.os.ServiceSpecificException
 android.os.StrictMode
 android.os.SystemClock
@@ -252,6 +256,9 @@
 android.view.Display$Mode
 android.view.DisplayInfo
 
+android.hardware.SerialManager
+android.hardware.SerialManagerInternal
+
 android.telephony.ActivityStatsTechSpecificInfo
 android.telephony.CellSignalStrength
 android.telephony.ModemActivityInfo
@@ -310,3 +317,9 @@
 com.google.android.collect.Lists
 com.google.android.collect.Maps
 com.google.android.collect.Sets
+
+com.android.server.SerialService
+com.android.server.SystemService
+com.android.server.SystemServiceManager
+
+com.android.server.utils.TimingsTraceAndSlog
diff --git a/ravenwood/ravenwood-services-jarjar-rules.txt b/ravenwood/ravenwood-services-jarjar-rules.txt
new file mode 100644
index 0000000..8fdd340
--- /dev/null
+++ b/ravenwood/ravenwood-services-jarjar-rules.txt
@@ -0,0 +1,11 @@
+# Ignore one-off class defined out in core/java/
+rule com.android.server.LocalServices @0
+rule com.android.server.pm.pkg.AndroidPackage @0
+rule com.android.server.pm.pkg.AndroidPackageSplit @0
+
+# Rename all other service internals so that tests can continue to statically
+# link services code when owners aren't ready to support on Ravenwood
+rule com.android.server.** repackaged.@0
+
+# TODO: support AIDL generated Parcelables via hoststubgen
+rule android.hardware.power.stats.** repackaged.@0
diff --git a/ravenwood/services-test/Android.bp b/ravenwood/services-test/Android.bp
new file mode 100644
index 0000000..39858f0
--- /dev/null
+++ b/ravenwood/services-test/Android.bp
@@ -0,0 +1,21 @@
+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: "RavenwoodServicesTest",
+    static_libs: [
+        "androidx.annotation_annotation",
+        "androidx.test.ext.junit",
+        "androidx.test.rules",
+    ],
+    srcs: [
+        "test/**/*.java",
+    ],
+    auto_gen_config: true,
+}
diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java
new file mode 100644
index 0000000..c1dee5d
--- /dev/null
+++ b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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 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.ravenwood.RavenwoodRule;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.server.LocalServices;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodServicesTest {
+    private static final String TEST_VIRTUAL_PORT = "virtual:example";
+
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProcessSystem()
+            .setServicesRequired(SerialManager.class)
+            .build();
+
+    @Test
+    public void testDefined() {
+        final SerialManager fromName = (SerialManager)
+                mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
+        final SerialManager fromClass =
+                mRavenwood.getContext().getSystemService(SerialManager.class);
+        assertNotNull(fromName);
+        assertNotNull(fromClass);
+        assertEquals(fromName, fromClass);
+
+        assertNotNull(LocalServices.getService(SerialManagerInternal.class));
+    }
+
+    @Test
+    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);
+    }
+
+    @Test
+    public void testDriven() {
+        final SerialManager service = (SerialManager)
+                mRavenwood.getContext().getSystemService(Context.SERIAL_SERVICE);
+        final SerialManagerInternal internal = LocalServices.getService(
+                SerialManagerInternal.class);
+
+        internal.addVirtualSerialPortForTest(TEST_VIRTUAL_PORT, () -> {
+            throw new UnsupportedOperationException(
+                    "Needs socketpair() to offer accurate emulation");
+        });
+        final String[] ports = service.getSerialPorts();
+        assertEquals(1, ports.length);
+        assertEquals(TEST_VIRTUAL_PORT, ports[0]);
+    }
+}
diff --git a/ravenwood/services.core-ravenwood-policies.txt b/ravenwood/services.core-ravenwood-policies.txt
new file mode 100644
index 0000000..d8d563e
--- /dev/null
+++ b/ravenwood/services.core-ravenwood-policies.txt
@@ -0,0 +1,7 @@
+# Ravenwood "policy" file for services.core.
+
+# Keep all AIDL interfaces
+class :aidl stubclass
+
+# Keep all feature flag implementations
+class :feature_flags stubclass
diff --git a/services/core/java/com/android/server/SerialService.java b/services/core/java/com/android/server/SerialService.java
index ff903a0..82c2038 100644
--- a/services/core/java/com/android/server/SerialService.java
+++ b/services/core/java/com/android/server/SerialService.java
@@ -17,51 +17,123 @@
 package com.android.server;
 
 import android.annotation.EnforcePermission;
+import android.annotation.NonNull;
 import android.content.Context;
 import android.hardware.ISerialManager;
+import android.hardware.SerialManagerInternal;
 import android.os.ParcelFileDescriptor;
+import android.os.PermissionEnforcer;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
 
 import java.io.File;
 import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.function.Supplier;
 
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class SerialService extends ISerialManager.Stub {
-
     private final Context mContext;
-    private final String[] mSerialPorts;
+
+    @GuardedBy("mSerialPorts")
+    private final LinkedHashMap<String, Supplier<ParcelFileDescriptor>> mSerialPorts =
+            new LinkedHashMap<>();
+
+    private static final String PREFIX_VIRTUAL = "virtual:";
 
     public SerialService(Context context) {
+        super(PermissionEnforcer.fromContext(context));
         mContext = context;
-        mSerialPorts = context.getResources().getStringArray(
+
+        synchronized (mSerialPorts) {
+            final String[] serialPorts = getSerialPorts(context);
+            for (String serialPort : serialPorts) {
+                mSerialPorts.put(serialPort, () -> {
+                    return native_open(serialPort);
+                });
+            }
+        }
+    }
+
+    @android.ravenwood.annotation.RavenwoodReplace
+    private static String[] getSerialPorts(Context context) {
+        return context.getResources().getStringArray(
                 com.android.internal.R.array.config_serialPorts);
     }
 
+    private static String[] getSerialPorts$ravenwood(Context context) {
+        return new String[0];
+    }
+
+    public static class Lifecycle extends SystemService {
+        private SerialService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = new SerialService(getContext());
+            publishBinderService(Context.SERIAL_SERVICE, mService);
+            publishLocalService(SerialManagerInternal.class, mService.mInternal);
+        }
+    }
+
     @EnforcePermission(android.Manifest.permission.SERIAL_PORT)
     public String[] getSerialPorts() {
         super.getSerialPorts_enforcePermission();
 
-        ArrayList<String> ports = new ArrayList<String>();
-        for (int i = 0; i < mSerialPorts.length; i++) {
-            String path = mSerialPorts[i];
-            if (new File(path).exists()) {
-                ports.add(path);
+        synchronized (mSerialPorts) {
+            final ArrayList<String> ports = new ArrayList<>();
+            for (String path : mSerialPorts.keySet()) {
+                if (path.startsWith(PREFIX_VIRTUAL) || new File(path).exists()) {
+                    ports.add(path);
+                }
             }
+            return ports.toArray(new String[ports.size()]);
         }
-        String[] result = new String[ports.size()];
-        ports.toArray(result);
-        return result;
     }
 
     @EnforcePermission(android.Manifest.permission.SERIAL_PORT)
     public ParcelFileDescriptor openSerialPort(String path) {
         super.openSerialPort_enforcePermission();
 
-        for (int i = 0; i < mSerialPorts.length; i++) {
-            if (mSerialPorts[i].equals(path)) {
-                return native_open(path);
+        synchronized (mSerialPorts) {
+            final Supplier<ParcelFileDescriptor> supplier = mSerialPorts.get(path);
+            if (supplier != null) {
+                return supplier.get();
+            } else {
+                throw new IllegalArgumentException("Invalid serial port " + path);
             }
         }
-        throw new IllegalArgumentException("Invalid serial port " + path);
     }
 
+    private final SerialManagerInternal mInternal = new SerialManagerInternal() {
+        @Override
+        public void addVirtualSerialPortForTest(@NonNull String name,
+                @NonNull Supplier<ParcelFileDescriptor> supplier) {
+            synchronized (mSerialPorts) {
+                Preconditions.checkState(!mSerialPorts.containsKey(name),
+                        "Port " + name + " already defined");
+                Preconditions.checkArgument(name.startsWith(PREFIX_VIRTUAL),
+                        "Port " + name + " must be under " + PREFIX_VIRTUAL);
+                mSerialPorts.put(name, supplier);
+            }
+        }
+
+        @Override
+        public void removeVirtualSerialPortForTest(@NonNull String name) {
+            synchronized (mSerialPorts) {
+                Preconditions.checkState(mSerialPorts.containsKey(name),
+                        "Port " + name + " not yet defined");
+                Preconditions.checkArgument(name.startsWith(PREFIX_VIRTUAL),
+                        "Port " + name + " must be under " + PREFIX_VIRTUAL);
+                mSerialPorts.remove(name);
+            }
+        }
+    };
+
     private native ParcelFileDescriptor native_open(String path);
 }
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index 933d259..7dc9f10 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -66,6 +66,7 @@
  * {@hide}
  */
 @SystemApi(client = Client.SYSTEM_SERVER)
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public abstract class SystemService {
 
     /** @hide */
diff --git a/services/core/java/com/android/server/SystemServiceManager.java b/services/core/java/com/android/server/SystemServiceManager.java
index a05b84b..20816a1 100644
--- a/services/core/java/com/android/server/SystemServiceManager.java
+++ b/services/core/java/com/android/server/SystemServiceManager.java
@@ -67,6 +67,8 @@
  *
  * {@hide}
  */
+@android.ravenwood.annotation.RavenwoodKeepPartialClass
+@android.ravenwood.annotation.RavenwoodKeepStaticInitializer
 public final class SystemServiceManager implements Dumpable {
     private static final String TAG = SystemServiceManager.class.getSimpleName();
     private static final boolean DEBUG = false;
@@ -123,7 +125,8 @@
     @GuardedBy("mTargetUsers")
     @Nullable private TargetUser mCurrentUser;
 
-    SystemServiceManager(Context context) {
+    @android.ravenwood.annotation.RavenwoodKeep
+    public SystemServiceManager(Context context) {
         mContext = context;
         mServices = new ArrayList<>();
         mServiceClassnames = new ArraySet<>();
@@ -136,6 +139,7 @@
      *
      * @return The service instance.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public SystemService startService(String className) {
         final Class<SystemService> serviceClass = loadClassFromLoader(className,
                 this.getClass().getClassLoader());
@@ -178,6 +182,7 @@
      * Loads and initializes a class from the given classLoader. Returns the class.
      */
     @SuppressWarnings("unchecked")
+    @android.ravenwood.annotation.RavenwoodKeep
     private static Class<SystemService> loadClassFromLoader(String className,
             ClassLoader classLoader) {
         try {
@@ -201,6 +206,7 @@
      * @return The service instance, never null.
      * @throws RuntimeException if the service fails to start.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public <T extends SystemService> T startService(Class<T> serviceClass) {
         try {
             final String name = serviceClass.getName();
@@ -237,6 +243,7 @@
         }
     }
 
+    @android.ravenwood.annotation.RavenwoodKeep
     public void startService(@NonNull final SystemService service) {
         // Check if already started
         String className = service.getClass().getName();
@@ -261,7 +268,8 @@
     }
 
     /** Disallow starting new services after this call. */
-    void sealStartedServices() {
+    @android.ravenwood.annotation.RavenwoodKeep
+    public void sealStartedServices() {
         mServiceClassnames = Collections.emptySet();
         mServices = Collections.unmodifiableList(mServices);
     }
@@ -273,6 +281,7 @@
      * @param t     trace logger
      * @param phase The boot phase to start.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public void startBootPhase(@NonNull TimingsTraceAndSlog t, int phase) {
         if (phase <= mCurrentPhase) {
             throw new IllegalArgumentException("Next phase must be larger than previous");
@@ -305,13 +314,23 @@
         if (phase == SystemService.PHASE_BOOT_COMPLETED) {
             final long totalBootTime = SystemClock.uptimeMillis() - mRuntimeStartUptime;
             t.logDuration("TotalBootTime", totalBootTime);
-            SystemServerInitThreadPool.shutdown();
+            shutdownInitThreadPool();
         }
     }
 
+    @android.ravenwood.annotation.RavenwoodReplace
+    private void shutdownInitThreadPool() {
+        SystemServerInitThreadPool.shutdown();
+    }
+
+    private void shutdownInitThreadPool$ravenwood() {
+        // Thread pool not configured yet on Ravenwood; ignored
+    }
+
     /**
      * @return true if system has completed the boot; false otherwise.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean isBootCompleted() {
         return mCurrentPhase >= SystemService.PHASE_BOOT_COMPLETED;
     }
@@ -646,12 +665,14 @@
     }
 
     /** Logs the failure. That's all. Tests may rely on parsing it, so only modify carefully. */
+    @android.ravenwood.annotation.RavenwoodKeep
     private void logFailure(String onWhat, TargetUser curUser, String serviceName, Exception ex) {
         Slog.wtf(TAG, "SystemService failure: Failure reporting " + onWhat + " of user "
                 + curUser + " to service " + serviceName, ex);
     }
 
     /** Sets the safe mode flag for services to query. */
+    @android.ravenwood.annotation.RavenwoodKeep
     void setSafeMode(boolean safeMode) {
         mSafeMode = safeMode;
     }
@@ -661,6 +682,7 @@
      *
      * @return safe mode flag
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean isSafeMode() {
         return mSafeMode;
     }
@@ -668,6 +690,7 @@
     /**
      * @return true if runtime was restarted, false if it's normal boot
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public boolean isRuntimeRestarted() {
         return mRuntimeRestarted;
     }
@@ -675,6 +698,7 @@
     /**
      * @return Time when SystemServer was started, in elapsed realtime.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public long getRuntimeStartElapsedTime() {
         return mRuntimeStartElapsedTime;
     }
@@ -682,17 +706,20 @@
     /**
      * @return Time when SystemServer was started, in uptime.
      */
+    @android.ravenwood.annotation.RavenwoodKeep
     public long getRuntimeStartUptime() {
         return mRuntimeStartUptime;
     }
 
-    void setStartInfo(boolean runtimeRestarted,
+    @android.ravenwood.annotation.RavenwoodKeep
+    public void setStartInfo(boolean runtimeRestarted,
             long runtimeStartElapsedTime, long runtimeStartUptime) {
         mRuntimeRestarted = runtimeRestarted;
         mRuntimeStartElapsedTime = runtimeStartElapsedTime;
         mRuntimeStartUptime = runtimeStartUptime;
     }
 
+    @android.ravenwood.annotation.RavenwoodKeep
     private void warnIfTooLong(long duration, SystemService service, String operation) {
         if (duration > SERVICE_CALL_WARN_TIME_MS) {
             Slog.w(TAG, "Service " + service.getClass().getName() + " took " + duration + " ms in "
diff --git a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
index 1050e8a..9ea143e 100644
--- a/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
+++ b/services/core/java/com/android/server/power/stats/MobileRadioPowerCalculator.java
@@ -32,7 +32,6 @@
 
 import java.util.ArrayList;
 
-@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public class MobileRadioPowerCalculator extends PowerCalculator {
     private static final String TAG = "MobRadioPowerCalculator";
     private static final boolean DEBUG = PowerCalculator.DEBUG;
diff --git a/services/core/java/com/android/server/utils/TimingsTraceAndSlog.java b/services/core/java/com/android/server/utils/TimingsTraceAndSlog.java
index b45c962..d8dfd9f 100644
--- a/services/core/java/com/android/server/utils/TimingsTraceAndSlog.java
+++ b/services/core/java/com/android/server/utils/TimingsTraceAndSlog.java
@@ -23,6 +23,7 @@
 /**
  * Helper class for reporting boot and shutdown timing metrics, also logging to {@link Slog}.
  */
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
 public final class TimingsTraceAndSlog extends TimingsTraceLog {
 
     /**
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index ee758db..e19f08c 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -1486,7 +1486,6 @@
         VcnManagementService vcnManagement = null;
         NetworkPolicyManagerService networkPolicy = null;
         WindowManagerService wm = null;
-        SerialService serial = null;
         NetworkTimeUpdateService networkTimeUpdater = null;
         InputManagerService inputManager = null;
         TelephonyRegistry telephonyRegistry = null;
@@ -2362,13 +2361,7 @@
 
             if (!isWatch) {
                 t.traceBegin("StartSerialService");
-                try {
-                    // Serial port support
-                    serial = new SerialService(context);
-                    ServiceManager.addService(Context.SERIAL_SERVICE, serial);
-                } catch (Throwable e) {
-                    Slog.e(TAG, "Failure starting SerialService", e);
-                }
+                mSystemServiceManager.startService(SerialService.Lifecycle.class);
                 t.traceEnd();
             }