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.settingslib.metadata
18 
19 import java.util.TreeMap
20 import javax.annotation.processing.AbstractProcessor
21 import javax.annotation.processing.ProcessingEnvironment
22 import javax.annotation.processing.RoundEnvironment
23 import javax.lang.model.SourceVersion
24 import javax.lang.model.element.AnnotationMirror
25 import javax.lang.model.element.AnnotationValue
26 import javax.lang.model.element.Element
27 import javax.lang.model.element.ElementKind
28 import javax.lang.model.element.ExecutableElement
29 import javax.lang.model.element.Modifier
30 import javax.lang.model.element.TypeElement
31 import javax.lang.model.type.TypeMirror
32 import javax.tools.Diagnostic
33 
34 /** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
35 class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
36     private val screens = TreeMap<String, ConstructorType>()
37     private val overlays = mutableMapOf<String, String>()
<lambda>null38     private val contextType: TypeMirror by lazy {
39         processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
40     }
41 
42     private var options: Map<String, Any?>? = null
43     private lateinit var annotationElement: TypeElement
44     private lateinit var optionsElement: TypeElement
45     private lateinit var screenType: TypeMirror
46 
getSupportedAnnotationTypesnull47     override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
48 
49     override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
50 
51     override fun init(processingEnv: ProcessingEnvironment) {
52         super.init(processingEnv)
53         val elementUtils = processingEnv.elementUtils
54         annotationElement = elementUtils.getTypeElement(ANNOTATION)
55         optionsElement = elementUtils.getTypeElement(OPTIONS)
56         screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
57     }
58 
processnull59     override fun process(
60         annotations: MutableSet<out TypeElement>,
61         roundEnv: RoundEnvironment,
62     ): Boolean {
63         roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
64             if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
65             options =
66                 annotationMirrors
67                     .single { it.isElement(optionsElement) }
68                     .elementValues
69                     .entries
70                     .associate { it.key.simpleName.toString() to it.value.value }
71         }
72         for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
73             (element as? TypeElement)?.process()
74         }
75         if (roundEnv.processingOver()) codegen()
76         return false
77     }
78 
processnull79     private fun TypeElement.process() {
80         if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
81             error("@$ANNOTATION_NAME must be added to non abstract class", this)
82             return
83         }
84         if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
85             error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
86             return
87         }
88         val constructorType = getConstructorType()
89         if (constructorType == null) {
90             error(
91                 "Class must be an object, or has single public constructor that " +
92                     "accepts no parameter or a Context parameter",
93                 this,
94             )
95             return
96         }
97         val screenQualifiedName = qualifiedName.toString()
98         screens[screenQualifiedName] = constructorType
99         val annotation = annotationMirrors.single { it.isElement(annotationElement) }
100         val overlay = annotation.getOverlay()
101         if (overlay != null) {
102             overlays.put(overlay, screenQualifiedName)?.let {
103                 error("$overlay has been overlaid by $it", this)
104             }
105         }
106     }
107 
codegennull108     private fun codegen() {
109         val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
110         if (collector.isEmpty()) return
111         val parts = collector.split('/')
112         if (parts.size == 3) {
113             generateCode(parts[0], parts[1], parts[2])
114         } else {
115             throw IllegalArgumentException(
116                 "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
117             )
118         }
119     }
120 
generateCodenull121     private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
122         for ((overlay, screen) in overlays) {
123             if (screens.remove(overlay) == null) {
124                 warn("$overlay is overlaid by $screen but not annotated with @$ANNOTATION_NAME")
125             } else {
126                 processingEnv.messager.printMessage(
127                     Diagnostic.Kind.NOTE,
128                     "$overlay is overlaid by $screen",
129                 )
130             }
131         }
132         processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
133             it.write("package $outputPkg;\n\n")
134             it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n\n")
135             it.write("// Generated by annotation processor for @$ANNOTATION_NAME\n")
136             it.write("public final class $outputClass {\n")
137             it.write("  private $outputClass() {}\n\n")
138             it.write(
139                 "  public static java.util.List<$PREFERENCE_SCREEN_METADATA> " +
140                     "$outputFun(android.content.Context context) {\n"
141             )
142             it.write(
143                 "    java.util.ArrayList<$PREFERENCE_SCREEN_METADATA> screens = " +
144                     "new java.util.ArrayList<>(${screens.size});\n"
145             )
146             for ((screen, constructorType) in screens) {
147                 when (constructorType) {
148                     ConstructorType.DEFAULT -> it.write("    screens.add(new $screen());\n")
149                     ConstructorType.CONTEXT -> it.write("    screens.add(new $screen(context));\n")
150                     ConstructorType.SINGLETON -> it.write("    screens.add($screen.INSTANCE);\n")
151                 }
152             }
153             for ((overlay, screen) in overlays) {
154                 it.write("    // $overlay is overlaid by $screen\n")
155             }
156             it.write("    return screens;\n")
157             it.write("  }\n")
158             it.write("}")
159         }
160     }
161 
AnnotationMirrornull162     private fun AnnotationMirror.isElement(element: TypeElement) =
163         processingEnv.typeUtils.isSameType(annotationType.asElement().asType(), element.asType())
164 
165     private fun AnnotationMirror.getOverlay(): String? {
166         for ((key, value) in elementValues) {
167             if (key.simpleName.contentEquals("overlay")) {
168                 return if (value.isDefaultClassValue(key)) null else value.value.toString()
169             }
170         }
171         return null
172     }
173 
AnnotationValuenull174     private fun AnnotationValue.isDefaultClassValue(key: ExecutableElement) =
175         processingEnv.typeUtils.isSameType(
176             value as TypeMirror,
177             key.defaultValue.value as TypeMirror,
178         )
179 
180     private fun TypeElement.getConstructorType(): ConstructorType? {
181         var constructor: ExecutableElement? = null
182         for (element in enclosedElements) {
183             if (element.isKotlinObject()) return ConstructorType.SINGLETON
184             if (element.kind != ElementKind.CONSTRUCTOR) continue
185             if (!element.modifiers.contains(Modifier.PUBLIC)) continue
186             if (constructor != null) return null
187             constructor = element as ExecutableElement
188         }
189         return constructor?.parameters?.run {
190             when {
191                 isEmpty() -> ConstructorType.DEFAULT
192                 size == 1 && processingEnv.typeUtils.isSameType(this[0].asType(), contextType) ->
193                     ConstructorType.CONTEXT
194                 else -> null
195             }
196         }
197     }
198 
Elementnull199     private fun Element.isKotlinObject() =
200         kind == ElementKind.FIELD &&
201             modifiers.run { contains(Modifier.PUBLIC) && contains(Modifier.STATIC) } &&
202             simpleName.toString() == "INSTANCE"
203 
warnnull204     private fun warn(msg: CharSequence) =
205         processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
206 
207     private fun error(msg: CharSequence, element: Element) =
208         processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
209 
210     private enum class ConstructorType {
211         DEFAULT, // default constructor with no parameter
212         CONTEXT, // constructor with a Context parameter
213         SINGLETON, // Kotlin object class
214     }
215 
216     companion object {
217         private const val PACKAGE = "com.android.settingslib.metadata"
218         private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
219         private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
220         private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
221 
222         private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
223         private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
224         private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
225     }
226 }
227