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 androidx.annotation.IntDef
22 import androidx.annotation.RequiresApi
23 import androidx.annotation.RequiresPermission
24 import androidx.appfunctions.internal.AppFunctionManagerApi
25 import androidx.appfunctions.internal.AppFunctionReader
26 import androidx.appfunctions.internal.AppSearchAppFunctionReader
27 import androidx.appfunctions.internal.Dependencies
28 import androidx.appfunctions.internal.ExtensionAppFunctionManagerApi
29 import androidx.appfunctions.internal.TranslatorSelector
30 import androidx.appfunctions.metadata.AppFunctionMetadata
31 import androidx.appfunctions.metadata.AppFunctionSchemaMetadata
32 import com.android.extensions.appfunctions.AppFunctionManager
33 import kotlinx.coroutines.flow.Flow
34 
35 /**
36  * Provides access to interact with App Functions. This is a backward-compatible wrapper for the
37  * platform class [android.app.appfunctions.AppFunctionManager].
38  */
39 @RequiresApi(Build.VERSION_CODES.TIRAMISU)
40 public class AppFunctionManagerCompat
41 internal constructor(
42     private val context: Context,
43     private val translatorSelector: TranslatorSelector = Dependencies.translatorSelector,
44     private val appFunctionReader: AppFunctionReader = AppSearchAppFunctionReader(context),
45     private val appFunctionManagerApi: AppFunctionManagerApi =
46         ExtensionAppFunctionManagerApi(context)
47 ) {
48 
49     /**
50      * Checks if [functionId] in the caller's package is enabled.
51      *
52      * This method matches the platform behavior defined in
53      * [android.app.appfunctions.AppFunctionManager.isAppFunctionEnabled].
54      *
55      * @param functionId The identifier of the app function.
56      * @throws IllegalArgumentException If the [functionId] is not available in caller's package.
57      */
isAppFunctionEnablednull58     public suspend fun isAppFunctionEnabled(functionId: String): Boolean {
59         return isAppFunctionEnabled(packageName = context.packageName, functionId = functionId)
60     }
61 
62     /**
63      * Checks if [functionId] in [packageName] is enabled.
64      *
65      * This method matches the platform behavior defined in
66      * [android.app.appfunctions.AppFunctionManager.isAppFunctionEnabled].
67      *
68      * @param packageName The package name of the owner of [functionId].
69      * @param functionId The identifier of the app function.
70      * @throws IllegalArgumentException If the [functionId] is not available under [packageName].
71      */
72     @RequiresPermission(value = "android.permission.EXECUTE_APP_FUNCTIONS", conditional = true)
isAppFunctionEnablednull73     public suspend fun isAppFunctionEnabled(packageName: String, functionId: String): Boolean {
74         return appFunctionManagerApi.isAppFunctionEnabled(
75             packageName = packageName,
76             functionId = functionId
77         )
78     }
79 
80     /**
81      * Sets [newEnabledState] to an app function [functionId] owned by the calling package.
82      *
83      * This method matches the platform behavior defined in
84      * [android.app.appfunctions.AppFunctionManager.setAppFunctionEnabled].
85      *
86      * @param functionId The identifier of the app function.
87      * @param newEnabledState The new state of the app function.
88      * @throws IllegalArgumentException If the [functionId] is not available.
89      */
90     @RequiresPermission(value = "android.permission.EXECUTE_APP_FUNCTIONS", conditional = true)
setAppFunctionEnablednull91     public suspend fun setAppFunctionEnabled(
92         functionId: String,
93         @EnabledState newEnabledState: Int
94     ) {
95         return appFunctionManagerApi.setAppFunctionEnabled(functionId, newEnabledState)
96     }
97 
98     /**
99      * Execute the app function.
100      *
101      * This method matches the platform behavior defined in
102      * [android.app.appfunctions.AppFunctionManager.executeAppFunction].
103      *
104      * @param request the app function details and the arguments.
105      * @return the result of the attempt to execute the function.
106      */
107     @RequiresPermission(value = "android.permission.EXECUTE_APP_FUNCTIONS", conditional = true)
executeAppFunctionnull108     public suspend fun executeAppFunction(
109         request: ExecuteAppFunctionRequest,
110     ): ExecuteAppFunctionResponse {
111 
112         val schemaMetadata: AppFunctionSchemaMetadata? =
113             try {
114                 appFunctionReader.getAppFunctionSchemaMetadata(
115                     functionId = request.functionIdentifier,
116                     packageName = request.targetPackageName
117                 )
118             } catch (ex: AppFunctionFunctionNotFoundException) {
119                 return ExecuteAppFunctionResponse.Error(ex)
120             } catch (ex: Exception) {
121                 return ExecuteAppFunctionResponse.Error(
122                     AppFunctionSystemUnknownException(
123                         "Something went wrong when querying the app function from AppSearch: ${ex.message}"
124                     )
125                 )
126             }
127 
128         // Translate the request when necessary by looking into the target schema version.
129         val translator =
130             if (schemaMetadata?.version == LEGACY_SDK_GLOBAL_SCHEMA_VERSION) {
131                 checkNotNull(translatorSelector.getTranslator(schemaMetadata))
132             } else {
133                 null
134             }
135         val translatedRequest: ExecuteAppFunctionRequest =
136             if (translator != null) {
137                 val functionParametersToExecute =
138                     translator.downgradeRequest(request.functionParameters)
139                 request.copy(functionParameters = functionParametersToExecute)
140             } else {
141                 request
142             }
143 
144         val executeAppFunctionResponse = appFunctionManagerApi.executeAppFunction(translatedRequest)
145 
146         // Translate the response back to what the agent app expects.
147         val successResponse =
148             executeAppFunctionResponse as? ExecuteAppFunctionResponse.Success
149                 ?: return executeAppFunctionResponse
150         return if (translator != null) {
151             successResponse.copy(translator.upgradeResponse(successResponse.returnValue))
152         } else {
153             successResponse
154         }
155     }
156 
157     /**
158      * Observes for available app functions metadata based on the provided filters.
159      *
160      * Allows discovering app functions that match the given [searchSpec] criteria and continuously
161      * emits updates when relevant metadata changes. The calling app can only observe metadata for
162      * functions in packages that it is allowed to query via
163      * [android.content.pm.PackageManager.canPackageQuery]. If a package is not queryable by the
164      * calling app, its functions' metadata will not be visible.
165      *
166      * Updates to [AppFunctionMetadata] can occur when the app defining the function is updated or
167      * when a function's enabled state changes.
168      *
169      * If multiple updates happen within a short duration, only the latest update might be emitted.
170      *
171      * @param searchSpec an [AppFunctionSearchSpec] instance specifying the filters for searching
172      *   the app function metadata.
173      * @return a flow that emits a list of [AppFunctionMetadata] matching the search criteria and
174      *   updated versions of this list when underlying data changes.
175      */
176     @RequiresPermission(value = "android.permission.EXECUTE_APP_FUNCTIONS", conditional = true)
observeAppFunctionsnull177     public fun observeAppFunctions(
178         searchSpec: AppFunctionSearchSpec
179     ): Flow<List<AppFunctionMetadata>> {
180         return appFunctionReader.searchAppFunctions(searchSpec)
181     }
182 
183     @IntDef(
184         value =
185             [APP_FUNCTION_STATE_DEFAULT, APP_FUNCTION_STATE_ENABLED, APP_FUNCTION_STATE_DISABLED]
186     )
187     @Retention(AnnotationRetention.SOURCE)
188     internal annotation class EnabledState
189 
190     public companion object {
191         /**
192          * The default state of the app function. Call [setAppFunctionEnabled] with this to reset
193          * enabled state to the default value.
194          */
195         public const val APP_FUNCTION_STATE_DEFAULT: Int =
196             AppFunctionManager.APP_FUNCTION_STATE_DEFAULT
197         /**
198          * The app function is enabled. To enable an app function, call [setAppFunctionEnabled] with
199          * this value.
200          */
201         public const val APP_FUNCTION_STATE_ENABLED: Int =
202             AppFunctionManager.APP_FUNCTION_STATE_ENABLED
203         /**
204          * The app function is disabled. To disable an app function, call [setAppFunctionEnabled]
205          * with this value.
206          */
207         public const val APP_FUNCTION_STATE_DISABLED: Int =
208             AppFunctionManager.APP_FUNCTION_STATE_DISABLED
209 
210         /** The version shared across all schema defined in the legacy SDK. */
211         private const val LEGACY_SDK_GLOBAL_SCHEMA_VERSION = 1L
212 
213         /**
214          * Checks whether the AppFunction feature is supported.
215          *
216          * Support is determined by verifying if the device implements the App Functions extension
217          * library
218          *
219          * @return `true` if the AppFunctions feature is supported on this device, `false`
220          *   otherwise.
221          */
isSupportednull222         private fun isSupported(): Boolean {
223             // TODO(b/395589225): Check isSupported based on SDK version and update the document.
224             return try {
225                 Class.forName("com.android.extensions.appfunctions.AppFunctionManager")
226                 true
227             } catch (_: ClassNotFoundException) {
228                 false
229             }
230         }
231 
232         /**
233          * Gets an instance of [AppFunctionManagerCompat] if the AppFunction feature is supported.
234          *
235          * Support is determined by verifying if the device implements the App Functions extension
236          * library.
237          *
238          * @return an instance of [AppFunctionManagerCompat] if the AppFunction feature is supported
239          *   or `null`.
240          */
241         @JvmStatic
getInstancenull242         public fun getInstance(context: Context): AppFunctionManagerCompat? =
243             if (isSupported()) {
244                 AppFunctionManagerCompat(context)
245             } else {
246                 null
247             }
248     }
249 }
250