1 /* <lambda>null2 * Copyright 2025 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 androidx.appfunctions.compiler.processors 18 19 import androidx.appfunctions.compiler.core.AnnotatedAppFunctionSerializableProxy.ResolvedAnnotatedSerializableProxies 20 import androidx.appfunctions.compiler.core.AnnotatedAppFunctions 21 import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver 22 import androidx.appfunctions.metadata.AppFunctionComponentsMetadata 23 import androidx.appfunctions.metadata.AppFunctionDataTypeMetadata 24 import androidx.appfunctions.metadata.CompileTimeAppFunctionMetadata 25 import com.google.devtools.ksp.processing.CodeGenerator 26 import com.google.devtools.ksp.processing.Dependencies 27 import com.google.devtools.ksp.processing.Resolver 28 import com.google.devtools.ksp.processing.SymbolProcessor 29 import com.google.devtools.ksp.symbol.KSAnnotated 30 import javax.xml.parsers.DocumentBuilderFactory 31 import javax.xml.transform.OutputKeys 32 import javax.xml.transform.TransformerFactory 33 import javax.xml.transform.dom.DOMSource 34 import javax.xml.transform.stream.StreamResult 35 import kotlin.reflect.KProperty1 36 import org.w3c.dom.Document 37 import org.w3c.dom.Element 38 39 /** 40 * Generates AppFunction's index xml file with all properties of [CompileTimeAppFunctionMetadata] 41 * for the AppSearch indexer to index. 42 * 43 * The generator would write an XML file as `/assets/app_functions_dynamic_schema.xml`. The file 44 * would be packaged into the APK's asset when assembled, so that the AppSearch indexer can look up 45 * the asset and inject metadata into platform AppSearch database accordingly. 46 */ 47 class AppFunctionIndexXmlProcessor( 48 private val codeGenerator: CodeGenerator, 49 ) : SymbolProcessor { 50 51 override fun process(resolver: Resolver): List<KSAnnotated> { 52 val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver) 53 val resolvedAnnotatedSerializableProxies = 54 ResolvedAnnotatedSerializableProxies( 55 appFunctionSymbolResolver.resolveAllAnnotatedSerializableProxiesFromModule() 56 ) 57 generateIndexXml( 58 appFunctionSymbolResolver.getAnnotatedAppFunctionsFromAllModules(), 59 resolvedAnnotatedSerializableProxies 60 ) 61 return emptyList() 62 } 63 64 /** 65 * Generates AppFunction's index xml files for indexer in App Search. 66 * 67 * @param appFunctionsByClass a collection of functions annotated with @AppFunction 68 * @param resolvedAnnotatedSerializableProxies a collection of resolved annotated serializable 69 * proxies 70 */ 71 private fun generateIndexXml( 72 appFunctionsByClass: List<AnnotatedAppFunctions>, 73 resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies 74 ) { 75 if (appFunctionsByClass.isEmpty()) { 76 return 77 } 78 writeXmlFile(appFunctionsByClass, resolvedAnnotatedSerializableProxies) 79 } 80 81 private fun writeXmlFile( 82 appFunctionsByClass: List<AnnotatedAppFunctions>, 83 resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies 84 ) { 85 val appFunctionMetadataList = 86 appFunctionsByClass.flatMap { 87 it.createAppFunctionMetadataList(resolvedAnnotatedSerializableProxies) 88 } 89 90 val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() 91 val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true } 92 93 val appFunctionsElement = xmlDocument.createElement(APP_FUNCTIONS_ELEMENTS_TAG) 94 xmlDocument.appendChild(appFunctionsElement) 95 96 val aggregatedDataTypes: MutableMap<String, AppFunctionDataTypeMetadata> = mutableMapOf() 97 for (appFunctionMetadata in appFunctionMetadataList) { 98 appFunctionMetadata.components.dataTypes.forEach { (objectKey, dataTypeMetadata) -> 99 aggregatedDataTypes.putIfAbsent(objectKey, dataTypeMetadata) 100 } 101 val sanitizedAppFunctionMetadata = 102 appFunctionMetadata.copy(components = AppFunctionComponentsMetadata()) 103 104 val appFunctionElement = 105 xmlDocument.createElementWithInstance( 106 APP_FUNCTION_ITEM_TAG, 107 sanitizedAppFunctionMetadata.toAppFunctionMetadataDocument(), 108 // Below properties are named differently in platform's 109 // AppFunctionStaticMetadata GD hence we encode them in XML accordingly. 110 customTagNames = mapOf("isEnabledByDefault" to "enabledByDefault"), 111 // Irrelevant properties that do not need to be encoded in XML. 112 skipProperties = setOf("namespace") 113 ) 114 appFunctionElement.appendChild( 115 xmlDocument.createElementWithTextNode( 116 APP_FUNCTION_ID_TAG, 117 sanitizedAppFunctionMetadata.id 118 ) 119 ) 120 appFunctionsElement.appendChild(appFunctionElement) 121 } 122 123 val componentElement = 124 xmlDocument.createElementWithInstance( 125 COMPONENT_ITEM_TAG, 126 AppFunctionComponentsMetadata(aggregatedDataTypes) 127 .toAppFunctionComponentsMetadataDocument(), 128 // Below properties are named differently in platform's 129 // AppFunctionStaticMetadata GD hence we encode them in XML accordingly. 130 customTagNames = mapOf("isEnabledByDefault" to "enabledByDefault"), 131 // Irrelevant properties that do not need to be encoded in XML. 132 skipProperties = setOf("namespace") 133 ) 134 appFunctionsElement.appendChild(componentElement) 135 136 val transformer = 137 TransformerFactory.newInstance().newTransformer().apply { 138 setOutputProperty(OutputKeys.INDENT, "yes") 139 setOutputProperty(OutputKeys.ENCODING, "UTF-8") 140 setOutputProperty(OutputKeys.VERSION, "1.0") 141 setOutputProperty(OutputKeys.STANDALONE, "yes") 142 } 143 144 codeGenerator 145 .createNewFile( 146 Dependencies( 147 aggregating = true, 148 *appFunctionsByClass.flatMap { it.getSourceFiles() }.toTypedArray() 149 ), 150 XML_PACKAGE_NAME, 151 XML_FILE_NAME, 152 XML_EXTENSION 153 ) 154 .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) } 155 } 156 157 /** 158 * Creates an XML element from [instance], including nested structures and collections. 159 * 160 * This function recursively converts a data class instance into an XML element, handling nested 161 * data classes and collections appropriately. For non-data-class values, it creates text nodes. 162 * 163 * @param elementName The name of the root XML element to create. 164 * @param instance The instance to convert into an XML structure. 165 * @param customTagNames Mapping of property names to customized tag names when creating nodes. 166 * @param skipProperties Property names to skip when creating XML elements. 167 * @return The created XML element representing the instance. 168 */ 169 private fun Document.createElementWithInstance( 170 elementName: String, 171 instance: Any, 172 customTagNames: Map<String, String>, 173 skipProperties: Set<String>, 174 ): Element { 175 if (instance.isPrimitiveType()) { 176 return createElementWithTextNode(elementName, instance.toString()) 177 } 178 179 val doc = this 180 val element = createElement(elementName) 181 182 for (property in instance::class.members.filterIsInstance<KProperty1<Any, *>>()) { 183 184 if (property.name in skipProperties) continue 185 186 val value = property.get(instance) ?: continue 187 val propertyName = customTagNames[property.name] ?: property.name 188 189 when { 190 value is List<*> -> 191 value 192 .filterNotNull() 193 .map { item -> 194 doc.createElementWithInstance( 195 propertyName, 196 item, 197 customTagNames, 198 skipProperties 199 ) 200 } 201 .forEach(element::appendChild) 202 else -> 203 element.appendChild( 204 doc.createElementWithInstance( 205 propertyName, 206 value, 207 customTagNames, 208 skipProperties 209 ) 210 ) 211 } 212 } 213 214 return element 215 } 216 217 private fun Any.isPrimitiveType(): Boolean { 218 return this is Byte || 219 this is Short || 220 this is Int || 221 this is Long || 222 this is Float || 223 this is Double || 224 this is Char || 225 this is Boolean || 226 this is String 227 } 228 229 private fun Document.createElementWithTextNode(elementName: String, text: String): Element = 230 createElement(elementName).apply { appendChild(createTextNode(text)) } 231 232 private companion object { 233 const val XML_PACKAGE_NAME = "assets" 234 const val XML_FILE_NAME = "app_functions_v2" 235 const val XML_EXTENSION = "xml" 236 const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions" 237 const val APP_FUNCTION_ITEM_TAG = "appfunction" 238 const val COMPONENT_ITEM_TAG = "components" 239 const val APP_FUNCTION_ID_TAG = "functionId" 240 } 241 } 242