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

Commit 93d7a5e5 authored by Victor Gabriel Savu's avatar Victor Gabriel Savu
Browse files

Split up annotation processing

Split up annotation processing into @PolicyDefinition,
@EnumPolicyDefinition and @BooleanPolicyDefinition handling. Also
extensively documents what each processor does.

Bug: 442556276
Test: atest frameworks/base/tools/processors/devicepolicy/test/src/android/processor/devicepolicy/test/PolicyProcessorTest.kt
Flag: android.app.admin.flags.policy_streamlining
Change-Id: I7aa85d8b5c35244664a2e45666e35f16ecc88bce
parent 2db3be6d
Loading
Loading
Loading
Loading
+62 −0
Original line number Diff line number Diff line
/*
* 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.
*/

package android.processor.devicepolicy

import com.android.json.stream.JsonWriter
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Element
import javax.lang.model.type.TypeMirror

/**
 * Process elements with @BooleanPolicyDefinition.
 *
 * Since this annotation holds no data and we don't export any type-specific information, this only
 * contains type-specific checks.
 */
class BooleanProcessor(processingEnv: ProcessingEnvironment) : Processor(processingEnv) {
    private companion object {
        const val SIMPLE_TYPE_BOOLEAN = "java.lang.Boolean"
    }

    /** Represents a built-in Boolean */
    val booleanType: TypeMirror =
        processingEnv.elementUtils.getTypeElement(SIMPLE_TYPE_BOOLEAN).asType()

    /**
     * Process an {@link Element} representing a {@link android.app.admin.PolicyIdentifier} into useful data.
     *
     * @return null if the element does not have a @BooleanPolicyDefinition or on error, {@link BooleanPolicyMetadata} otherwise.
     */
    fun process(element: Element): BooleanPolicyMetadata? {
        element.getAnnotation(BooleanPolicyDefinition::class.java) ?: return null

        if (!processingEnv.typeUtils.isSameType(policyType(element), booleanType)) {
            printError(
                element,
                "booleanValue in @PolicyDefinition can only be applied to policies of type $booleanType."
            )
        }

        return BooleanPolicyMetadata()
    }
}

class BooleanPolicyMetadata() : PolicyMetadata() {
    override fun dump(writer: JsonWriter) {
        // Nothing to include for BooleanPolicyMetadata.
    }
}
 No newline at end of file
+215 −20
Original line number Diff line number Diff line
@@ -17,21 +17,216 @@
package android.processor.devicepolicy

import com.android.json.stream.JsonWriter
import com.sun.source.tree.IdentifierTree
import com.sun.source.tree.MemberSelectTree
import com.sun.source.tree.NewArrayTree
import com.sun.source.util.SimpleTreeVisitor
import com.sun.source.util.Trees
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.AnnotationMirror
import javax.lang.model.element.AnnotationValue
import javax.lang.model.element.Element
import javax.lang.model.element.TypeElement
import javax.lang.model.type.TypeMirror

/**
 * Process elements with @EnumPolicyDefinition.
 *
 * Since information about enums values is encoded by @IntDef, we need to go trough that annotation
 * as well. Since this processor is not the direct consumer of @IntDef and we want the names for the
 * enum entries, not just values, we will have to use the JDK to walk the AST.
 *
 * We extract:
 * <ul>
 *     <li> The default value. </li>
 *     <li> The documentation for the enumeration. </li>
 *     <li> All enumeration entries with value, name and documentation. </li>
 * </ul>
 *
 * We will use the following example to illustrate what we are doing:
 * {@snippet :
 *     public static final int ENUM_ENTRY_1 = 0;
 *     public static final int ENUM_ENTRY_2 = 1;
 *
 *     @Retention(RetentionPolicy.SOURCE)
 *     @IntDef(prefix = { "ENUM_ENTRY_" }, value = {
 *             ENUM_ENTRY_1,
 *             ENUM_ENTRY_2,
 *     })
 *     public @interface PolicyEnum {}
 *
 *     @PolicyDefinition
 *     @EnumPolicyDefinition(
 *             defaultValue = ENUM_ENTRY_2,
 *             intDef = PolicyEnum.class
 *     )
 *     public static final PolicyIdentifier<Integer> EXAMPLE_POLICY) = new PolicyIdentifier<>(
 *             "EXAMPLE_POLICY");
 * }
 */
