• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 javax.annotation.processing.AbstractProcessor
20 import javax.annotation.processing.ProcessingEnvironment
21 import javax.annotation.processing.RoundEnvironment
22 import javax.lang.model.SourceVersion
23 import javax.lang.model.element.AnnotationMirror
24 import javax.lang.model.element.AnnotationValue
25 import javax.lang.model.element.Element
26 import javax.lang.model.element.ElementKind
27 import javax.lang.model.element.ExecutableElement
28 import javax.lang.model.element.Modifier
29 import javax.lang.model.element.TypeElement
30 import javax.lang.model.type.TypeMirror
31 import javax.tools.Diagnostic
32 
33 /** Processor to gather preference screens annotated with `@ProvidePreferenceScreen`. */
34 class PreferenceScreenAnnotationProcessor : AbstractProcessor() {
35     private val screens = mutableListOf<Screen>()
<lambda>null36     private val bundleType: TypeMirror by lazy {
37         processingEnv.elementUtils.getTypeElement("android.os.Bundle").asType()
38     }
<lambda>null39     private val contextType: TypeMirror by lazy {
40         processingEnv.elementUtils.getTypeElement("android.content.Context").asType()
41     }
42 
43     private var options: Map<String, Any?>? = null
44     private lateinit var annotationElement: TypeElement
45     private lateinit var optionsElement: TypeElement
46     private lateinit var screenType: TypeMirror
47 
getSupportedAnnotationTypesnull48     override fun getSupportedAnnotationTypes() = setOf(ANNOTATION, OPTIONS)
49 
50     override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latestSupported()
51 
52     override fun init(processingEnv: ProcessingEnvironment) {
53         super.init(processingEnv)
54         val elementUtils = processingEnv.elementUtils
55         annotationElement = elementUtils.getTypeElement(ANNOTATION)
56         optionsElement = elementUtils.getTypeElement(OPTIONS)
57         screenType = elementUtils.getTypeElement("$PACKAGE.$PREFERENCE_SCREEN_METADATA").asType()
58     }
59 
processnull60     override fun process(
61         annotations: MutableSet<out TypeElement>,
62         roundEnv: RoundEnvironment,
63     ): Boolean {
64         roundEnv.getElementsAnnotatedWith(optionsElement).singleOrNull()?.run {
65             if (options != null) error("@$OPTIONS_NAME is already specified: $options", this)
66             options =
67                 annotationMirrors
68                     .single { it.isElement(optionsElement) }
69                     .elementValues
70                     .entries
71                     .associate { it.key.simpleName.toString() to it.value.value }
72         }
73         for (element in roundEnv.getElementsAnnotatedWith(annotationElement)) {
74             (element as? TypeElement)?.process()
75         }
76         if (roundEnv.processingOver()) codegen()
77         return false
78     }
79 
processnull80     private fun TypeElement.process() {
81         if (kind != ElementKind.CLASS || modifiers.contains(Modifier.ABSTRACT)) {
82             error("@$ANNOTATION_NAME must be added to non abstract class", this)
83             return
84         }
85         if (!processingEnv.typeUtils.isAssignable(asType(), screenType)) {
86             error("@$ANNOTATION_NAME must be added to $PREFERENCE_SCREEN_METADATA subclass", this)
87             return
88         }
89         fun reportConstructorError() =
90             error(
91                 "Must have only one public constructor: constructor(), " +
92                     "constructor(Context), constructor(Bundle) or constructor(Context, Bundle)",
93                 this,
94             )
95         val constructor = findConstructor()
96         if (constructor == null || constructor.parameters.size > 2) {
97             reportConstructorError()
98             return
99         }
100         val constructorHasContextParameter = constructor.hasParameter(0, contextType)
101         var index = if (constructorHasContextParameter) 1 else 0
102         val annotation = annotationMirrors.single { it.isElement(annotationElement) }
103         val key = annotation.fieldValue<String>("value")!!
104         val overlay = annotation.fieldValue<Boolean>("overlay") == true
105         val parameterized = annotation.fieldValue<Boolean>("parameterized") == true
106         var parametersHasContextParameter = false
107         if (parameterized) {
108             val parameters = findParameters()
109             if (parameters == null) {
110                 error("require a static 'parameters()' or 'parameters(Context)' method", this)
111                 return
112             }
113             parametersHasContextParameter = parameters
114             if (constructor.hasParameter(index, bundleType)) {
115                 index++
116             } else {
117                 error(
118                     "Parameterized screen constructor must be" +
119                         "constructor(Bundle) or constructor(Context, Bundle)",
120                     this,
121                 )
122                 return
123             }
124         }
125         if (index == constructor.parameters.size) {
126             screens.add(
127                 Screen(
128                     key,
129                     overlay,
130                     parameterized,
131                     annotation.fieldValue<Boolean>("parameterizedMigration") == true,
132                     qualifiedName.toString(),
133                     constructorHasContextParameter,
134                     parametersHasContextParameter,
135                 )
136             )
137         } else {
138             reportConstructorError()
139         }
140     }
141 
codegennull142     private fun codegen() {
143         val collector = (options?.get("codegenCollector") as? String) ?: DEFAULT_COLLECTOR
144         if (collector.isEmpty()) return
145         val parts = collector.split('/')
146         if (parts.size == 3) {
147             generateCode(parts[0], parts[1], parts[2])
148         } else {
149             throw IllegalArgumentException(
150                 "Collector option '$collector' does not follow 'PKG/CLASS/METHOD' format"
151             )
152         }
153     }
154 
generateCodenull155     private fun generateCode(outputPkg: String, outputClass: String, outputFun: String) {
156         // sort by screen keys to make the output deterministic and naturally fit to FixedArrayMap
157         screens.sort()
158         processingEnv.filer.createSourceFile("$outputPkg.$outputClass").openWriter().use {
159             it.write("package $outputPkg;\n\n")
160             it.write("import android.content.Context;\n")
161             it.write("import android.os.Bundle;\n")
162             it.write("import $PACKAGE.FixedArrayMap;\n")
163             it.write("import $PACKAGE.FixedArrayMap.OrderedInitializer;\n")
164             it.write("import $PACKAGE.$PREFERENCE_SCREEN_METADATA;\n")
165             it.write("import $PACKAGE.$FACTORY;\n")
166             it.write("import $PACKAGE.$PARAMETERIZED_FACTORY;\n")
167             it.write("import kotlinx.coroutines.flow.Flow;\n")
168             it.write("\n// Generated by annotation processor for @$ANNOTATION_NAME\n")
169             it.write("public final class $outputClass {\n")
170             it.write("  private $outputClass() {}\n\n")
171             it.write("  public static FixedArrayMap<String, $FACTORY> $outputFun() {\n")
172             val size = screens.size
173             it.write("    return new FixedArrayMap<>($size, $outputClass::init);\n")
174             it.write("  }\n\n")
175             fun Screen.write() {
176                 it.write("    screens.put(\"$key\", ")
177                 if (parameterized) {
178                     it.write("new $PARAMETERIZED_FACTORY() {\n")
179                     it.write("      @Override public PreferenceScreenMetadata create")
180                     it.write("(Context context, Bundle args) {\n")
181                     it.write("        return new $klass(")
182                     if (constructorHasContextParameter) it.write("context, ")
183                     it.write("args);\n")
184                     it.write("      }\n\n")
185                     it.write("      @Override public Flow<Bundle> parameters(Context context) {\n")
186                     it.write("        return $klass.parameters(")
187                     if (parametersHasContextParameter) it.write("context")
188                     it.write(");\n")
189                     it.write("      }\n")
190                     if (parameterizedMigration) {
191                         it.write("\n      @Override public boolean acceptEmptyArguments()")
192                         it.write(" { return true; }\n")
193                     }
194                     it.write("    });")
195                 } else {
196                     it.write("context -> new $klass(")
197                     if (constructorHasContextParameter) it.write("context")
198                     it.write("));")
199                 }
200                 if (overlay) it.write(" // overlay")
201                 it.write("\n")
202             }
203             it.write("  private static void init(OrderedInitializer<String, $FACTORY> screens) {\n")
204             var index = 0
205             while (index < size) {
206                 val screen = screens[index]
207                 var next = index + 1
208                 while (next < size && screen.key == screens[next].key) next++
209                 val n = next - index
210                 if (n == 1) {
211                     screen.write()
212                 } else if (n == 2 && screen.overlay && !screens[index + 1].overlay) {
213                     it.write("    // ${screen.klass} overlays ${screens[index + 1].klass}\n")
214                     screen.write()
215                 } else {
216                     val msg = StringBuilder("${screen.key} is associated to")
217                     for (i in index until next) msg.append(" ${screens[i]}")
218                     processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg)
219                 }
220                 index = next
221             }
222             it.write("  }\n}")
223         }
224     }
225 
AnnotationMirrornull226     private fun AnnotationMirror.isElement(element: TypeElement) =
227         annotationType.asElement().asType().isSameType(element.asType())
228 
229     @Suppress("UNCHECKED_CAST")
230     private fun <T> AnnotationMirror.fieldValue(name: String): T? = field(name)?.value as? T
231 
232     private fun AnnotationMirror.field(name: String): AnnotationValue? {
233         for ((key, value) in elementValues) {
234             if (key.simpleName.contentEquals(name)) return value
235         }
236         return null
237     }
238 
findConstructornull239     private fun TypeElement.findConstructor(): ExecutableElement? {
240         var constructor: ExecutableElement? = null
241         for (element in enclosedElements) {
242             if (element.kind != ElementKind.CONSTRUCTOR) continue
243             if (!element.modifiers.contains(Modifier.PUBLIC)) continue
244             if (constructor != null) return null
245             constructor = element as ExecutableElement
246         }
247         return constructor
248     }
249 
findParametersnull250     private fun TypeElement.findParameters(): Boolean? {
251         for (element in enclosedElements) {
252             if (element.kind != ElementKind.METHOD) continue
253             if (!element.modifiers.contains(Modifier.PUBLIC)) continue
254             if (!element.modifiers.contains(Modifier.STATIC)) continue
255             if (!element.simpleName.contentEquals("parameters")) return null
256             val parameters = (element as ExecutableElement).parameters
257             if (parameters.isEmpty()) return false
258             if (parameters.size == 1 && parameters[0].asType().isSameType(contextType)) return true
259             error("parameters method should have no parameter or a Context parameter", element)
260             return null
261         }
262         return null
263     }
264 
ExecutableElementnull265     private fun ExecutableElement.hasParameter(index: Int, typeMirror: TypeMirror) =
266         index < parameters.size && parameters[index].asType().isSameType(typeMirror)
267 
268     private fun TypeMirror.isSameType(typeMirror: TypeMirror) =
269         processingEnv.typeUtils.isSameType(this, typeMirror)
270 
271     private fun warn(msg: CharSequence) =
272         processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, msg)
273 
274     private fun error(msg: CharSequence, element: Element) =
275         processingEnv.messager.printMessage(Diagnostic.Kind.ERROR, msg, element)
276 
277     private data class Screen(
278         val key: String,
279         val overlay: Boolean,
280         val parameterized: Boolean,
281         val parameterizedMigration: Boolean,
282         val klass: String,
283         val constructorHasContextParameter: Boolean,
284         val parametersHasContextParameter: Boolean,
285     ) : Comparable<Screen> {
286         override fun compareTo(other: Screen): Int {
287             val diff = key.compareTo(other.key)
288             return if (diff != 0) diff else other.overlay.compareTo(overlay)
289         }
290     }
291 
292     companion object {
293         private const val PACKAGE = "com.android.settingslib.metadata"
294         private const val ANNOTATION_NAME = "ProvidePreferenceScreen"
295         private const val ANNOTATION = "$PACKAGE.$ANNOTATION_NAME"
296         private const val PREFERENCE_SCREEN_METADATA = "PreferenceScreenMetadata"
297         private const val FACTORY = "PreferenceScreenMetadataFactory"
298         private const val PARAMETERIZED_FACTORY = "PreferenceScreenMetadataParameterizedFactory"
299 
300         private const val OPTIONS_NAME = "ProvidePreferenceScreenOptions"
301         private const val OPTIONS = "$PACKAGE.$OPTIONS_NAME"
302         private const val DEFAULT_COLLECTOR = "$PACKAGE/PreferenceScreenCollector/get"
303     }
304 }
305