Skip to content

Instantly share code, notes, and snippets.

@gptshubham595
Last active April 26, 2026 10:05
Show Gist options
  • Select an option

  • Save gptshubham595/c1b2b3206c3e2c464b911af0fbbfa9aa to your computer and use it in GitHub Desktop.

Select an option

Save gptshubham595/c1b2b3206c3e2c464b911af0fbbfa9aa to your computer and use it in GitHub Desktop.
AppFunctions Fallback ToolProvider
/**
* 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