class EnumProcessor(processingEnv: ProcessingEnvironment) : Processor(processingEnv) {
    private companion object {
        const val SIMPLE_TYPE_INTEGER = "java.lang.Integer"

        /**
         * Find the first value matching a predicate on the key.
         */
        fun <K, V> Map<K, V>.firstValue(filter: (K) -> Boolean): V {
            return entries.first { (key, _) -> filter(key) }.value
        }
    }

    /** Represents a built-in Integer */
    val integerType: TypeMirror =
        processingEnv.elementUtils.getTypeElement(SIMPLE_TYPE_INTEGER).asType()

    /**
     * Process an {@link Element} representing a {@link android.app.admin.PolicyIdentifier} into useful data.
     *
     * @return null if the element does not have a @EnumPolicyDefinition or on error, {@link EnumPolicyMetadata} otherwise.
     */
    fun process(element: Element): EnumPolicyMetadata? {
        val enumPolicyAnnotation =
            element.getAnnotation(EnumPolicyDefinition::class.java) ?: return null

        if (!processingEnv.typeUtils.isSameType(policyType(element), integerType)) {
            printError(
                element,
                "@EnumPolicyDefinition can only be applied to policies of type $integerType."
            )
        }

        // In the class-level example above, this would be PolicyEnum.
        val intDefElement = getIntDefElement(element)

        // This is the IntDef annotation class.
        val intDefClass = processingEnv.elementUtils.getTypeElement("android.annotation.IntDef")
        // In the class-level example above, this is @IntDef annotation on the PolicyEnum.
        val annotationMirror =
            intDefElement.annotationMirrors.firstOrNull { it.annotationType.asElement() == intDefClass }

        if (annotationMirror == null) {
            printError(
                element, "@EnumPolicyDefinition.intDef must be the interface marked with @IntDef."
            )

            return null
        }

        val enumName = intDefElement.qualifiedName.toString()
        val enumDoc = processingEnv.elementUtils.getDocComment(intDefElement)

        // In the class-level example above, these would be ENUM_ENTRY_1 and ENUM_ENTRY_2.
        val entries = getIntDefIdentifiers(annotationMirror, intDefElement)

        return EnumPolicyMetadata(
            enumPolicyAnnotation.defaultValue, enumName, enumDoc, entries
        )
    }

    /**
     * Given a policy definition element finds the type element representing the IntDef definition
     * Same as `processingEnv.elementUtils.getTypeElement(enumPolicyMetadata.intDef.qualifiedName)`,
     * but we have to use type mirrors.
     */
    private fun getIntDefElement(element: Element): TypeElement {
        val am = element.annotationMirrors.first {
            it.annotationType.toString() == EnumPolicyDefinition::class.java.name
        }
        val av = am.elementValues.firstValue { key ->
            key.simpleName.toString() == "intDef"
        }
        val mirror = av.value as TypeMirror
        return processingEnv.typeUtils.asElement(mirror) as TypeElement
    }

    /**
     * For an @IntDef marked element, get the identifiers used to build up that enum.
     *
     * In the class-level example above, these would be ENUM_ENTRY_1 and ENUM_ENTRY_2.     *
     */
    private fun getIntDefIdentifiers(
        annotationMirror: AnnotationMirror, intDefElement: TypeElement
    ): List<EnumEntryMetadata> {
        val annotationValue: AnnotationValue = annotationMirror.elementValues.firstValue { key ->
            key.simpleName.contentEquals("value")
        }

        // Walk the AST as we want the actual identifiers passed to @IntDef.
        val trees = Trees.instance(processingEnv)
        val tree = trees.getTree(intDefElement, annotationMirror, annotationValue)

        val identifiers = ArrayList<String>()
        tree.accept(IdentifierVisitor(), identifiers)

        // In the class-level example above, these would be {ENUM_ENTRY_1,ENUM_ENTRY_2}.
        @Suppress("UNCHECKED_CAST") val values = annotationValue.value as List<AnnotationValue>

        val documentations = identifiers.map { identifier ->
            // TODO(b/442973945): Fail gracefully when the element is not part of the parent class.
            val identifierElement = intDefElement.enclosingElement.enclosedElements.find {
                it.simpleName.toString() == identifier
            }

            processingEnv.elementUtils.getDocComment(identifierElement)
        }

        return identifiers.mapIndexed { i, identifier ->
            EnumEntryMetadata(identifier, values[i].value as Int, documentations[i])
        }
    }

