diff options
11 files changed, 345 insertions, 19 deletions
diff --git a/Ravenwood.bp b/Ravenwood.bp index c73e04896173..4791640239b3 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -149,6 +149,7 @@ java_library { installable: false, srcs: [":services.fakes-sources"], libs: [ + "ravenwood-framework", "services.core.ravenwood", ], jarjar_rules: ":ravenwood-services-jarjar-rules", @@ -204,6 +205,7 @@ android_ravenwood_libgroup { // 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 @@ android_ravenwood_libgroup { 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 41a4a1ad1ccb..e535f0a05a02 100644 --- a/ravenwood/Android.bp +++ b/ravenwood/Android.bp @@ -77,6 +77,7 @@ java_library { libs: [ "android.test.mock", "framework-minus-apex.ravenwood", + "ravenwood-framework", "services.core.ravenwood", "junit", ], @@ -102,6 +103,21 @@ java_library { 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 000000000000..fc713b1bd164 --- /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 000000000000..381a901f234a --- /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 c17d0903f856..109ef76b535f 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.Looper; 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 @@ public class RavenwoodContext extends MockContext { 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 @@ public class RavenwoodContext extends MockContext { } /** - * 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 3de96c0990ea..cd6b61df392f 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 @@ package android.platform.test.ravenwood; 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 @@ public class RavenwoodSystemServer { */ 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 @@ public class RavenwoodSystemServer { // 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 @@ public class RavenwoodSystemServer { 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 @@ public class RavenwoodSystemServer { 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 a520d4ccafa1..52ea3402fa62 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.rules.TestRule; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; @@ -127,7 +129,7 @@ public class RavenwoodRule implements TestRule { 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 000000000000..efe468d0df25 --- /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 7dc9f10e646c..4de85fe9f2ff 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.io.PrintWriter; 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 abstract class SystemService { 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 @@ public abstract class SystemService { * @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 @@ public abstract class SystemService { } /** + * 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 000000000000..8e3c8d73df9c --- /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 000000000000..e0be7337698d --- /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); + } + } +} |