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.AppFunctionMetadataDocument 23 import com.google.devtools.ksp.processing.CodeGenerator 24 import com.google.devtools.ksp.processing.Dependencies 25 import com.google.devtools.ksp.processing.Resolver 26 import com.google.devtools.ksp.processing.SymbolProcessor 27 import com.google.devtools.ksp.symbol.KSAnnotated 28 import javax.xml.parsers.DocumentBuilderFactory 29 import javax.xml.transform.OutputKeys 30 import javax.xml.transform.TransformerFactory 31 import javax.xml.transform.dom.DOMSource 32 import javax.xml.transform.stream.StreamResult 33 import org.w3c.dom.Document 34 import org.w3c.dom.Element 35 36 /** 37 * Generates AppFunction's index xml file for the legacy AppSearch indexer to index. 38 * 39 * The generator would write an XML file as `/assets/app_functions.xml`. The file would be packaged 40 * into the APK's asset when assembled. So that the AppSearch indexer can look up the asset and 41 * inject metadata into platform AppSearch database accordingly. 42 * 43 * The new indexer will index additional properties based on the schema defined in SDK instead of 44 * the pre-defined one in AppSearch. 45 */ 46 class AppFunctionLegacyIndexXmlProcessor( 47 private val codeGenerator: CodeGenerator, 48 ) : SymbolProcessor { 49 50 override fun process(resolver: Resolver): List<KSAnnotated> { 51 val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver) 52 val resolvedAnnotatedSerializableProxies = 53 ResolvedAnnotatedSerializableProxies( 54 appFunctionSymbolResolver.resolveAllAnnotatedSerializableProxiesFromModule() 55 ) 56 generateLegacyIndexXml( 57 appFunctionSymbolResolver.getAnnotatedAppFunctionsFromAllModules(), 58 resolvedAnnotatedSerializableProxies 59 ) 60 return emptyList() 61 } 62 63 /** 64 * Generates AppFunction's legacy index xml files for v1 indexer in App Search. 65 * 66 * @param appFunctionsByClass a collection of functions annotated with @AppFunction grouped by 67 * their enclosing classes. 68 */ 69 private fun generateLegacyIndexXml( 70 appFunctionsByClass: List<AnnotatedAppFunctions>, 71 resolvedAnnotatedSerializableProxies: ResolvedAnnotatedSerializableProxies 72 ) { 73 if (appFunctionsByClass.isEmpty()) { 74 return 75 } 76 val appFunctionMetadataList = 77 appFunctionsByClass.flatMap { 78 it.createAppFunctionMetadataList(resolvedAnnotatedSerializableProxies).map { 79 it.toAppFunctionMetadataDocument() 80 } 81 } 82 writeXmlFile(appFunctionMetadataList, appFunctionsByClass) 83 } 84 85 private fun writeXmlFile( 86 appFunctionMetadataList: List<AppFunctionMetadataDocument>, 87 appFunctionsByClass: List<AnnotatedAppFunctions>, 88 ) { 89 val xmlDocumentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder() 90 val xmlDocument = xmlDocumentBuilder.newDocument().apply { xmlStandalone = true } 91 92 val appFunctionsElement = xmlDocument.createElement(XmlElement.APP_FUNCTIONS_ELEMENTS_TAG) 93 xmlDocument.appendChild(appFunctionsElement) 94 95 for (appFunctionMetadata in appFunctionMetadataList) { 96 appFunctionsElement.appendChild( 97 xmlDocument.createAppFunctionElement(appFunctionMetadata) 98 ) 99 } 100 101 val transformer = 102 TransformerFactory.newInstance().newTransformer().apply { 103 setOutputProperty(OutputKeys.INDENT, "yes") 104 setOutputProperty(OutputKeys.ENCODING, "UTF-8") 105 setOutputProperty(OutputKeys.VERSION, "1.0") 106 setOutputProperty(OutputKeys.STANDALONE, "yes") 107 } 108 109 codeGenerator 110 .createNewFile( 111 Dependencies( 112 aggregating = true, 113 *appFunctionsByClass.flatMap { it.getSourceFiles() }.toTypedArray() 114 ), 115 XML_PACKAGE_NAME, 116 XML_FILE_NAME, 117 XML_EXTENSION 118 ) 119 .use { stream -> transformer.transform(DOMSource(xmlDocument), StreamResult(stream)) } 120 } 121 122 private fun Document.createAppFunctionElement( 123 appFunctionMetadata: AppFunctionMetadataDocument 124 ): Element = 125 createElement(XmlElement.APP_FUNCTION_ITEM_TAG).apply { 126 appendChild( 127 createElementWithTextNode(XmlElement.APP_FUNCTION_ID_TAG, appFunctionMetadata.id) 128 ) 129 130 val schemaName = appFunctionMetadata.schemaName 131 val schemaCategory = appFunctionMetadata.schemaCategory 132 val schemaVersion = appFunctionMetadata.schemaVersion 133 if (schemaName != null && schemaCategory != null && schemaVersion != null) { 134 appendChild( 135 createElementWithTextNode( 136 XmlElement.APP_FUNCTION_SCHEMA_CATEGORY_TAG, 137 schemaCategory, 138 ) 139 ) 140 appendChild( 141 createElementWithTextNode( 142 XmlElement.APP_FUNCTION_SCHEMA_NAME_TAG, 143 schemaName, 144 ) 145 ) 146 appendChild( 147 createElementWithTextNode( 148 XmlElement.APP_FUNCTION_SCHEMA_VERSION_TAG, 149 schemaVersion.toString(), 150 ) 151 ) 152 } 153 appendChild( 154 createElementWithTextNode( 155 XmlElement.APP_FUNCTION_ENABLE_BY_DEFAULT_TAG, 156 appFunctionMetadata.isEnabledByDefault.toString(), 157 ) 158 ) 159 } 160 161 private fun Document.createElementWithTextNode(elementName: String, text: String): Element = 162 createElement(elementName).apply { appendChild(createTextNode(text)) } 163 164 private companion object { 165 private const val XML_PACKAGE_NAME = "assets" 166 private const val XML_FILE_NAME = "app_functions" 167 private const val XML_EXTENSION = "xml" 168 169 private object XmlElement { 170 const val APP_FUNCTIONS_ELEMENTS_TAG = "appfunctions" 171 const val APP_FUNCTION_ITEM_TAG = "appfunction" 172 const val APP_FUNCTION_ID_TAG = "function_id" 173 const val APP_FUNCTION_SCHEMA_CATEGORY_TAG = "schema_category" 174 const val APP_FUNCTION_SCHEMA_NAME_TAG = "schema_name" 175 const val APP_FUNCTION_SCHEMA_VERSION_TAG = "schema_version" 176 const val APP_FUNCTION_ENABLE_BY_DEFAULT_TAG = "enabled_by_default" 177 } 178 } 179 } 180