    private class IdentifierVisitor : SimpleTreeVisitor<Void, ArrayList<String>>() {
        override fun visitNewArray(node: NewArrayTree, identifiers: ArrayList<String>): Void? {
            for (initializer in node.initializers) {
                initializer.accept(this, identifiers)
            }

            return null
        }

        /**
         * Called when the identifier used in IntDef is a member of the class.
         */
        override fun visitMemberSelect(
            node: MemberSelectTree, identifiers: ArrayList<String>
        ): Void? {
            identifiers.add(node.identifier.toString())

            return null
        }

        /**
         * Called when the identifier in IntDef is an arbitrary identifier pointing outside the
         * current class.
         *
         * This is not present in the class-level example above, but was added to be consistent
         * with the original @IntDef processor logic.
         */
        override fun visitIdentifier(node: IdentifierTree, identifiers: ArrayList<String>): Void? {
            identifiers.add(node.name.toString())

            return null
        }
    }
}

class EnumEntryMetadata(val name: String, val value: Int, val documentation: String) {
    fun dump(writer: JsonWriter) {
        writer.beginObject()
        writer.apply {
            beginObject()

        writer.name("name")
        writer.value(name)
            name("name")
            value(name)

        writer.name("value")
        writer.value(value.toLong())
            name("value")
            value(value.toLong())

        writer.name("documentation")
        writer.value(documentation)
            name("documentation")
            value(documentation)

        writer.endObject()
            endObject()
        }
    }
}

@@ -42,20 +237,20 @@ class EnumPolicyMetadata(
    val entries: List<EnumEntryMetadata>
) : PolicyMetadata() {
    override fun dump(writer: JsonWriter) {
        writer.name("default")
        writer.value(defaultValue.toLong())

        writer.name("enum")
        writer.value(enum)
        writer.apply {
            name("default")
            value(defaultValue.toLong())

        writer.name("enumDocumentation")
        writer.value(enumDocumentation)
            name("enum")
            value(enum)

        writer.name("options")
        writer.beginArray()
            name("enumDocumentation")
            value(enumDocumentation)

            name("options")
            beginArray()
            entries.forEach { it.dump(writer) }

            writer.endArray()
        }
    }
}
 No newline at end of file
+110 −19
Original line number Diff line number Diff line
@@ -17,28 +17,125 @@
package android.processor.devicepolicy

import com.android.json.stream.JsonWriter
import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Element
import javax.lang.model.element.ElementKind
import javax.lang.model.type.DeclaredType

/**
 * Process elements with @PolicyDefinition.
 */
class PolicyAnnotationProcessor(processingEnv: ProcessingEnvironment) : Processor(processingEnv) {
    private companion object {
        const val POLICY_IDENTIFIER = "android.app.admin.PolicyIdentifier"
    }

    val booleanProcessor = BooleanProcessor(processingEnv)
    val enumProcessor = EnumProcessor(processingEnv)

    val policyIdentifierElem =
        processingEnv.elementUtils.getTypeElement(POLICY_IDENTIFIER) ?: throw IllegalStateException(
            "Could not find $POLICY_IDENTIFIER"
        )

    /** Represents a android.app.admin.PolicyIdentifier<T> */
    val policyIdentifierType = policyIdentifierElem.asType()
        ?: throw IllegalStateException("Could not get type of $POLICY_IDENTIFIER")

    /** Represents a android.app.admin.PolicyIdentifier<?> */
    val genericPolicyIdentifierType = processingEnv.typeUtils.getDeclaredType(
        policyIdentifierElem, processingEnv.typeUtils.getWildcardType(null, null)
    ) ?: throw IllegalStateException("Could not get generic type of $POLICY_IDENTIFIER")

