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

Commit db15a931 authored by Victor Gabriel Savu's avatar Victor Gabriel Savu Committed by Android (Google) Code Review
Browse files

Merge changes I7aa85d8b,Ide540de9,I25b847ab into main

* changes:
  Split up annotation processing
  Add tests for enums in PolicyProcessorTest
  Introduce enum policy definitions
parents 0ec51676 93d7a5e5
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -51,9 +51,10 @@ java_test_host {
filegroup {
    name: "DevicePolicyAnnotationProcessorTestSource",
    srcs: [
        // We need the java sources for compiler tests.
        ":framework-metalava-annotations",
        "test/resources/**/*.java",
        "test/resources/**/*.json",
    ],
    path: "test/resources/",
    visibility: ["//visibility:private"],
}
+32 −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;

/**
 * Metadata for a enum policy. Represented by an Integer.
 */
public @interface EnumPolicyDefinition {
    /**
     * Indicates which IntDef represents this enum.
     */
    Class<?> intDef();

    /**
     * Indicates the default value of this policy when unset or cleared.
     */
    int defaultValue();
}
+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
+256 −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 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.apply {
            beginObject()

            name("name")
            value(name)

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

            name("documentation")
            value(documentation)

            endObject()
        }
    }
}

class EnumPolicyMetadata(
    val defaultValue: Int,
    val enum: String,
    val enumDocumentation: String,
    val entries: List<EnumEntryMetadata>
) : PolicyMetadata() {
    override fun dump(writer: JsonWriter) {
        writer.apply {
            name("default")
            value(defaultValue.toLong())

            name("enum")
            value(enum)

            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()

Loading