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);
+ }
+ }
+}