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