AppFunctions allow an Android app to expose functionality that can then be used by AI agents and assistants running on the device. With the recent announcement of Android MCP (which is powered by the AppFunctions API and allows Gemini to make use of functionality in your app) I thought it was a good time to look at adding AppFunctions support to the Confetti conference app. This article will outline the changes made to do that (included in this PR).

The idea is to allow an agent to answer questions about the sessions in the conference that’s currently selected in Confetti and also to bookmark/unbookmark sessions on behalf of the user. We’ll expose 4 functions to do that: getConference, listSessions, addBookmark and removeBookmark.

Dependencies

We’re using 1.0.0-beta01 of the AppFunctions library. We need the following dependencies (along with the KSP plugin which is used to generate code and metadata from our annotated functions). Note also that AppFunctions requires compileSdk 37 and targetSdk 36 or later.

libs.versions.toml

[versions]
...
androidx-appfunctions = "1.0.0-beta01"

[libraries]
...
androidx-appfunctions = { module = "androidx.appfunctions:appfunctions", version.ref = "androidx-appfunctions" }
androidx-appfunctions-compiler = { module = "androidx.appfunctions:appfunctions-compiler", version.ref = "androidx-appfunctions" }

build.gradle.kts

implementation(libs.androidx.appfunctions)
ksp(libs.androidx.appfunctions.compiler)

Implementation

The functions are declared in an abstract class extending AppFunctionService and annotated with @AppFunctionServiceEntryPoint. KSP then generates the concrete service (ConfettiAppFunctionService in our case) along with the XML schema that describes the functions to the system. Each function is annotated with @AppFunctionDeclaration. Confetti uses Koin so we just make the class a KoinComponent and inject the existing repositories as we would elsewhere (the functions themselves just call in to existing repository code).

The following shows how those pieces fit together.

AppFunctions overview

@RequiresApi(36)
@AppFunctionServiceEntryPoint(
    serviceName = "ConfettiAppFunctionService",
    appFunctionXmlFileName = "confetti_app_function_service",
)
abstract class BaseConfettiAppFunctionService : AppFunctionService(), KoinComponent {
    private val repository: ConfettiRepository by inject()
    private val appSettings: AppSettings by inject()
    private val authentication: Authentication by inject()

    /**
     * Lists every session in the currently selected conference.
     *
     * @return All sessions with their ID, title, description and start/end times.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun listSessions(): List<SessionResponse> = withContext(Dispatchers.IO) {
        val conference = requireConference()
        val user = authentication.currentUser.value
        val response = repository.sessions(conference, user?.uid, user, FetchPolicy.CacheFirst)
        response.data?.sessions?.nodes?.map { it.sessionDetails.toResponse() } ?: emptyList()
    }

    /**
     * Bookmarks a session in the currently selected conference. Requires the user to be
     * signed in to Confetti.
     *
     * @param sessionId ID of the session to bookmark, as returned in [SessionResponse.id].
     * @return true if the bookmark was saved.
     */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    suspend fun addBookmark(sessionId: String): Boolean = withContext(Dispatchers.IO) {
        val conference = requireConference()
        val user = requireSignedInUser()
        repository.addBookmark(conference, user.uid, user, sessionId)
    }

    // getConference, removeBookmark and helper functions omitted
    ...
}

Note that isDescribedByKDoc = true means the KDoc is what gets passed to the LLM….so it’s worth spending a bit of time on those descriptions. The same applies to @AppFunctionSerializable classes (where the KDoc needs to be inline on each property). Session times are returned as LocalDateTime which is one of the types the library natively supports.

/** A session (talk or workshop) in a conference. */
@AppFunctionSerializable(isDescribedByKDoc = true)
data class SessionResponse(
    /** Unique ID of the session, used to add or remove a bookmark. */
    val id: String,
    /** Title of the session. */
    val title: String,
    /** Abstract describing the session content, if provided. */
    val description: String?,
    /** Start time in the conference's local time zone. */
    val startsAt: LocalDateTime,
    /** End time in the conference's local time zone. */
    val endsAt: LocalDateTime,
)

We also need to register the generated service in AndroidManifest.xml…

AndroidManifest.xml

<service
    android:name="dev.johnoreilly.confetti.functions.ConfettiAppFunctionService"
    android:permission="android.permission.BIND_APP_FUNCTION_SERVICE"
    android:exported="true"
    tools:targetApi="36">
    <property
        android:name="android.app.appfunctions.schema"
        android:value="app_functions_schema.xsd" />
    <property
        android:name="android.app.appfunctions.v2"
        android:value="confetti_app_function_service.xml" />
    <intent-filter>
        <action android:name="android.app.appfunctions.AppFunctionService" />
    </intent-filter>
</service>
<property
    android:name="android.app.appfunctions.app_metadata"
    android:resource="@xml/app_metadata" />

…along with an app_metadata.xml file which provides an overall description of the app (including, for example, how functions relate to each other).

app_metadata.xml

<AppFunctionAppMetadata xmlns:appfn="http://schemas.android.com/apk/androidx.appfunctions"
    appfn:instructions="Confetti is a conference schedule app: it lists the sessions of the conference the user has selected and manages the user's session bookmarks.
Operational patterns:
- 'listSessions' returns the session IDs that 'addBookmark' and 'removeBookmark' take.
- All functions act on the currently selected conference, which 'getConference' returns.
Constraints:
- A conference must already be selected in the app; switching conference is not supported.
- Adding or removing bookmarks requires the user to be signed in to Confetti." />

Testing

The quickest way to test is from the command line using adb.

adb shell cmd app_function list-app-functions --package dev.johnoreilly.confetti

adb shell "cmd app_function execute-app-function --package dev.johnoreilly.confetti \
  --function 'dev.johnoreilly.confetti.functions.BaseConfettiAppFunctionService#getConference' \
  --parameters '{}' --brief-yaml"

I also used the AppFunction Testing Agent app from the android/appfunctions repo. The Debug tab there shows the functions it has discovered for an app and allows you to run them directly.

Testing Agent debug tab

And the Agent Demo tab lets you interact with those functions using Gemini. In the following, with droidcon London 2026 selected in Confetti, I asked which sessions were about agents. Gemini called getConference and listSessions and then grouped the relevant sessions by day/time.

Testing Agent - list sessions

I then asked it to bookmark one of those sessions (with Gemini working out the session id itself from the earlier listSessions result). That bookmark then showed up as expected in the Confetti Bookmarks tab.

Testing Agent - add bookmark


Next step will be trying this out with Gemini using Android MCP once I’m able to. I’m also interested in looking at the experimental support for returning A2UI content from AppFunctions (something used for example in the ChatApp sample in that same android/appfunctions repo).

The code shown here is part of the androidApp module in the Confetti repository.