    /**
     * Process an {@link Element} representing a {@link android.app.admin.PolicyIdentifier} into useful data.
     *
     * @return All data about the policy or null on error and reports the error to the user.
     */
    fun process(element: Element): Policy? {
        element.getAnnotation(PolicyDefinition::class.java)
            ?: throw IllegalArgumentException("Element $element does not have the @PolicyDefinition annotation")

        if (element.kind != ElementKind.FIELD) {
            printError(element, "@PolicyDefinition can only be applied to fields")
            return null
        }

        val elementType = element.asType() as DeclaredType
        val enclosingType = element.enclosingElement.asType()

        if (!processingEnv.typeUtils.isAssignable(elementType, genericPolicyIdentifierType)) {
            printError(
                element,
                "@PolicyDefinition can only be applied to $policyIdentifierType, it was applied to $elementType."
            )
            return null
        }

        if (elementType.typeArguments.size != 1) {
            printError(
                element, "Only expected 1 type parameter in $elementType"
            )
            return null
        }

        val policyType = policyType(element)

        // Temporary check until the API is rolled out. Later other module should be able to use @PolicyDefinition.
        if (!processingEnv.typeUtils.isAssignable(enclosingType, genericPolicyIdentifierType)) {
            printError(
                element,
                "@PolicyDefinition can only be applied to fields in $policyIdentifierType, it was applied to a field in $enclosingType."
            )
        }

        val allMetadata = listOfNotNull(
            booleanProcessor.process(element), enumProcessor.process(element)
        )

        if (allMetadata.isEmpty()) {
            printError(
                element, "@PolicyDefinition has no type specific definition."
            )
            return null
        }

        if (allMetadata.size > 1) {
            printError(
                element, "@PolicyDefinition must only have one type specific annotation."
            )
            return null
        }

        val name = "$enclosingType.$element"
        val documentation = processingEnv.elementUtils.getDocComment(element)
        val type = policyType.toString()
        val metadata = allMetadata.first()

        return Policy(name, type, documentation, metadata)
    }
}

data class Policy(
    val name: String,
    val type: String,
    val documentation: String,
    val metadata: PolicyMetadata
    val name: String, val type: String, val documentation: String, val metadata: PolicyMetadata
) {
    fun dump(writer: JsonWriter) {
        writer.beginObject()
        writer.apply {
            beginObject()

        writer.name("name")
        writer.value(name)
            name("name")
            value(name)

        writer.name("type")
        writer.value(type)
            name("type")
            value(type)

        writer.name("documentation")
        writer.value(documentation)
            name("documentation")
            value(documentation)

            metadata.dump(writer)

        writer.endObject()
            endObject()
        }
    }
}

@@ -46,12 +143,6 @@ abstract class PolicyMetadata() {
    abstract fun dump(writer: JsonWriter)
}

class BooleanPolicyMetadata() : PolicyMetadata() {
    override fun dump(writer: JsonWriter) {
        // Nothing to include for BooleanPolicyMetadata.
    }
}

fun dumpJSON(writer: JsonWriter, items: List<Policy>) {
    writer.beginArray()

+3 −251

File changed.

Preview size limit exceeded, changes collapsed.

+53 −0
Original line number Diff line number Diff line
/*
* 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.
*/

package android.processor.devicepolicy

import javax.annotation.processing.ProcessingEnvironment
import javax.lang.model.element.Element
import javax.lang.model.type.DeclaredType
import javax.lang.model.type.TypeMirror
import javax.tools.Diagnostic

open class Processor(protected val processingEnv: ProcessingEnvironment) {
    /**
     * Given an element that represents a PolicyIdentifier field, get the type of the policy.
     */
    protected fun policyType(element: Element): TypeMirror {
        val elementType = element.asType() as DeclaredType

        if (elementType.typeArguments.size != 1) {
            printError(
                element, "Only expected 1 type parameter in $elementType"
            )

            throw IllegalArgumentException("Element $element is not a policy")
        }

        return elementType.typeArguments[0]
    }

    /**
     * Print an error and make compilation fail.
     */
    protected fun printError(element: Element, message: String) {
        processingEnv.messager.printMessage(
            Diagnostic.Kind.ERROR,
            message,
            element,
        )
    }
}
 No newline at end of file