Add SystemFeaturesMetadata.maybeGetSdkFeatureIndex

Add a simple index lookup method for feature names. This returns:
  * [0, SDK_FEATURE_INDEX) for PackageManager-defined system features
  * -1 otherwise

This will be used in a follow-up to efficiently compute a dense array
of cached system feature versions that can be used for fast lookup in
client processes.

Bug: 375000483
Test: atest SystemFeaturesMetadataProcessorTest
Flag: EXEMPT currently unused
Change-Id: I145c08f7eb58b832534f62950357a18f7b4e3651
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
index 100d869..4a6d4b1 100644
--- a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesMetadataProcessor.kt
@@ -17,8 +17,10 @@
 package com.android.systemfeatures
 
 import android.annotation.SdkConstant
+import com.squareup.javapoet.ClassName
 import com.squareup.javapoet.FieldSpec
 import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.TypeSpec
 import java.io.IOException
 import javax.annotation.processing.AbstractProcessor
@@ -27,6 +29,7 @@
 import javax.lang.model.SourceVersion
 import javax.lang.model.element.Modifier
 import javax.lang.model.element.TypeElement
+import javax.lang.model.element.VariableElement
 import javax.tools.Diagnostic
 
 /*
@@ -35,7 +38,16 @@
  * <p>The output is a single class file, `com.android.internal.pm.SystemFeaturesMetadata`, with
  * properties computed from feature constant definitions in the PackageManager class. This
  * class is only produced if the processed environment includes PackageManager; all other
- * invocations are ignored.
+ * invocations are ignored. The generated API is as follows:
+ *
+ * <pre>
+ * package android.content.pm;
+ * public final class SystemFeaturesMetadata {
+ *     public static final int SDK_FEATURE_COUNT;
+ *     // @return [0, SDK_FEATURE_COUNT) if an SDK-defined system feature, -1 otherwise.
+ *     public static int maybeGetSdkFeatureIndex(String featureName);
+ * }
+ * </pre>
  */
 class SystemFeaturesMetadataProcessor : AbstractProcessor() {
 
@@ -56,19 +68,31 @@
             return false
         }
 
-        // We're only interested in feature constants defined in PackageManager.
-        var featureCount = 0
-        roundEnv.getElementsAnnotatedWith(SdkConstant::class.java).forEach {
-            if (
-                it.enclosingElement == packageManagerType &&
-                    it.getAnnotation(SdkConstant::class.java).value ==
-                        SdkConstant.SdkConstantType.FEATURE
-            ) {
-                featureCount++
-            }
-        }
+        // Collect all FEATURE-annotated fields from PackageManager, and
+        //  1) Use the field values to de-duplicate, as there can be multiple FEATURE_* fields that
+        //     map to the same feature string name value.
+        //  2) Ensure they're sorted to ensure consistency and determinism between builds.
+        val featureVarNames =
+            roundEnv
+                .getElementsAnnotatedWith(SdkConstant::class.java)
+                .asSequence()
+                .filter {
+                    it.enclosingElement == packageManagerType &&
+                        it.getAnnotation(SdkConstant::class.java).value ==
+                            SdkConstant.SdkConstantType.FEATURE
+                }
+                .mapNotNull { element ->
+                    (element as? VariableElement)?.let { varElement ->
+                        varElement.getConstantValue()?.toString() to
+                            varElement.simpleName.toString()
+                    }
+                }
+                .toMap()
+                .values
+                .sorted()
+                .toList()
 
