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
18 
19 import android.content.Context
20 import android.os.Build
21 import android.os.OutcomeReceiver
22 import android.util.Log
23 import androidx.annotation.RequiresApi
24 import androidx.appfunctions.internal.AggregatedAppFunctionInventory
25 import androidx.appfunctions.internal.AggregatedAppFunctionInvoker
26 import androidx.appfunctions.internal.Constants.APP_FUNCTIONS_TAG
27 import androidx.appfunctions.internal.Translator
28 import androidx.appfunctions.internal.TranslatorSelector
29 import androidx.appfunctions.internal.unsafeBuildReturnValue
30 import androidx.appfunctions.internal.unsafeGetParameterValue
31 import androidx.appfunctions.metadata.AppFunctionSchemaMetadata
32 import androidx.appfunctions.metadata.CompileTimeAppFunctionMetadata
33 import kotlin.coroutines.CoroutineContext
34 import kotlinx.coroutines.CancellationException
35 import kotlinx.coroutines.CoroutineScope
36 import kotlinx.coroutines.Job
37 import kotlinx.coroutines.launch
38 import kotlinx.coroutines.withContext
39 
40 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
41 internal class AppFunctionServiceDelegate(
42     context: Context,
43     workerCoroutineContext: CoroutineContext,
44     private val mainCoroutineContext: CoroutineContext,
45     private val aggregatedInventory: AggregatedAppFunctionInventory,
46     private val aggregatedInvoker: AggregatedAppFunctionInvoker,
47     private val translatorSelector: TranslatorSelector
48 ) {
49 
50     private val job = Job()
51     private val workerCoroutineScope = CoroutineScope(workerCoroutineContext + job)
52     private val appContext = context.applicationContext
53 
onExecuteFunctionnull54     internal fun onExecuteFunction(
55         executeAppFunctionRequest: ExecuteAppFunctionRequest,
56         callingPackageName: String,
57         callback: OutcomeReceiver<ExecuteAppFunctionResponse, AppFunctionException>,
58     ): Job =
59         workerCoroutineScope.launch {
60             try {
61                 val appFunctionMetadata =
62                     aggregatedInventory.functionIdToMetadataMap[
63                             executeAppFunctionRequest.functionIdentifier]
64                 if (appFunctionMetadata == null) {
65                     Log.d(
66                         APP_FUNCTIONS_TAG,
67                         "${executeAppFunctionRequest.functionIdentifier} is not available"
68                     )
69                     callback.onError(
70                         AppFunctionFunctionNotFoundException(
71                             "${executeAppFunctionRequest.functionIdentifier} is not available"
72                         )
73                     )
74                     return@launch
75                 }
76                 val translator =
77                     getTranslator(executeAppFunctionRequest, appFunctionMetadata.schema)
78 
79                 val parameters =
80                     extractParameters(executeAppFunctionRequest, appFunctionMetadata, translator)
81                 callback.onResult(
82                     unsafeInvokeFunction(
83                         executeAppFunctionRequest,
84                         callingPackageName,
85                         appFunctionMetadata,
86                         parameters,
87                         translator
88                     )
89                 )
90             } catch (e: CancellationException) {
91                 callback.onError(AppFunctionCancelledException(e.message))
92             } catch (e: AppFunctionException) {
93                 callback.onError(e)
94             } catch (e: Exception) {
95                 callback.onError(AppFunctionAppUnknownException(e.message))
96             }
97         }
98 
onDestroynull99     internal fun onDestroy() {
100         job.cancel()
101     }
102 
getTranslatornull103     private fun getTranslator(
104         request: ExecuteAppFunctionRequest,
105         schemaMetadata: AppFunctionSchemaMetadata?
106     ): Translator? {
107         if (request.useJetpackSchema) {
108             return null
109         }
110         return schemaMetadata?.let { translatorSelector.getTranslator(it) }
111     }
112 
extractParametersnull113     private fun extractParameters(
114         request: ExecuteAppFunctionRequest,
115         appFunctionMetadata: CompileTimeAppFunctionMetadata,
116         translator: Translator?
117     ): Map<String, Any?> {
118         // Upgrade the parameters from the agents, if they are using the old format.
119         val translatedParameters =
120             translator?.upgradeRequest(request.functionParameters) ?: request.functionParameters
121 
122         return buildMap {
123             for (parameterMetadata in appFunctionMetadata.parameters) {
124                 this[parameterMetadata.name] =
125                     translatedParameters.unsafeGetParameterValue(parameterMetadata)
126             }
127         }
128     }
129 
unsafeInvokeFunctionnull130     private suspend fun unsafeInvokeFunction(
131         request: ExecuteAppFunctionRequest,
132         callingPackageName: String,
133         appFunctionMetadata: CompileTimeAppFunctionMetadata,
134         parameters: Map<String, Any?>,
135         translator: Translator?
136     ): ExecuteAppFunctionResponse {
137         val result =
138             withContext(mainCoroutineContext) {
139                 aggregatedInvoker.unsafeInvoke(
140                     buildAppFunctionContext(callingPackageName),
141                     request.functionIdentifier,
142                     parameters
143                 )
144             }
145         val returnValue = appFunctionMetadata.response.unsafeBuildReturnValue(result)
146         // Downgrade the return value from the agents, if they are using the old format.
147         val translatedReturnValue = translator?.downgradeResponse(returnValue) ?: returnValue
148         return ExecuteAppFunctionResponse.Success(translatedReturnValue)
149     }
150 
buildAppFunctionContextnull151     private fun buildAppFunctionContext(callingPackageName: String): AppFunctionContext {
152         return object : AppFunctionContext {
153             override val context: Context
154                 get() = appContext
155 
156             override val callingPackageName: String
157                 get() = callingPackageName
158         }
159     }
160 }
161