Last active
April 26, 2026 10:05
-
-
Save gptshubham595/c1b2b3206c3e2c464b911af0fbbfa9aa to your computer and use it in GitHub Desktop.
AppFunctions Fallback ToolProvider
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * Provides AppFunction metadata to an external agent via ContentResolver.call(). | |
| * | |
| * This is a fallback approach: metadata ideally should be queried by the agent directly, but | |
| * some devices/builds may require fetching it inside the tool app process [6]. | |
| */ | |
| class ToolProvider : ContentProvider() { | |
| companion object { | |
| private const val TAG = "ComposeTodoToolProvider" | |
| // Must match the manifest authorities value exactly. | |
| const val AUTHORITY = "com.shubham.todojetpackcompose.appfunctions.provider" | |
| val CONTENT_URI: Uri = "content://$AUTHORITY".toUri() | |
| // ContentResolver.call() method names | |
| const val METHOD_START = "start" | |
| const val METHOD_GET_METADATA = "get_metadata" | |
| const val METHOD_EXECUTE = "execute" | |
| // Bundle key | |
| const val KEY_METADATA_JSON = "metadata_json" | |
| const val KEY_FUNCTION_ID = "function_id" | |
| const val KEY_ARGUMENTS_JSON = "arguments_json" | |
| const val KEY_RESULT_JSON = "result_json" | |
| const val KEY_ERROR = "error" | |
| } | |
| private val metadataItems = | |
| AtomicReference<List<AppFunctionPackageMetadata>>(emptyList()) | |
| private val gson by lazy { | |
| GsonBuilder() | |
| .registerTypeAdapter( | |
| AppFunctionDataTypeMetadata::class.java, | |
| AppFunctionDataTypeMetadataAdapter(), | |
| ) | |
| .create() | |
| } | |
| override fun onCreate(): Boolean { | |
| refreshMetadataSnapshot() | |
| return true | |
| } | |
| override fun call(method: String, arg: String?, extras: Bundle?): Bundle? { | |
| Log.i(TAG, "call(method=$method)") | |
| return when (method) { | |
| METHOD_START -> { | |
| refreshMetadataSnapshot() | |
| null | |
| } | |
| METHOD_GET_METADATA -> { | |
| if (metadataItems.get().isEmpty()) { | |
| refreshMetadataSnapshot() | |
| } | |
| Bundle().apply { | |
| putString(KEY_METADATA_JSON, gson.toJson(metadataItems.get())) | |
| } | |
| } | |
| METHOD_EXECUTE -> executeAppFunction(extras) | |
| else -> throw UnsupportedOperationException("Unknown method: $method") | |
| } | |
| } | |
| private fun executeAppFunction(extras: Bundle?): Bundle { | |
| val functionId = extras?.getString(KEY_FUNCTION_ID) | |
| ?: throw IllegalArgumentException("Missing $KEY_FUNCTION_ID") | |
| val argumentsJson = extras.getString(KEY_ARGUMENTS_JSON).orEmpty() | |
| val appContext = context?.applicationContext | |
| ?: throw IllegalStateException("Context is unavailable") | |
| return runCatching { | |
| val result = runBlocking { | |
| `$TodoAppFunctions_AppFunctionInvoker`().unsafeInvoke( | |
| appFunctionContext = object : androidx.appfunctions.AppFunctionContext { | |
| override val context = appContext | |
| }, | |
| functionIdentifier = functionId, | |
| parameters = parseExecutionArguments(argumentsJson), | |
| ) | |
| } | |
| Bundle().apply { | |
| putString(KEY_RESULT_JSON, gson.toJson(result)) | |
| } | |
| }.getOrElse { error -> | |
| Bundle().apply { | |
| putString(KEY_ERROR, error.message ?: error.toString()) | |
| } | |
| } | |
| } | |
| private fun parseExecutionArguments(argumentsJson: String): Map<String, Any?> { | |
| if (argumentsJson.isBlank()) return emptyMap() | |
| val root = JsonParser.parseString(argumentsJson) | |
| if (!root.isJsonObject) { | |
| throw IllegalArgumentException("Execution arguments must be a JSON object.") | |
| } | |
| return root.asJsonObject.entrySet().associate { (key, value) -> | |
| key to value.toNativeValue() | |
| } | |
| } | |
| private fun JsonElement.toNativeValue(): Any? = when { | |
| isJsonNull -> null | |
| isJsonPrimitive -> { | |
| val primitive = asJsonPrimitive | |
| when { | |
| primitive.isBoolean -> primitive.asBoolean | |
| primitive.isString -> primitive.asString | |
| primitive.isNumber -> { | |
| val raw = primitive.asString | |
| when { | |
| raw.contains('.') || raw.contains('e', ignoreCase = true) -> primitive.asDouble | |
| else -> raw.toLongOrNull() ?: primitive.asDouble | |
| } | |
| } | |
| else -> primitive.asString | |
| } | |
| } | |
| isJsonArray -> asJsonArray.map { it.toNativeValue() } | |
| isJsonObject -> asJsonObject.entrySet().associate { (key, value) -> key to value.toNativeValue() } | |
| else -> gson.fromJson(this, Any::class.java) | |
| } | |
| private fun refreshMetadataSnapshot() { | |
| val pkg = context?.packageName ?: return | |
| val inventory = `$TodoAppFunctions_AppFunctionInventory`() | |
| val sharedComponents = inventory.componentsMetadata | |
| val appFunctions = inventory.functionIdToMetadataMap.values | |
| .map { metadata -> | |
| AppFunctionMetadata( | |
| metadata.id, | |
| pkg, | |
| metadata.isEnabledByDefault, | |
| metadata.schema, | |
| metadata.parameters, | |
| metadata.response, | |
| sharedComponents, | |
| metadata.description.ifBlank { metadata.id.substringAfterLast("#") }, | |
| metadata.deprecation, | |
| ) | |
| } | |
| .sortedBy { it.id } | |
| val snapshot = listOf( | |
| AppFunctionPackageMetadata( | |
| packageName = pkg, | |
| appFunctions = appFunctions, | |
| ) | |
| ) | |
| metadataItems.set(snapshot) | |
| notifyChange() | |
| Log.i(TAG, "Loaded AppFunction metadata: functions=${appFunctions.size}") | |
| } | |
| private fun notifyChange() { | |
| context?.contentResolver?.notifyChange(CONTENT_URI, null) | |
| } | |
| // Not used; this provider only supports call() | |
| override fun query( | |
| uri: Uri, | |
| projection: Array<out String>?, | |
| selection: String?, | |
| selectionArgs: Array<out String>?, | |
| sortOrder: String?, | |
| ): Cursor = throw UnsupportedOperationException() | |
| override fun insert(uri: Uri, values: ContentValues?): Uri = | |
| throw UnsupportedOperationException() | |
| override fun delete(uri: Uri, selection: String?, selectionArgs: Array<out String>?): Int = | |
| throw UnsupportedOperationException() | |
| override fun update( | |
| uri: Uri, | |
| values: ContentValues?, | |
| selection: String?, | |
| selectionArgs: Array<out String>?, | |
| ): Int = throw UnsupportedOperationException() | |
| override fun getType(uri: Uri): String = | |
| throw UnsupportedOperationException() | |
| } | |
| /** | |
| * Enables Gson to deserialize AppFunctionDataTypeMetadata polymorphically by storing the class name. | |
| */ | |
| class AppFunctionDataTypeMetadataAdapter : | |
| JsonSerializer<AppFunctionDataTypeMetadata>, | |
| JsonDeserializer<AppFunctionDataTypeMetadata> { | |
| companion object { | |
| private const val CLASSNAME = "DATATYPE_CLASS" | |
| } | |
| override fun serialize( | |
| src: AppFunctionDataTypeMetadata, | |
| typeOfSrc: Type, | |
| context: JsonSerializationContext, | |
| ): JsonElement { | |
| val json = context.serialize(src) | |
| if (json.isJsonObject) { | |
| json.asJsonObject.addProperty(CLASSNAME, src.javaClass.name) | |
| } | |
| return json | |
| } | |
| override fun deserialize( | |
| json: JsonElement, | |
| typeOfT: Type, | |
| context: JsonDeserializationContext, | |
| ): AppFunctionDataTypeMetadata { | |
| val obj = json.asJsonObject | |
| val className = obj.get(CLASSNAME).asString | |
| obj.remove(CLASSNAME) | |
| return try { | |
| val clazz = Class.forName(className) | |
| context.deserialize(obj, clazz) | |
| } catch (e: ClassNotFoundException) { | |
| throw JsonParseException(e) | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment