1 /*
2  * Copyright (C) 2024 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.systemfeatures
18 
19 import com.google.common.base.CaseFormat
20 import com.squareup.javapoet.ClassName
21 import com.squareup.javapoet.JavaFile
22 import com.squareup.javapoet.MethodSpec
23 import com.squareup.javapoet.ParameterizedTypeName
24 import com.squareup.javapoet.TypeSpec
25 import javax.lang.model.element.Modifier
26 
27 /*
28  * Simple Java code generator that takes as input a list of defined features and generates an
29  * accessory class based on the provided versions.
30  *
31  * <p>Example:
32  *
33  * <pre>
34  *   <cmd> com.foo.RoSystemFeatures --readonly=true \
35  *           --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348 --feature=PC:UNAVAILABLE
36  *           --feature-apis=WATCH,PC,LEANBACK
37  * </pre>
38  *
39  * This generates a class that has the following signature:
40  *
41  * <pre>
42  * package com.foo;
43  * public final class RoSystemFeatures {
44  *     @AssumeTrueForR8
45  *     public static boolean hasFeatureWatch(Context context);
46  *     @AssumeFalseForR8
47  *     public static boolean hasFeaturePc(Context context);
48  *     @AssumeTrueForR8
49  *     public static boolean hasFeatureVulkan(Context context);
50  *     public static boolean hasFeatureAutomotive(Context context);
51  *     public static boolean hasFeatureLeanback(Context context);
52  *     public static Boolean maybeHasFeature(String feature, int version);
53  *     public static ArrayMap<String, FeatureInfo> getReadOnlySystemEnabledFeatures();
54  * }
55  * </pre>
56  */
57 object SystemFeaturesGenerator {
58     private const val FEATURE_ARG = "--feature="
59     private const val FEATURE_APIS_ARG = "--feature-apis="
60     private const val READONLY_ARG = "--readonly="
61     private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
62     private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
63     private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo")
64     private val ARRAYMAP_CLASS = ClassName.get("android.util", "ArrayMap")
65     private val ASSUME_TRUE_CLASS =
66         ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8")
67     private val ASSUME_FALSE_CLASS =
68         ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8")
69 
usagenull70     private fun usage() {
71         println("Usage: SystemFeaturesGenerator <outputClassName> [options]")
72         println(" Options:")
73         println("  --readonly=true|false    Whether to encode features as build-time constants")
74         println("  --feature=\$NAME:\$VER     A feature+version pair, where \$VER can be:")
75         println("                             * blank/empty == undefined (variable API)")
76         println("                             * valid int   == enabled   (constant API)")
77         println("                             * UNAVAILABLE == disabled  (constant API)")
78         println("                           This will always generate associated query APIs,")
79         println("                           adding to or replacing those from `--feature-apis=`.")
80         println("  --feature-apis=\$NAME_1,\$NAME_2")
81         println("                           A comma-separated set of features for which to always")
82         println("                           generate named query APIs. If a feature in this set is")
83         println("                           not explicitly defined via `--feature=`, then a simple")
84         println("                           runtime passthrough API will be generated, regardless")
85         println("                           of the `--readonly` flag. This allows decoupling the")
86         println("                           API surface from variations in device feature sets.")
87     }
88 
89     /** Main entrypoint for build-time system feature codegen. */
90     @JvmStatic
mainnull91     fun main(args: Array<String>) {
92         generate(args, System.out)
93     }
94 
95     /**
96      * Simple API entrypoint for build-time system feature codegen.
97      *
98      * Note: Typically this would be implemented in terms of a proper Builder-type input argument,
99      * but it's primarily used for testing as opposed to direct production usage.
100      */
101     @JvmStatic
generatenull102     fun generate(args: Array<String>, output: Appendable) {
103         if (args.size < 1) {
104             usage()
105             return
106         }
107 
108         var readonly = false
109         var outputClassName: ClassName? = null
110         val featureArgs = mutableListOf<FeatureInfo>()
111         // We could just as easily hardcode this list, as the static API surface should change
112         // somewhat infrequently, but this decouples the codegen from the framework completely.
113         val featureApiArgs = mutableSetOf<String>()
114         for (arg in args) {
115             when {
116                 arg.startsWith(READONLY_ARG) ->
117                     readonly = arg.substring(READONLY_ARG.length).toBoolean()
118                 arg.startsWith(FEATURE_ARG) -> {
119                     featureArgs.add(parseFeatureArg(arg))
120                 }
121                 arg.startsWith(FEATURE_APIS_ARG) -> {
122                     featureApiArgs.addAll(
123                         arg.substring(FEATURE_APIS_ARG.length).split(",").map {
124                             parseFeatureName(it)
125                         }
126                     )
127                 }
128                 else -> outputClassName = ClassName.bestGuess(arg)
129             }
130         }
131 
132         // First load in all of the feature APIs we want to generate. Explicit feature definitions
133         // will then override this set with the appropriate readonly and version value.
134         val features = mutableMapOf<String, FeatureInfo>()
135         featureApiArgs.associateByTo(
136             features,
137             { it },
138             { FeatureInfo(it, version = null, readonly = false) },
139         )
140         featureArgs.associateByTo(
141             features,
142             { it.name },
143             { FeatureInfo(it.name, it.version, it.readonly && readonly) },
144         )
145 
146         outputClassName
147             ?: run {
148                 println("Output class name must be provided.")
149                 usage()
150                 return
151             }
152 
153         val classBuilder =
154             TypeSpec.classBuilder(outputClassName)
155                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
156                 .addJavadoc("@hide")
157 
158         addFeatureMethodsToClass(classBuilder, features.values)
159         addMaybeFeatureMethodToClass(classBuilder, features.values)
160         addGetFeaturesMethodToClass(classBuilder, features.values)
161 
162         // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
163         JavaFile.builder(outputClassName.packageName(), classBuilder.build())
164             .indent("    ")
165             .skipJavaLangImports(true)
166             .addFileComment("This file is auto-generated. DO NOT MODIFY.\n")
167             .addFileComment("Args: ${args.joinToString(" \\\n           ")}")
168             .build()
169             .writeTo(output)
170     }
171 
172     /*
173      * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional.
174      *   * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled)
175      *   * "--feature=WATCH:7" -> Feature enabled w/ version 7
176      *   * "--feature=WATCH:"  -> Feature status undefined, runtime API generated
177      *   * "--feature=WATCH:UNAVAILABLE"  -> Feature disabled
178      */
parseFeatureArgnull179     private fun parseFeatureArg(arg: String): FeatureInfo {
180         val featureArgs = arg.substring(FEATURE_ARG.length).split(":")
181         val name = parseFeatureName(featureArgs[0])
182         return when (featureArgs.getOrNull(1)) {
183             null, "" -> FeatureInfo(name, null, readonly = false)
184             "UNAVAILABLE" -> FeatureInfo(name, null, readonly = true)
185             else -> {
186                 val featureVersion =
187                     featureArgs[1].toIntOrNull()
188                         ?: throw IllegalArgumentException(
189                             "Invalid feature version input for $name: ${featureArgs[1]}"
190                         )
191                 FeatureInfo(name, featureVersion, readonly = true)
192             }
193         }
194     }
195 
parseFeatureNamenull196     private fun parseFeatureName(name: String): String =
197         when {
198             name.startsWith("android") ->
199                 throw IllegalArgumentException(
200                     "Invalid feature name input: \"android\"-namespaced features must be " +
201                     "provided as PackageManager.FEATURE_* suffixes, not raw feature strings."
202                 )
203             name.startsWith("FEATURE_") -> name
204             else -> "FEATURE_$name"
205         }
206 
207     /*
208      * Adds per-feature query methods to the class with the form:
209      * {@code public static boolean hasFeatureX(Context context)},
210      * returning the fallback value from PackageManager if not readonly.
211      */
addFeatureMethodsToClassnull212     private fun addFeatureMethodsToClass(
213         builder: TypeSpec.Builder,
214         features: Collection<FeatureInfo>,
215     ) {
216         for (feature in features) {
217             // Turn "FEATURE_FOO" into "hasFeatureFoo".
218             val methodName =
219                 "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name)
220             val methodBuilder =
221                 MethodSpec.methodBuilder(methodName)
222                     .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
223                     .addJavadoc("Check for ${feature.name}.\n\n@hide")
224                     .returns(Boolean::class.java)
225                     .addParameter(CONTEXT_CLASS, "context")
226 
227             if (feature.readonly) {
228                 val featureEnabled = compareValues(feature.version, 0) >= 0
229                 methodBuilder.addAnnotation(
230                     if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS
231                 )
232                 methodBuilder.addStatement("return $featureEnabled")
233             } else {
234                 methodBuilder.addStatement(
235                     "return hasFeatureFallback(context, \$T.\$N)",
236                     PACKAGEMANAGER_CLASS,
237                     feature.name
238                 )
239             }
240             builder.addMethod(methodBuilder.build())
241         }
242 
243         // This is a trivial method, even if unused based on readonly-codegen, it does little harm
244         // to always include it.
245         builder.addMethod(
246             MethodSpec.methodBuilder("hasFeatureFallback")
247                 .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
248                 .returns(Boolean::class.java)
249                 .addParameter(CONTEXT_CLASS, "context")
250                 .addParameter(String::class.java, "featureName")
251                 .addStatement("return context.getPackageManager().hasSystemFeature(featureName, 0)")
252                 .build()
253         )
254     }
255 
256     /*
257      * Adds a generic query method to the class with the form: {@code public static boolean
258      * maybeHasFeature(String featureName, int version)}, returning null if the feature version is
259      * undefined or not (compile-time) readonly.
260      *
261      * This method is useful for internal usage within the framework, e.g., from the implementation
262      * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only
263      * want a valid result if it's defined as readonly, and we want a custom fallback otherwise
264      * (e.g., to the existing runtime binder query).
265      */
addMaybeFeatureMethodToClassnull266     private fun addMaybeFeatureMethodToClass(
267         builder: TypeSpec.Builder,
268         features: Collection<FeatureInfo>,
269     ) {
270         val methodBuilder =
271             MethodSpec.methodBuilder("maybeHasFeature")
272                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
273                 .addAnnotation(ClassName.get("android.annotation", "Nullable"))
274                 .addJavadoc("@hide")
275                 .returns(Boolean::class.javaObjectType) // Use object type for nullability
276                 .addParameter(String::class.java, "featureName")
277                 .addParameter(Int::class.java, "version")
278 
279         var hasSwitchBlock = false
280         for (feature in features) {
281             // We only return non-null results for queries against readonly-defined features.
282             if (!feature.readonly) {
283                 continue
284             }
285             if (!hasSwitchBlock) {
286                 // As an optimization, only create the switch block if needed. Even an empty
287                 // switch-on-string block can induce a hash, which we can avoid if readonly
288                 // support is completely disabled.
289                 hasSwitchBlock = true
290                 methodBuilder.beginControlFlow("switch (featureName)")
291             }
292             methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
293             if (feature.version != null) {
294                 methodBuilder.addStatement("return \$L >= version", feature.version)
295             } else {
296                 methodBuilder.addStatement("return false")
297             }
298         }
299         if (hasSwitchBlock) {
300             methodBuilder.addCode("default: ")
301             methodBuilder.addStatement("break")
302             methodBuilder.endControlFlow()
303         }
304         methodBuilder.addStatement("return null")
305         builder.addMethod(methodBuilder.build())
306     }
307 
308     /*
309      * Adds a method to get all compile-time enabled features.
310      *
311      * This method is useful for internal usage within the framework to augment
312      * any system features that are parsed from the various partitions.
313      */
addGetFeaturesMethodToClassnull314     private fun addGetFeaturesMethodToClass(
315         builder: TypeSpec.Builder,
316         features: Collection<FeatureInfo>,
317     ) {
318         val methodBuilder =
319                 MethodSpec.methodBuilder("getReadOnlySystemEnabledFeatures")
320                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
321                 .addAnnotation(ClassName.get("android.annotation", "NonNull"))
322                 .addJavadoc("Gets features marked as available at compile-time, keyed by name." +
323                         "\n\n@hide")
324                 .returns(ParameterizedTypeName.get(
325                         ARRAYMAP_CLASS,
326                         ClassName.get(String::class.java),
327                         FEATUREINFO_CLASS))
328 
329         val availableFeatures = features.filter { it.readonly && it.version != null }
330         methodBuilder.addStatement("\$T<String, FeatureInfo> features = new \$T<>(\$L)",
331                 ARRAYMAP_CLASS, ARRAYMAP_CLASS, availableFeatures.size)
332         if (!availableFeatures.isEmpty()) {
333             methodBuilder.addStatement("FeatureInfo fi = new FeatureInfo()")
334         }
335         for (feature in availableFeatures) {
336             methodBuilder.addStatement("fi.name = \$T.\$N", PACKAGEMANAGER_CLASS, feature.name)
337             methodBuilder.addStatement("fi.version = \$L", feature.version)
338             methodBuilder.addStatement("features.put(fi.name, new FeatureInfo(fi))")
339         }
340         methodBuilder.addStatement("return features")
341         builder.addMethod(methodBuilder.build())
342     }
343 
344     private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean)
345 }
346