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