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