1 /*
2  * 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.AppFunctionCompiler
20 import androidx.appfunctions.compiler.AppFunctionCompilerOptions
21 import androidx.appfunctions.compiler.core.AppFunctionSymbolResolver
22 import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTIONS_INTERNAL_PACKAGE_NAME
23 import androidx.appfunctions.compiler.core.IntrospectionHelper.APP_FUNCTION_INVENTORY_CLASS
24 import androidx.appfunctions.compiler.core.IntrospectionHelper.AggregatedAppFunctionInventoryClass
25 import androidx.appfunctions.compiler.core.IntrospectionHelper.AggregatedAppFunctionInvokerClass
26 import androidx.appfunctions.compiler.core.IntrospectionHelper.AppFunctionInvokerClass
27 import androidx.appfunctions.compiler.core.toClassName
28 import com.google.devtools.ksp.processing.CodeGenerator
29 import com.google.devtools.ksp.processing.Dependencies
30 import com.google.devtools.ksp.processing.Resolver
31 import com.google.devtools.ksp.processing.SymbolProcessor
32 import com.google.devtools.ksp.symbol.KSAnnotated
33 import com.google.devtools.ksp.symbol.KSClassDeclaration
34 import com.squareup.kotlinpoet.FileSpec
35 import com.squareup.kotlinpoet.KModifier
36 import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
37 import com.squareup.kotlinpoet.PropertySpec
38 import com.squareup.kotlinpoet.TypeSpec
39 import com.squareup.kotlinpoet.asClassName
40 import com.squareup.kotlinpoet.buildCodeBlock
41 
42 /**
43  * The processor generates the implementation of AggregatedAppFunctionInventory and
44  * AggregatedAppFunctionInvoker.
45  *
46  * The processor would only start aggregation process when
47  * * [AppFunctionCompilerOptions.aggregateAppFunctions] is true.
48  * * AND there is no remaining @AppFunction nodes to processed.
49  */
50 class AppFunctionAggregateProcessor(
51     private val options: AppFunctionCompilerOptions,
52     private val codeGenerator: CodeGenerator,
53 ) : SymbolProcessor {
54 
55     private var hasProcessed = false
56 
processnull57     override fun process(resolver: Resolver): List<KSAnnotated> {
58         if (!options.aggregateAppFunctions) {
59             return emptyList()
60         }
61 
62         if (hasProcessed) {
63             return emptyList()
64         }
65 
66         if (!shouldProcess(resolver)) {
67             return emptyList()
68         }
69 
70         generateAggregatedAppFunctionInventory(resolver)
71         generateAggregatedAppFunctionInvoker(resolver)
72         generateAggregatedIndexXml(resolver)
73 
74         hasProcessed = true
75         return emptyList()
76     }
77 
generateAggregatedAppFunctionInventorynull78     private fun generateAggregatedAppFunctionInventory(resolver: Resolver) {
79         val generatedInventories =
80             AppFunctionSymbolResolver(resolver).getGeneratedAppFunctionInventories()
81         val aggregatedInventoryClassName =
82             "${'$'}${AggregatedAppFunctionInventoryClass.CLASS_NAME.simpleName}_Impl"
83 
84         val aggregatedInventoryClassBuilder = TypeSpec.classBuilder(aggregatedInventoryClassName)
85         aggregatedInventoryClassBuilder.superclass(AggregatedAppFunctionInventoryClass.CLASS_NAME)
86         aggregatedInventoryClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
87         aggregatedInventoryClassBuilder.addProperty(buildInventoriesProperty(generatedInventories))
88 
89         val fileSpec =
90             FileSpec.builder(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, aggregatedInventoryClassName)
91                 .addType(aggregatedInventoryClassBuilder.build())
92                 .build()
93 
94         codeGenerator
95             .createNewFile(
96                 // TODO: Collect all AppFunction files as source files set
97                 Dependencies.ALL_FILES,
98                 APP_FUNCTIONS_INTERNAL_PACKAGE_NAME,
99                 aggregatedInventoryClassName
100             )
101             .bufferedWriter()
102             .use { fileSpec.writeTo(it) }
103     }
104 
buildInventoriesPropertynull105     private fun buildInventoriesProperty(
106         generatedInventories: List<KSClassDeclaration>
107     ): PropertySpec {
108         return PropertySpec.builder(
109                 AggregatedAppFunctionInventoryClass.PROPERTY_INVENTORIES_NAME,
110                 List::class.asClassName().parameterizedBy(APP_FUNCTION_INVENTORY_CLASS)
111             )
112             .addModifiers(KModifier.OVERRIDE)
113             .initializer(
114                 buildCodeBlock {
115                     addStatement("listOf(")
116                     indent()
117                     for (generatedInventory in generatedInventories) {
118                         addStatement("%T(),", generatedInventory.toClassName())
119                     }
120                     unindent()
121                     addStatement(")")
122                 }
123             )
124             .build()
125     }
126 
generateAggregatedAppFunctionInvokernull127     private fun generateAggregatedAppFunctionInvoker(resolver: Resolver) {
128         val generatedInvokers =
129             AppFunctionSymbolResolver(resolver).getGeneratedAppFunctionInvokers()
130         val aggregatedInvokerClassName =
131             "${'$'}${AggregatedAppFunctionInvokerClass.CLASS_NAME.simpleName}_Impl"
132 
133         val aggregatedInvokerClassBuilder = TypeSpec.classBuilder(aggregatedInvokerClassName)
134         aggregatedInvokerClassBuilder.superclass(AggregatedAppFunctionInvokerClass.CLASS_NAME)
135         aggregatedInvokerClassBuilder.addAnnotation(AppFunctionCompiler.GENERATED_ANNOTATION)
136         aggregatedInvokerClassBuilder.addProperty(buildInvokersProperty(generatedInvokers))
137 
138         val fileSpec =
139             FileSpec.builder(APP_FUNCTIONS_INTERNAL_PACKAGE_NAME, aggregatedInvokerClassName)
140                 .addType(aggregatedInvokerClassBuilder.build())
141                 .build()
142 
143         codeGenerator
144             .createNewFile(
145                 // TODO: Collect all AppFunction files as source files set
146                 Dependencies.ALL_FILES,
147                 APP_FUNCTIONS_INTERNAL_PACKAGE_NAME,
148                 aggregatedInvokerClassName
149             )
150             .bufferedWriter()
151             .use { fileSpec.writeTo(it) }
152     }
153 
buildInvokersPropertynull154     private fun buildInvokersProperty(generatedInvokers: List<KSClassDeclaration>): PropertySpec {
155         return PropertySpec.builder(
156                 AggregatedAppFunctionInvokerClass.PROPERTY_INVOKERS_NAME,
157                 List::class.asClassName().parameterizedBy(AppFunctionInvokerClass.CLASS_NAME)
158             )
159             .addModifiers(KModifier.OVERRIDE)
160             .initializer(
161                 buildCodeBlock {
162                     addStatement("listOf(")
163                     indent()
164                     for (generatedInvoker in generatedInvokers) {
165                         addStatement("%T(),", generatedInvoker.toClassName())
166                     }
167                     unindent()
168                     addStatement(")")
169                 }
170             )
171             .build()
172     }
173 
generateAggregatedIndexXmlnull174     private fun generateAggregatedIndexXml(resolver: Resolver) {
175         // We generate both XML formats supported by old and new AppSearch indexer respectively
176         // as it can't be guaranteed that the device will have the latest version of AppSearch.
177         // TODO: Add compiler option to disable legacy xml generator.
178         val legacyIndexProcessor = AppFunctionLegacyIndexXmlProcessor(codeGenerator)
179         legacyIndexProcessor.process(resolver)
180 
181         val indexProcessor = AppFunctionIndexXmlProcessor(codeGenerator)
182         indexProcessor.process(resolver)
183     }
184 
shouldProcessnull185     private fun shouldProcess(resolver: Resolver): Boolean {
186         val appFunctionSymbolResolver = AppFunctionSymbolResolver(resolver)
187         val appFunctionClasses = appFunctionSymbolResolver.resolveAnnotatedAppFunctions()
188         return appFunctionClasses.isEmpty()
189     }
190 }
191