• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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  * <p> If `--metadata-only=true` is set, the resulting class would simply be:
58  * <pre>
59  * package com.foo;
60  * public final class RoSystemFeatures {
61  *     public static String getMethodNameForFeatureName(String featureName);
62   * }
63  * </pre>
64  */
65 object SystemFeaturesGenerator {
66     private const val FEATURE_ARG = "--feature="
67     private const val FEATURE_APIS_ARG = "--feature-apis="
68     private const val READONLY_ARG = "--readonly="
69     private const val METADATA_ONLY_ARG = "--metadata-only="
70     private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
71     private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
72     private val FEATUREINFO_CLASS = ClassName.get("android.content.pm", "FeatureInfo")
73     private val ARRAYMAP_CLASS = ClassName.get("android.util", "ArrayMap")
74     private val ASSUME_TRUE_CLASS =
75         ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8")
76     private val ASSUME_FALSE_CLASS =
77         ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8")
78 
usagenull79     private fun usage() {
80         println("Usage: SystemFeaturesGenerator <outputClassName> [options]")
81         println(" Options:")
82         println("  --readonly=true|false    Whether to encode features as build-time constants")
83         println("  --feature=\$NAME:\$VER     A feature+version pair, where \$VER can be:")
84         println("                             * blank/empty == undefined (variable API)")
85         println("                             * valid int   == enabled   (constant API)")
86         println("                             * UNAVAILABLE == disabled  (constant API)")
87         println("                           This will always generate associated query APIs,")
88         println("                           adding to or replacing those from `--feature-apis=`.")
89         println("  --feature-apis=\$NAME_1,\$NAME_2")
90         println("                           A comma-separated set of features for which to always")
91         println("                           generate named query APIs. If a feature in this set is")
92         println("                           not explicitly defined via `--feature=`, then a simple")
93         println("                           runtime passthrough API will be generated, regardless")
94         println("                           of the `--readonly` flag. This allows decoupling the")
95         println("                           API surface from variations in device feature sets.")
96         println("  --metadata-only=true|false Whether to simply output metadata about the")
97         println("                             generated API surface.")
98     }
99 
100     /** Main entrypoint for build-time system feature codegen. */
101     @JvmStatic
mainnull102     fun main(args: Array<String>) {
103         generate(args, System.out)
104     }
105 
106     /**
107      * Simple API entrypoint for build-time system feature codegen.
108      *
109      * Note: Typically this would be implemented in terms of a proper Builder-type input argument,
110      * but it's primarily used for testing as opposed to direct production usage.
111      */
112     @JvmStatic
generatenull113     fun generate(args: Array<String>, output: Appendable) {
114         if (args.size < 1) {
115             usage()
116             return
117         }
118 
119         var readonly = false
120         var metadataOnly = false
121         var outputClassName: ClassName? = null
122         val featureArgs = mutableListOf<FeatureInfo>()
123         // We could just as easily hardcode this list, as the static API surface should change
124         // somewhat infrequently, but this decouples the codegen from the framework completely.
125         val featureApiArgs = mutableSetOf<String>()
126         for (arg in args) {
127             when {
128                 arg.startsWith(READONLY_ARG) ->
129                     readonly = arg.substring(READONLY_ARG.length).toBoolean()
130                 arg.startsWith(METADATA_ONLY_ARG) ->
131                     metadataOnly = arg.substring(METADATA_ONLY_ARG.length).toBoolean()
132                 arg.startsWith(FEATURE_ARG) -> {
133                     featureArgs.add(parseFeatureArg(arg))
134                 }
135                 arg.startsWith(FEATURE_APIS_ARG) -> {
136                     featureApiArgs.addAll(
137                         arg.substring(FEATURE_APIS_ARG.length).split(",").map {
138                             parseFeatureName(it)
139                         }
140                     )
141                 }
142                 else -> outputClassName = ClassName.bestGuess(arg)
143             }
144         }
145 
146         // First load in all of the feature APIs we want to generate. Explicit feature definitions
147         // will then override this set with the appropriate readonly and version value.
148         val features = mutableMapOf<String, FeatureInfo>()
149         featureApiArgs.associateByTo(
150             features,
151             { it },
152             { FeatureInfo(it, version = null, readonly = false) },
153         )
154         featureArgs.associateByTo(
155             features,
156             { it.name },
157             { FeatureInfo(it.name, it.version, it.readonly && readonly) },
158         )
159 
160         outputClassName
161             ?: run {
162                 println("Output class name must be provided.")
163                 usage()
164                 return
165             }
166 
167         val classBuilder =
168             TypeSpec.classBuilder(outputClassName)
169                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
170                 .addJavadoc("@hide")
171 
172         if (metadataOnly) {
173             addMetadataMethodToClass(classBuilder, features.values)
174         } else {
175             addFeatureMethodsToClass(classBuilder, features.values)
176             addMaybeFeatureMethodToClass(classBuilder, features.values)
177             addGetFeaturesMethodToClass(classBuilder, features.values)
178         }
179 
180         // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
181         JavaFile.builder(outputClassName.packageName(), classBuilder.build())
182             .indent("    ")
183             .skipJavaLangImports(true)
184             .addFileComment("This file is auto-generated. DO NOT MODIFY.\n")
185             .addFileComment("Args: ${args.joinToString(" \\\n           ")}")
186             .build()
187             .writeTo(output)
188     }
189 
190     /*
191      * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional.
192      *   * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled)
193      *   * "--feature=WATCH:7" -> Feature enabled w/ version 7
194      *   * "--feature=WATCH:"  -> Feature status undefined, runtime API generated
195      *   * "--feature=WATCH:UNAVAILABLE"  -> Feature disabled
196      */
parseFeatureArgnull197     private fun parseFeatureArg(arg: String): FeatureInfo {
198         val featureArgs = arg.substring(FEATURE_ARG.length).split(":")
199         val name = parseFeatureName(featureArgs[0])
200         return when (featureArgs.getOrNull(1)) {
201             null, "" -> FeatureInfo(name, null, readonly = false)
202             "UNAVAILABLE" -> FeatureInfo(name, null, readonly = true)
203             else -> {
204                 val featureVersion =
205                     featureArgs[1].toIntOrNull()
206                         ?: throw IllegalArgumentException(
207                             "Invalid feature version input for $name: ${featureArgs[1]}"
208                         )
209                 FeatureInfo(name, featureVersion, readonly = true)
210             }
211         }
212     }
213 
parseFeatureNamenull214     private fun parseFeatureName(name: String): String =
215         when {
216             name.startsWith("android") ->
217                 throw IllegalArgumentException(
218                     "Invalid feature name input: \"android\"-namespaced features must be " +
219                     "provided as PackageManager.FEATURE_* suffixes, not raw feature strings."
220                 )
221             name.startsWith("FEATURE_") -> name
222             else -> "FEATURE_$name"
223         }
224 
225     /*
226      * Adds per-feature query methods to the class with the form:
227      * {@code public static boolean hasFeatureX(Context context)},
228      * returning the fallback value from PackageManager if not readonly.
229      */
addFeatureMethodsToClassnull230     private fun addFeatureMethodsToClass(
231         builder: TypeSpec.Builder,
232         features: Collection<FeatureInfo>,
233     ) {
234         for (feature in features) {
235             val methodBuilder =
236                 MethodSpec.methodBuilder(feature.methodName)
237                     .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
238                     .addJavadoc("Check for ${feature.name}.\n\n@hide")
239                     .returns(Boolean::class.java)
240                     .addParameter(CONTEXT_CLASS, "context")
241 
242             if (feature.readonly) {
243                 val featureEnabled = compareValues(feature.version, 0) >= 0
244                 methodBuilder.addAnnotation(
245                     if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS
246                 )
247                 methodBuilder.addStatement("return $featureEnabled")
248             } else {
249                 methodBuilder.addStatement(
250                     "return hasFeatureFallback(context, \$T.\$N)",
251                     PACKAGEMANAGER_CLASS,
252                     feature.name
253                 )
254             }
255             builder.addMethod(methodBuilder.build())
256         }
257 
258         // This is a trivial method, even if unused based on readonly-codegen, it does little harm
259         // to always include it.
260         builder.addMethod(
261             MethodSpec.methodBuilder("hasFeatureFallback")
262                 .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
263                 .returns(Boolean::class.java)
264                 .addParameter(CONTEXT_CLASS, "context")
265                 .addParameter(String::class.java, "featureName")
266                 .addStatement("return context.getPackageManager().hasSystemFeature(featureName)")
267                 .build()
268         )
269     }
270 
271     /*
272      * Adds a generic query method to the class with the form: {@code public static boolean
273      * maybeHasFeature(String featureName, int version)}, returning null if the feature version is
274      * undefined or not (compile-time) readonly.
275      *
276      * This method is useful for internal usage within the framework, e.g., from the implementation
277      * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only
278      * want a valid result if it's defined as readonly, and we want a custom fallback otherwise
279      * (e.g., to the existing runtime binder query).
280      */
addMaybeFeatureMethodToClassnull281     private fun addMaybeFeatureMethodToClass(
282         builder: TypeSpec.Builder,
283         features: Collection<FeatureInfo>,
284     ) {
285         val methodBuilder =
286             MethodSpec.methodBuilder("maybeHasFeature")
287                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
288                 .addAnnotation(ClassName.get("android.annotation", "Nullable"))
289                 .addJavadoc("@hide")
290                 .returns(Boolean::class.javaObjectType) // Use object type for nullability
291                 .addParameter(String::class.java, "featureName")
292                 .addParameter(Int::class.java, "version")
293 
294         var hasSwitchBlock = false
295         for (feature in features) {
296             // We only return non-null results for queries against readonly-defined features.
297             if (!feature.readonly) {
298                 continue
299             }
300             if (!hasSwitchBlock) {
301                 // As an optimization, only create the switch block if needed. Even an empty
302                 // switch-on-string block can induce a hash, which we can avoid if readonly
303                 // support is completely disabled.
304                 hasSwitchBlock = true
305                 methodBuilder.beginControlFlow("switch (featureName)")
306             }
307             methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
308             if (feature.version != null) {
309                 methodBuilder.addStatement("return \$L >= version", feature.version)
310             } else {
311                 methodBuilder.addStatement("return false")
312             }
313         }
314         if (hasSwitchBlock) {
315             methodBuilder.addCode("default: ")
316             methodBuilder.addStatement("break")
317             methodBuilder.endControlFlow()
318         }
319         methodBuilder.addStatement("return null")
320         builder.addMethod(methodBuilder.build())
321     }
322 
323     /*
324      * Adds a method to get all compile-time enabled features.
325      *
326      * This method is useful for internal usage within the framework to augment
327      * any system features that are parsed from the various partitions.
328      */
addGetFeaturesMethodToClassnull329     private fun addGetFeaturesMethodToClass(
330         builder: TypeSpec.Builder,
331         features: Collection<FeatureInfo>,
332     ) {
333         val methodBuilder =
334                 MethodSpec.methodBuilder("getReadOnlySystemEnabledFeatures")
335                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
336                 .addAnnotation(ClassName.get("android.annotation", "NonNull"))
337                 .addJavadoc("Gets features marked as available at compile-time, keyed by name." +
338                         "\n\n@hide")
339                 .returns(ParameterizedTypeName.get(
340                         ARRAYMAP_CLASS,
341                         ClassName.get(String::class.java),
342                         FEATUREINFO_CLASS))
343 
344         val availableFeatures = features.filter { it.readonly && it.version != null }
345         methodBuilder.addStatement("\$T<String, FeatureInfo> features = new \$T<>(\$L)",
346                 ARRAYMAP_CLASS, ARRAYMAP_CLASS, availableFeatures.size)
347         if (!availableFeatures.isEmpty()) {
348             methodBuilder.addStatement("FeatureInfo fi = new FeatureInfo()")
349         }
350         for (feature in availableFeatures) {
351             methodBuilder.addStatement("fi.name = \$T.\$N", PACKAGEMANAGER_CLASS, feature.name)
352             methodBuilder.addStatement("fi.version = \$L", feature.version)
353             methodBuilder.addStatement("features.put(fi.name, new FeatureInfo(fi))")
354         }
355         methodBuilder.addStatement("return features")
356         builder.addMethod(methodBuilder.build())
357     }
358 
359     /*
360      * Adds a metadata helper method that maps FEATURE_FOO names to their generated hasFeatureFoo()
361      * API counterpart, if defined.
362      */
addMetadataMethodToClassnull363     private fun addMetadataMethodToClass(
364         builder: TypeSpec.Builder,
365         features: Collection<FeatureInfo>,
366     ) {
367         val methodBuilder =
368             MethodSpec.methodBuilder("getMethodNameForFeatureName")
369                 .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
370                 .addJavadoc("@return \"hasFeatureFoo\" if FEATURE_FOO is in the API, else null")
371                 .returns(String::class.java)
372                 .addParameter(String::class.java, "featureVarName")
373 
374         methodBuilder.beginControlFlow("switch (featureVarName)")
375         for (feature in features) {
376             methodBuilder.addStatement("case \$S: return \$S", feature.name, feature.methodName)
377         }
378         methodBuilder.addStatement("default: return null").endControlFlow()
379 
380         builder.addMethod(methodBuilder.build())
381     }
382 
383     private data class FeatureInfo(val name: String, val version: Int?, val readonly: Boolean) {
384         // Turn "FEATURE_FOO" into "hasFeatureFoo".
385         val methodName get() = "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, name)
386     }
387 }
388