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