Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit abda5788 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add feature XML file parsing support" into main

parents 189c8410 1922f4ed
Loading
Loading
Loading
Loading
+15 −1
Original line number Diff line number Diff line
@@ -82,15 +82,23 @@ java_plugin {

genrule {
    name: "systemfeatures-gen-tests-srcs",
    srcs: [
        "tests/data/features-1.xml",
        "tests/data/features-2.xml",
    ],
    cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true --feature-apis=WATCH > $(location RoNoFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:UNAVAILABLE --feature=AUTO: > $(location RwFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:UNAVAILABLE --feature=AUTO: --feature-apis=WATCH,PC > $(location RoFeatures.java)",
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:UNAVAILABLE --feature=AUTO: --feature-apis=WATCH,PC > $(location RoFeatures.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeaturesFromXml --readonly=false --feature-xml-files=$(location tests/data/features-1.xml),$(location tests/data/features-2.xml) > $(location RwFeaturesFromXml.java) && " +
        "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeaturesFromXml --readonly=true --feature-xml-files=$(location tests/data/features-1.xml),$(location tests/data/features-2.xml) > $(location RoFeaturesFromXml.java)",
    out: [
        "RwNoFeatures.java",
        "RoNoFeatures.java",
        "RwFeatures.java",
        "RoFeatures.java",
        "RwFeaturesFromXml.java",
        "RoFeaturesFromXml.java",
    ],
    tools: ["systemfeatures-gen-tool"],
}
@@ -103,6 +111,10 @@ java_test_host {
        "tests/src/**/*.java",
        ":systemfeatures-gen-tests-srcs",
    ],
    data: [
        "tests/data/features-1.xml",
        "tests/data/features-2.xml",
    ],
    test_options: {
        unit_test: true,
    },
@@ -130,6 +142,8 @@ genrule {
        "tests/gen/RoNoFeatures.java.gen",
        "tests/gen/RwFeatures.java.gen",
        "tests/gen/RoFeatures.java.gen",
        "tests/gen/RwFeaturesFromXml.java.gen",
        "tests/gen/RoFeaturesFromXml.java.gen",
    ],
}

+105 −9
Original line number Diff line number Diff line
@@ -22,7 +22,13 @@ import com.squareup.javapoet.JavaFile
import com.squareup.javapoet.MethodSpec
import com.squareup.javapoet.ParameterizedTypeName
import com.squareup.javapoet.TypeSpec
import java.io.File
import javax.lang.model.element.Modifier
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.xpath.XPathConstants
import javax.xml.xpath.XPathFactory
import org.w3c.dom.Element
import org.w3c.dom.Node

/*
 * Simple Java code generator that takes as input a list of defined features and generates an
@@ -64,6 +70,7 @@ import javax.lang.model.element.Modifier
 */
object SystemFeaturesGenerator {
    private const val FEATURE_ARG = "--feature="
    private const val FEATURE_XML_FILES_ARG = "--feature-xml-files="
    private const val FEATURE_APIS_ARG = "--feature-apis="
    private const val READONLY_ARG = "--readonly="
    private const val METADATA_ONLY_ARG = "--metadata-only="
@@ -93,6 +100,10 @@ object SystemFeaturesGenerator {
        println("                           runtime passthrough API will be generated, regardless")
        println("                           of the `--readonly` flag. This allows decoupling the")
        println("                           API surface from variations in device feature sets.")
        println("  --feature-xml-files=\$XML_FILE_1,\$XML_FILE_2")
        println("                           A comma-separated list of XML permission feature files")
        println("                           to parse and add to the generated query APIs. The file")
        println("                           format matches that used by SystemConfig parsing.")
        println("  --metadata-only=true|false Whether to simply output metadata about the")
        println("                             generated API surface.")
    }
@@ -139,19 +150,37 @@ object SystemFeaturesGenerator {
                        }
                    )
                }
                arg.startsWith(FEATURE_XML_FILES_ARG) -> {
                    featureArgs.addAll(
                        parseFeatureXmlFiles(arg.substring(FEATURE_XML_FILES_ARG.length).split(","))
                    )
                }
                else -> outputClassName = ClassName.bestGuess(arg)
            }
        }

        // First load in all of the feature APIs we want to generate. Explicit feature definitions
        // will then override this set with the appropriate readonly and version value.
        val features = mutableMapOf<String, FeatureInfo>()
        // will then override this set with the appropriate readonly and version value. Note that we
        // use a sorted map to ensure stable codegen outputs given identical inputs.
        val features = sortedMapOf<String, FeatureInfo>()
        featureApiArgs.associateByTo(
            features,
            { it },
            { FeatureInfo(it, version = null, readonly = false) },
        )
        featureArgs.associateByTo(

        // Multiple defs for the same feature may be present when aggregating permission files.
        // To preserve SystemConfig semantics, use the following ordering for insertion priority:
        //     * readonly (build-time overrides runtime)
        //     * unavailable (version == null, overrides available)
        //     * version (higher overrides lower)
        featureArgs
            .sortedWith(
                compareBy<FeatureInfo> { it.readonly }
                    .thenBy { it.version == null }
                    .thenBy { it.version }
            )
            .associateByTo(
                features,
                { it.name },
                { FeatureInfo(it.name, it.version, it.readonly && readonly) },
@@ -159,7 +188,7 @@ object SystemFeaturesGenerator {

        outputClassName
            ?: run {
                println("Output class name must be provided.")
                System.err.println("Output class name must be provided.")
                usage()
                return
            }
@@ -214,7 +243,7 @@ object SystemFeaturesGenerator {
    private fun parseFeatureName(name: String): String =
        when {
            name.startsWith("android") -> {
                SystemFeaturesLookup.getDeclaredFeatureVarNameFromValue(name)
                parseFeatureNameFromValue(name)
                    ?: throw IllegalArgumentException(
                        "Unrecognized Android system feature name: $name"
                    )
@@ -223,6 +252,73 @@ object SystemFeaturesGenerator {
            else -> "FEATURE_$name"
        }

    private fun parseFeatureNameFromValue(name: String): String? =
        SystemFeaturesLookup.getDeclaredFeatureVarNameFromValue(name)


    /**
     * Parses a list of feature permission XML file paths into a list of FeatureInfo definitions.
     */
    private fun parseFeatureXmlFiles(filePaths: Collection<String>): Collection<FeatureInfo> =
        filePaths.flatMap {
            try {
                parseFeatureXmlFile(File(it))
            } catch (e: Exception) {
                throw IllegalArgumentException("Error parsing feature XML file: $it", e)
            }
        }

    /**
     * Parses a feature permission XML file into a (possibly empty) list of FeatureInfo definitions.
     */
    private fun parseFeatureXmlFile(file: File): Collection<FeatureInfo> {
        val doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file)
        doc.documentElement.normalize()

        val xPath = XPathFactory.newInstance().newXPath()
        val rootElement =
            xPath.evaluate("/permissions", doc, XPathConstants.NODE) as? Element
                ?: xPath.evaluate("/config", doc, XPathConstants.NODE) as? Element
        if (rootElement == null) {
            System.err.println("Warning: No <permissions>/<config> elements found in ${file.path}")
            return emptyList()
        }

        return rootElement.childNodes.let { nodeList ->
            (0 until nodeList.length)
                .asSequence()
                .map { nodeList.item(it) }
                .filter { it.nodeType == Node.ELEMENT_NODE }
                .map { it as Element }
                .mapNotNull { element ->
                    when (element.tagName) {
                        "feature" -> parseFeatureElement(element)
                        "unavailable-feature" -> parseUnavailableFeatureElement(element)
                        else -> null
                    }
                }
                .toList()
        }
    }

    private fun parseFeatureElement(element: Element): FeatureInfo? {
        val name = parseFeatureNameFromValue(element.getAttribute("name")) ?: return null
        return if (element.getAttribute("notLowRam") == "true") {
            // If a feature is marked as being disabled on low-ram devices (notLowRam==true), we
            // we cannot finalize the exported feature version or its availability, as we don't
            // (yet) know whether the target product is low-ram.
            FeatureInfo(name, version = null, readonly = false)
        } else {
            val version = element.getAttribute("version")
            FeatureInfo(name, version.toIntOrNull() ?: 0, readonly = true)
        }
    }

    private fun parseUnavailableFeatureElement(element: Element): FeatureInfo? {
        val name = parseFeatureNameFromValue(element.getAttribute("name")) ?: return null
        return FeatureInfo(name, version = null, readonly = true)
    }

    /*
     * Adds per-feature query methods to the class with the form:
     * {@code public static boolean hasFeatureX(Context context)},
+29 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2025 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<permissions>
     <!-- Implicit version=0 will be overridden in features-2.xml. -->
    <feature name="android.hardware.type.embedded" />
    <!--- Overridden by the pc <unavailable-feature /> def in features-2.xml. -->
    <feature name="android.hardware.type.pc" />
    <!-- Ignored as it's not a platform-defined feature. -->
    <feature name="com.arbitrary.feature" />
    <!-- Overrides the watch <feature /> def in features-2.xml. -->
    <unavailable-feature name="android.hardware.type.watch" />
    <!-- Exposed in the API but not as a build-time constant (readonly) feature,
         as we don't know if the device is low-ram. -->
    <feature name="android.hardware.bluetooth" notLowRam="true" />
</permissions>
 No newline at end of file
+28 −0
Original line number Diff line number Diff line
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright (C) 2025 The Android Open Source Project

     Licensed under the Apache License, Version 2.0 (the "License");
     you may not use this file except in compliance with the License.
     You may obtain a copy of the License at

          http://www.apache.org/licenses/LICENSE-2.0

     Unless required by applicable law or agreed to in writing, software
     distributed under the License is distributed on an "AS IS" BASIS,
     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     See the License for the specific language governing permissions and
     limitations under the License.
-->

<permissions>
    <!-- Overrides the embedded <feature /> version from features-1.xml. -->
    <feature name="android.hardware.type.embedded" version="1"/>
    <!-- Overridden by the <unavailable-feature /> def in features-1.xml. -->
    <feature name="android.hardware.type.watch" version="1" />
    <!-- Redundant with the wifi <feature /> def in features-1.xml. -->
    <feature name="android.hardware.wifi" />
    <!-- Ignored as it's not a platform-defined feature. -->
    <feature name="com.arbitrary.feature.2" />
    <!-- Overrides the pc <feature /> def from features-1.xml. -->
    <unavailable-feature name="android.hardware.type.pc" />
</permissions>
 No newline at end of file
+16 −16
Original line number Diff line number Diff line
@@ -22,13 +22,12 @@ import com.android.aconfig.annotations.AssumeTrueForR8;
 */
public final class RoFeatures {
    /**
     * Check for FEATURE_WATCH.
     * Check for FEATURE_AUTO.
     *
     * @hide
     */
    @AssumeTrueForR8
    public static boolean hasFeatureWatch(Context context) {
        return true;
    public static boolean hasFeatureAuto(Context context) {
        return hasFeatureFallback(context, PackageManager.FEATURE_AUTO);
    }

    /**
@@ -41,32 +40,33 @@ public final class RoFeatures {
    }

    /**
     * Check for FEATURE_WIFI.
     * Check for FEATURE_VULKAN.
     *
     * @hide
     */
    @AssumeTrueForR8
    public static boolean hasFeatureWifi(Context context) {
        return true;
    @AssumeFalseForR8
    public static boolean hasFeatureVulkan(Context context) {
        return false;
    }

    /**
     * Check for FEATURE_VULKAN.
     * Check for FEATURE_WATCH.
     *
     * @hide
     */
    @AssumeFalseForR8
    public static boolean hasFeatureVulkan(Context context) {
        return false;
    @AssumeTrueForR8
    public static boolean hasFeatureWatch(Context context) {
        return true;
    }

    /**
     * Check for FEATURE_AUTO.
     * Check for FEATURE_WIFI.
     *
     * @hide
     */
    public static boolean hasFeatureAuto(Context context) {
        return hasFeatureFallback(context, PackageManager.FEATURE_AUTO);
    @AssumeTrueForR8
    public static boolean hasFeatureWifi(Context context) {
        return true;
    }

    private static boolean hasFeatureFallback(Context context, String featureName) {
@@ -79,9 +79,9 @@ public final class RoFeatures {
    @Nullable
    public static Boolean maybeHasFeature(String featureName, int version) {
        switch (featureName) {
            case PackageManager.FEATURE_VULKAN: return false;
            case PackageManager.FEATURE_WATCH: return 1 >= version;
            case PackageManager.FEATURE_WIFI: return 0 >= version;
            case PackageManager.FEATURE_VULKAN: return false;
            default: break;
        }
        return null;
Loading