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