diff options
author | 2024-08-09 21:50:49 +0000 | |
---|---|---|
committer | 2024-08-09 21:50:49 +0000 | |
commit | a35b1f13ddef1d27f368c371dcd942c2c3fb37df (patch) | |
tree | 23be10fef0c7feffbbfbd3551d1f402106ba7c7f | |
parent | 31a796e38ec2978aada8fd3bc849868619b540ed (diff) | |
parent | 230d6502e7cfb6008d11825baa8604e7e331c1a2 (diff) |
Merge "Initial system feature codegen prototype" into main
-rw-r--r-- | tools/systemfeatures/Android.bp | 63 | ||||
-rw-r--r-- | tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt | 218 | ||||
-rw-r--r-- | tools/systemfeatures/tests/Context.java | 27 | ||||
-rw-r--r-- | tools/systemfeatures/tests/PackageManager.java | 30 | ||||
-rw-r--r-- | tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java | 135 |
5 files changed, 473 insertions, 0 deletions
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp new file mode 100644 index 000000000000..2cebfe9790d0 --- /dev/null +++ b/tools/systemfeatures/Android.bp @@ -0,0 +1,63 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +java_library_host { + name: "systemfeatures-gen-lib", + srcs: [ + "src/**/*.java", + "src/**/*.kt", + ], + static_libs: [ + "guava", + "javapoet", + ], +} + +java_binary_host { + name: "systemfeatures-gen-tool", + main_class: "com.android.systemfeatures.SystemFeaturesGenerator", + static_libs: ["systemfeatures-gen-lib"], +} + +// TODO(b/203143243): Add golden diff test for generated sources. +// Functional runtime behavior is covered in systemfeatures-gen-tests. +genrule { + name: "systemfeatures-gen-tests-srcs", + cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " + + "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)", + out: [ + "RwNoFeatures.java", + "RoNoFeatures.java", + "RwFeatures.java", + "RoFeatures.java", + ], + tools: ["systemfeatures-gen-tool"], +} + +java_test_host { + name: "systemfeatures-gen-tests", + test_suites: ["general-tests"], + srcs: [ + "tests/**/*.java", + ":systemfeatures-gen-tests-srcs", + ], + test_options: { + unit_test: true, + }, + static_libs: [ + "aconfig-annotations-lib", + "framework-annotations-lib", + "junit", + "objenesis", + "mockito", + "truth", + ], +} diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt new file mode 100644 index 000000000000..9bfda451067f --- /dev/null +++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt @@ -0,0 +1,218 @@ +/* + * 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.systemfeatures + +import com.google.common.base.CaseFormat +import com.squareup.javapoet.ClassName +import com.squareup.javapoet.JavaFile +import com.squareup.javapoet.MethodSpec +import com.squareup.javapoet.TypeSpec +import javax.lang.model.element.Modifier + +/* + * Simple Java code generator that takes as input a list of defined features and generates an + * accessory class based on the provided versions. + * + * <p>Example: + * + * <pre> + * <cmd> com.foo.RoSystemFeatures --readonly=true \ + * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 + * </pre> + * + * This generates a class that has the following signature: + * + * <pre> + * package com.foo; + * public final class RoSystemFeatures { + * @AssumeTrueForR8 + * public static boolean hasFeatureWatch(Context context); + * @AssumeFalseForR8 + * public static boolean hasFeatureAutomotive(Context context); + * @AssumeTrueForR8 + * public static boolean hasFeatureVulkan(Context context); + * public static Boolean maybeHasFeature(String feature, int version); + * } + * </pre> + */ +object SystemFeaturesGenerator { + private const val FEATURE_ARG = "--feature=" + private const val READONLY_ARG = "--readonly=" + private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager") + private val CONTEXT_CLASS = ClassName.get("android.content", "Context") + private val ASSUME_TRUE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8") + private val ASSUME_FALSE_CLASS = + ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8") + + private fun usage() { + println("Usage: SystemFeaturesGenerator <outputClassName> [options]") + println(" Options:") + println(" --readonly=true|false Whether to encode features as build-time constants") + println(" --feature=\$NAME:\$VER A feature+version pair (blank version == disabled)") + } + + /** Main entrypoint for build-time system feature codegen. */ + @JvmStatic + fun main(args: Array<String>) { + if (args.size < 1) { + usage() + return + } + + var readonly = false + var outputClassName: ClassName? = null + val features = mutableListOf<FeatureInfo>() + for (arg in args) { + when { + arg.startsWith(READONLY_ARG) -> + readonly = arg.substring(READONLY_ARG.length).toBoolean() + arg.startsWith(FEATURE_ARG) -> { + features.add(parseFeatureArg(arg)) + } + else -> outputClassName = ClassName.bestGuess(arg) + } + } + + outputClassName + ?: run { + println("Output class name must be provided.") + usage() + return + } + + val classBuilder = + TypeSpec.classBuilder(outputClassName) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addJavadoc("@hide") + + addFeatureMethodsToClass(classBuilder, readonly, features) + addMaybeFeatureMethodToClass(classBuilder, readonly, features) + + // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency. + JavaFile.builder(outputClassName.packageName(), classBuilder.build()) + .build() + .writeTo(System.out) + } + + /* + * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional. + * * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled) + * * "--feature=WATCH:7" -> Feature enabled w/ version 7 + * * "--feature=WATCH:" -> Feature disabled + */ + private fun parseFeatureArg(arg: String): FeatureInfo { + val featureArgs = arg.substring(FEATURE_ARG.length).split(":") + val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it } + val version = featureArgs.getOrNull(1)?.toIntOrNull() + return FeatureInfo(name, version) + } + + /* + * Adds per-feature query methods to the class with the form: + * {@code public static boolean hasFeatureX(Context context)}, + * returning the fallback value from PackageManager if not readonly. + */ + private fun addFeatureMethodsToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + for (feature in features) { + // Turn "FEATURE_FOO" into "hasFeatureFoo". + val methodName = + "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name) + val methodBuilder = + MethodSpec.methodBuilder(methodName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + + if (readonly) { + val featureEnabled = compareValues(feature.version, 0) >= 0 + methodBuilder.addAnnotation( + if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS + ) + methodBuilder.addStatement("return $featureEnabled") + } else { + methodBuilder.addStatement( + "return hasFeatureFallback(context, \$T.\$N)", + PACKAGEMANAGER_CLASS, + feature.name + ) + } + builder.addMethod(methodBuilder.build()) + } + + if (!readonly) { + builder.addMethod( + MethodSpec.methodBuilder("hasFeatureFallback") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .returns(Boolean::class.java) + .addParameter(CONTEXT_CLASS, "context") + .addParameter(String::class.java, "featureName") + .addStatement( + "return context.getPackageManager().hasSystemFeature(featureName, 0)" + ) + .build() + ) + } + } + + /* + * Adds a generic query method to the class with the form: {@code public static boolean + * maybeHasFeature(String featureName, int version)}, returning null if the feature version is + * undefined or not readonly. + * + * This method is useful for internal usage within the framework, e.g., from the implementation + * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only + * want a valid result if it's defined as readonly, and we want a custom fallback otherwise + * (e.g., to the existing runtime binder query). + */ + private fun addMaybeFeatureMethodToClass( + builder: TypeSpec.Builder, + readonly: Boolean, + features: List<FeatureInfo> + ) { + val methodBuilder = + MethodSpec.methodBuilder("maybeHasFeature") + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addAnnotation(ClassName.get("android.annotation", "Nullable")) + .returns(Boolean::class.javaObjectType) // Use object type for nullability + .addParameter(String::class.java, "featureName") + .addParameter(Int::class.java, "version") + + if (readonly) { + methodBuilder.beginControlFlow("switch (featureName)") + for (feature in features) { + methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name) + if (feature.version != null) { + methodBuilder.addStatement("return \$L >= version", feature.version) + } else { + methodBuilder.addStatement("return false") + } + } + methodBuilder.addCode("default: ") + methodBuilder.addStatement("break") + methodBuilder.endControlFlow() + } + methodBuilder.addStatement("return null") + builder.addMethod(methodBuilder.build()) + } + + private data class FeatureInfo(val name: String, val version: Int?) +} diff --git a/tools/systemfeatures/tests/Context.java b/tools/systemfeatures/tests/Context.java new file mode 100644 index 000000000000..630bc0771a01 --- /dev/null +++ b/tools/systemfeatures/tests/Context.java @@ -0,0 +1,27 @@ +/* + * 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.content; + +import android.content.pm.PackageManager; + +/** Stub for testing. */ +public class Context { + /** @hide */ + public PackageManager getPackageManager() { + return null; + } +} diff --git a/tools/systemfeatures/tests/PackageManager.java b/tools/systemfeatures/tests/PackageManager.java new file mode 100644 index 000000000000..645d500bc762 --- /dev/null +++ b/tools/systemfeatures/tests/PackageManager.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.content.pm; + +/** Stub for testing */ +public class PackageManager { + public static final String FEATURE_AUTO = "automotive"; + public static final String FEATURE_VULKAN = "vulkan"; + public static final String FEATURE_WATCH = "watch"; + public static final String FEATURE_WIFI = "wifi"; + + /** @hide */ + public boolean hasSystemFeature(String featureName, int version) { + return false; + } +} diff --git a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java new file mode 100644 index 000000000000..547d2cbd26f9 --- /dev/null +++ b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java @@ -0,0 +1,135 @@ +/* + * 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.systemfeatures; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.anyInt; +import static org.mockito.Mockito.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.pm.PackageManager; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.JUnit4; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnit; +import org.mockito.junit.MockitoRule; + +@RunWith(JUnit4.class) +public class SystemFeaturesGeneratorTest { + + @Rule public final MockitoRule mockito = MockitoJUnit.rule(); + + @Mock private Context mContext; + @Mock private PackageManager mPackageManager; + + @Before + public void setUp() { + when(mContext.getPackageManager()).thenReturn(mPackageManager); + } + + @Test + public void testReadonlyDisabledNoDefinedFeatures() { + // Always report null for conditional queries if readonly codegen is disabled. + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyNoDefinedFeatures() { + // If no features are explicitly declared as readonly available, always report + // null for conditional queries. + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyDisabledWithDefinedFeatures() { + // Always fall back to the PackageManager for defined, explicit features queries. + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true); + assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse(); + + when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false); + assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse(); + + // For defined and undefined features, conditional queries should report null (unknown). + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull(); + assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + } + + @Test + public void testReadonlyWithDefinedFeatures() { + // Always use the build-time feature version for defined, explicit feature queries, never + // falling back to the runtime query. + assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue(); + assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse(); + assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse(); + verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt()); + + // For defined feature types, conditional queries should reflect the build-time versions. + // VERSION=1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 100)).isFalse(); + + // VERSION=0 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse(); + + // VERSION=-1 + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse(); + + // DISABLED + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse(); + assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse(); + + // For undefined types, conditional queries should report null (unknown). + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull(); + assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull(); + } +} |