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