Merge "Support service dependencies on Ravenwood." into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index c73e048..4791640 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -149,6 +149,7 @@
     installable: false,
     srcs: [":services.fakes-sources"],
     libs: [
+        "ravenwood-framework",
         "services.core.ravenwood",
     ],
     jarjar_rules: ":ravenwood-services-jarjar-rules",
@@ -204,6 +205,7 @@
         // Provide runtime versions of utils linked in below
         "junit",
         "truth",
+        "ravenwood-framework",
         "ravenwood-junit-impl",
         "mockito-ravenwood-prebuilt",
         "inline-mockito-ravenwood-prebuilt",
@@ -218,6 +220,7 @@
     libs: [
         "junit",
         "truth",
+        "ravenwood-framework",
         "ravenwood-junit",
         "mockito-ravenwood-prebuilt",
         "inline-mockito-ravenwood-prebuilt",
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 41a4a1a..e535f0a 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -77,6 +77,7 @@
     libs: [
         "android.test.mock",
         "framework-minus-apex.ravenwood",
+        "ravenwood-framework",
         "services.core.ravenwood",
         "junit",
     ],
@@ -102,6 +103,21 @@
     visibility: ["//visibility:public"],
 }
 
+// Library used to publish a handful of `android.ravenwood` APIs into
+// the Ravenwood BCP; we don't want to publish these APIs into the BCP
+// on physical devices, which is why this is a separate library
+java_library {
+    name: "ravenwood-framework",
+    srcs: [
+        "framework-src/**/*.java",
+    ],
+    libs: [
+        "framework-minus-apex.ravenwood",
+    ],
+    sdk_version: "core_current",
+    visibility: ["//visibility:public"],
+}
+
 java_host_for_device {
     name: "androidx.test.monitor-for-device",
     libs: [
diff --git a/ravenwood/framework-src/android/ravenwood/example/BlueManager.java b/ravenwood/framework-src/android/ravenwood/example/BlueManager.java
new file mode 100644
index 0000000..fc713b1
--- /dev/null
+++ b/ravenwood/framework-src/android/ravenwood/example/BlueManager.java
@@ -0,0 +1,34 @@
+/*
+ * 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.ravenwood.example;
+
+import android.annotation.SystemService;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+@SystemService(BlueManager.SERVICE_NAME)
+public class BlueManager {
+    public static final String SERVICE_NAME = "example_blue";
+
+    public String getInterfaceDescriptor() {
+        try {
+            return ServiceManager.getService(SERVICE_NAME).getInterfaceDescriptor();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
diff --git a/ravenwood/framework-src/android/ravenwood/example/RedManager.java b/ravenwood/framework-src/android/ravenwood/example/RedManager.java
new file mode 100644
index 0000000..381a901
--- /dev/null
+++ b/ravenwood/framework-src/android/ravenwood/example/RedManager.java
@@ -0,0 +1,34 @@
+/*
+ * 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.ravenwood.example;
+
+import android.annotation.SystemService;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+
+@SystemService(RedManager.SERVICE_NAME)
+public class RedManager {
+    public static final String SERVICE_NAME = "example_red";
+
+    public String getInterfaceDescriptor() {
+        try {
+            return ServiceManager.getService(SERVICE_NAME).getInterfaceDescriptor();
+        } catch (RemoteException e) {
+            throw e.rethrowFromSystemServer();
+        }
+    }
+}
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 c17d090..109ef76 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java
@@ -26,6 +26,8 @@
 import android.os.PermissionEnforcer;
 import android.os.ServiceManager;
 import android.os.UserHandle;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
 import android.test.mock.MockContext;
 import android.util.ArrayMap;
 import android.util.Singleton;
@@ -53,16 +55,23 @@
         mPackageName = packageName;
         mMainThread = mainThread;
 
+        // Services provided by a typical shipping device
         registerService(ClipboardManager.class,
-                Context.CLIPBOARD_SERVICE, asSingleton(() ->
+                Context.CLIPBOARD_SERVICE, memoize(() ->
                         new ClipboardManager(this, getMainThreadHandler())));
         registerService(PermissionEnforcer.class,
                 Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer);
         registerService(SerialManager.class,
-                Context.SERIAL_SERVICE, asSingleton(() ->
+                Context.SERIAL_SERVICE, memoize(() ->
                         new SerialManager(this, ISerialManager.Stub.asInterface(
                                 ServiceManager.getService(Context.SERIAL_SERVICE)))
                 ));
+
+        // Additional services we provide for testing purposes
+        registerService(BlueManager.class,
+                BlueManager.SERVICE_NAME, memoize(() -> new BlueManager()));
+        registerService(RedManager.class,
+                RedManager.SERVICE_NAME, memoize(() -> new RedManager()));
     }
 
     @Override
@@ -143,9 +152,12 @@
     }
 
     /**
-     * Wrap the given {@link Supplier} to become a memoized singleton.
+     * Wrap the given {@link Supplier} to become memoized.
+     *
+     * The underlying {@link Supplier} will only be invoked once, and that result will be cached
+     * and returned for any future requests.
      */
-    private static <T> Supplier<T> asSingleton(ThrowingSupplier<T> supplier) {
+    private static <T> Supplier<T> memoize(ThrowingSupplier<T> supplier) {
         final Singleton<T> singleton = new Singleton<>() {
             @Override
             protected T create() {
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 3de96c0..cd6b61d 100644
--- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
+++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java
@@ -19,13 +19,19 @@
 import android.content.ClipboardManager;
 import android.hardware.SerialManager;
 import android.os.SystemClock;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
 import android.util.ArrayMap;
+import android.util.ArraySet;
 
 import com.android.server.LocalServices;
 import com.android.server.SystemService;
 import com.android.server.SystemServiceManager;
 import com.android.server.utils.TimingsTraceAndSlog;
 
+import java.util.List;
+import java.util.Set;
+
 public class RavenwoodSystemServer {
     /**
      * Set of services that we know how to provide under Ravenwood. We keep this set distinct
@@ -37,16 +43,21 @@
      */
     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 {
+        // Services provided by a typical shipping device
         sKnownServices.put(ClipboardManager.class,
                 "com.android.server.FakeClipboardService$Lifecycle");
         sKnownServices.put(SerialManager.class,
                 "com.android.server.SerialService$Lifecycle");
+
+        // Additional services we provide for testing purposes
+        sKnownServices.put(BlueManager.class,
+                "com.android.server.example.BlueManagerService$Lifecycle");
+        sKnownServices.put(RedManager.class,
+                "com.android.server.example.RedManagerService$Lifecycle");
     }
 
+    private static Set<Class<?>> sStartedServices;
     private static TimingsTraceAndSlog sTimings;
     private static SystemServiceManager sServiceManager;
 
@@ -54,6 +65,7 @@
         // Avoid overhead if no services required
         if (rule.mServicesRequired.isEmpty()) return;
 
+        sStartedServices = new ArraySet<>();
         sTimings = new TimingsTraceAndSlog();
         sServiceManager = new SystemServiceManager(rule.mContext);
         sServiceManager.setStartInfo(false,
@@ -61,17 +73,7 @@
                 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);
-            }
-        }
+        startServices(rule.mServicesRequired);
         sServiceManager.sealStartedServices();
 
         // TODO: expand to include additional boot phases when relevant
@@ -85,5 +87,26 @@
         LocalServices.removeServiceForTest(SystemServiceManager.class);
         sServiceManager = null;
         sTimings = null;
+        sStartedServices = null;
+    }
+
+    private static void startServices(List<Class<?>> serviceClasses) {
+        for (Class<?> serviceClass : serviceClasses) {
+            // Quietly ignore duplicate requests if service already started
+            if (sStartedServices.contains(serviceClass)) continue;
+            sStartedServices.add(serviceClass);
+
+            final String serviceName = sKnownServices.get(serviceClass);
+            if (serviceName == null) {
+                throw new RuntimeException("The requested service " + serviceClass
+                        + " 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");
+            }
+
+            // Start service and then depth-first traversal of any dependencies
+            final SystemService instance = sServiceManager.startService(serviceName);
+            startServices(instance.getDependencies());
+        }
     }
 }
diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
index a520d4c..52ea340 100644
--- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
+++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java
@@ -35,6 +35,8 @@
 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;
@@ -127,7 +129,7 @@
 
     final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
 
-    final ArraySet<Class<?>> mServicesRequired = new ArraySet<>();
+    final List<Class<?>> mServicesRequired = new ArrayList<>();
 
     volatile Context mContext;
     volatile Instrumentation mInstrumentation;
diff --git a/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java
new file mode 100644
index 0000000..efe468d
--- /dev/null
+++ b/ravenwood/services-test/test/com/android/ravenwood/RavenwoodServicesDependenciesTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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 android.platform.test.ravenwood.RavenwoodRule;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@RunWith(AndroidJUnit4.class)
+public class RavenwoodServicesDependenciesTest {
+    // NOTE: we carefully only ask for RedManager here, and rely on Ravenwood internals to spin
+    // up the implicit dependency on BlueManager
+    @Rule
+    public final RavenwoodRule mRavenwood = new RavenwoodRule.Builder()
+            .setProcessSystem()
+            .setServicesRequired(RedManager.class)
+            .build();
+
+    @Test
+    public void testDirect() {
+        final RedManager red = mRavenwood.getContext().getSystemService(
+                RedManager.class);
+        assertEquals("blue+red", red.getInterfaceDescriptor());
+    }
+
+    @Test
+    public void testIndirect() {
+        final BlueManager blue = mRavenwood.getContext().getSystemService(
+                BlueManager.class);
+        assertEquals("blue", blue.getInterfaceDescriptor());
+    }
+}
diff --git a/services/core/java/com/android/server/SystemService.java b/services/core/java/com/android/server/SystemService.java
index 7dc9f10..4de85fe 100644
--- a/services/core/java/com/android/server/SystemService.java
+++ b/services/core/java/com/android/server/SystemService.java
@@ -39,7 +39,9 @@
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Objects;
 
 /**
  * The base class for services running in the system process. Override and implement
@@ -135,6 +137,7 @@
     public @interface BootPhase {}
 
     private final Context mContext;
+    private final List<Class<?>> mDependencies;
 
     /**
      * Class representing user in question in the lifecycle callbacks.
@@ -332,7 +335,28 @@
      * @param context The system server context.
      */
     public SystemService(@NonNull Context context) {
+        this(context, Collections.emptyList());
+    }
+
+    /**
+     * Initializes the system service.
+     * <p>
+     * Subclasses must define a single argument constructor that accepts the context
+     * and passes it to super.
+     * </p>
+     *
+     * @param context The system server context.
+     * @param dependencies The list of dependencies that this service requires to operate.
+     *                     Currently only used by the Ravenwood deviceless testing environment to
+     *                     understand transitive dependencies needed to support a specific test.
+     *                     For example, including {@code PowerManager.class} here indicates that
+     *                     this service requires the {@code PowerManager} and/or {@code
+     *                     PowerManagerInternal} APIs to function.
+     * @hide
+     */
+    public SystemService(@NonNull Context context, @NonNull List<Class<?>> dependencies) {
         mContext = context;
+        mDependencies = Objects.requireNonNull(dependencies);
     }
 
     /**
@@ -356,6 +380,22 @@
     }
 
     /**
+     * Get the list of dependencies that this service requires to operate.
+     *
+     * Currently only used by the Ravenwood deviceless testing environment to understand transitive
+     * dependencies needed to support a specific test.
+     *
+     * For example, including {@code PowerManager.class} here indicates that this service
+     * requires the {@code PowerManager} and/or {@code PowerManagerInternal} APIs to function.
+     *
+     * @hide
+     */
+    @NonNull
+    public final List<Class<?>> getDependencies() {
+        return mDependencies;
+    }
+
+    /**
      * Returns true if the system is running in safe mode.
      * TODO: we should define in which phase this becomes valid
      *
diff --git a/services/fakes/java/com/android/server/example/BlueManagerService.java b/services/fakes/java/com/android/server/example/BlueManagerService.java
new file mode 100644
index 0000000..8e3c8d7
--- /dev/null
+++ b/services/fakes/java/com/android/server/example/BlueManagerService.java
@@ -0,0 +1,44 @@
+/*
+ * 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.server.example;
+
+import android.content.Context;
+import android.os.Binder;
+import android.ravenwood.example.BlueManager;
+
+import com.android.server.SystemService;
+
+public class BlueManagerService extends Binder {
+    public static class Lifecycle extends SystemService {
+        private BlueManagerService mService;
+
+        public Lifecycle(Context context) {
+            super(context);
+        }
+
+        @Override
+        public void onStart() {
+            mService = new BlueManagerService();
+            publishBinderService(BlueManager.SERVICE_NAME, mService);
+        }
+    }
+
+    @Override
+    public String getInterfaceDescriptor() {
+        return "blue";
+    }
+}
diff --git a/services/fakes/java/com/android/server/example/RedManagerService.java b/services/fakes/java/com/android/server/example/RedManagerService.java
new file mode 100644
index 0000000..e0be733
--- /dev/null
+++ b/services/fakes/java/com/android/server/example/RedManagerService.java
@@ -0,0 +1,64 @@
+/*
+ * 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.server.example;
+
+import android.content.Context;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.ravenwood.example.BlueManager;
+import android.ravenwood.example.RedManager;
+
+import com.android.server.SystemService;
+
+import java.util.List;
+
+public class RedManagerService extends Binder {
+    private IBinder mBlueService;
+
+    public static class Lifecycle extends SystemService {
+        private RedManagerService mService;
+
+        public Lifecycle(Context context) {
+            super(context, List.of(BlueManager.class));
+        }
+
+        @Override
+        public void onStart() {
+            mService = new RedManagerService();
+            publishBinderService(RedManager.SERVICE_NAME, mService);
+        }
+
+        @Override
+        public void onBootPhase(int phase) {
+            if (phase == PHASE_SYSTEM_SERVICES_READY) {
+                mService.mBlueService = getBinderService(BlueManager.SERVICE_NAME);
+            }
+        }
+    }
+
+    @Override
+    public String getInterfaceDescriptor() {
+        try {
+            // Obtain the answer from dependency, but then augment it to prove that the answer
+            // was channeled through us
+            return mBlueService.getInterfaceDescriptor() + "+red";
+        } catch (RemoteException e) {
+            throw new RuntimeException(e);
+        }
+    }
+}