-        if (featureCount == 0) {
+        if (featureVarNames.isEmpty()) {
             // This is fine, and happens for any environment that doesn't include PackageManager.
             return false
         }
@@ -77,16 +101,8 @@
             TypeSpec.classBuilder("SystemFeaturesMetadata")
                 .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
                 .addJavadoc("@hide")
-                .addField(
-                    FieldSpec.builder(Int::class.java, "SDK_FEATURE_COUNT")
-                        .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
-                        .addJavadoc(
-                            "The number of `@SdkConstant` features defined in PackageManager."
-                        )
-                        .addJavadoc("@hide")
-                        .initializer("\$L", featureCount)
-                        .build()
-                )
+                .addField(buildFeatureCount(featureVarNames))
+                .addMethod(buildFeatureIndexLookup(featureVarNames))
                 .build()
 
         try {
@@ -104,7 +120,41 @@
         return true
     }
 
+    private fun buildFeatureCount(featureVarNames: Collection<String>): FieldSpec {
+        return FieldSpec.builder(Int::class.java, "SDK_FEATURE_COUNT")
+            .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL)
+            .addJavadoc(
+                "# of {@link android.annotation.SdkConstant}` features defined in PackageManager."
+            )
+            .addJavadoc("\n\n@hide")
+            .initializer("\$L", featureVarNames.size)
+            .build()
+    }
+
+    private fun buildFeatureIndexLookup(featureVarNames: Collection<String>): MethodSpec {
+        val methodBuilder =
+            MethodSpec.methodBuilder("maybeGetSdkFeatureIndex")
+                .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+                .addJavadoc("@return an index in [0, SDK_FEATURE_COUNT) for features defined ")
+                .addJavadoc("in PackageManager, else -1.")
+                .addJavadoc("\n\n@hide")
+                .returns(Int::class.java)
+                .addParameter(String::class.java, "featureName")
+        methodBuilder.beginControlFlow("switch (featureName)")
+        featureVarNames.forEachIndexed { index, featureVarName ->
+            methodBuilder
+                .addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, featureVarName)
+                .addStatement("return \$L", index)
+        }
+        methodBuilder
+            .addCode("default: ")
+            .addStatement("return -1")
+            .endControlFlow()
+        return methodBuilder.build()
+    }
+
     companion object {
         private val SDK_CONSTANT_ANNOTATION_NAME = SdkConstant::class.qualifiedName
+        private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
     }
 }
diff --git a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
index 4ffb5b9..74ce6da 100644
--- a/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
+++ b/tools/systemfeatures/tests/src/SystemFeaturesMetadataProcessorTest.java
@@ -16,10 +16,16 @@
 
 package com.android.systemfeatures;
 
+import static com.android.internal.pm.SystemFeaturesMetadata.maybeGetSdkFeatureIndex;
+
 import static com.google.common.truth.Truth.assertThat;
 
+import android.content.pm.PackageManager;
+
 import com.android.internal.pm.SystemFeaturesMetadata;
 
+import com.google.common.collect.Range;
+
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.junit.runners.JUnit4;
@@ -33,4 +39,17 @@
         // It defines 5 annotated features, and any/all other constants should be ignored.
         assertThat(SystemFeaturesMetadata.SDK_FEATURE_COUNT).isEqualTo(5);
     }
+
+    @Test
+    public void testSdkFeatureIndex() {
+        // Only SDK-defined features return valid indices.
+        final Range validIndexRange = Range.closedOpen(0, SystemFeaturesMetadata.SDK_FEATURE_COUNT);
+        assertThat(maybeGetSdkFeatureIndex(PackageManager.FEATURE_PC)).isIn(validIndexRange);
+        assertThat(maybeGetSdkFeatureIndex(PackageManager.FEATURE_VULKAN)).isIn(validIndexRange);
+        assertThat(maybeGetSdkFeatureIndex(PackageManager.FEATURE_NOT_ANNOTATED)).isEqualTo(-1);
+        assertThat(maybeGetSdkFeatureIndex(PackageManager.NOT_FEATURE)).isEqualTo(-1);
+        assertThat(maybeGetSdkFeatureIndex("foo")).isEqualTo(-1);
+        assertThat(maybeGetSdkFeatureIndex("0")).isEqualTo(-1);
+        assertThat(maybeGetSdkFeatureIndex("")).isEqualTo(-1);
+    }
 }