Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Select an option

  • Save ashokvarmamatta/1f81c79e5a02cbfa50689fb8e4907619 to your computer and use it in GitHub Desktop.

Select an option

Save ashokvarmamatta/1f81c79e5a02cbfa50689fb8e4907619 to your computer and use it in GitHub Desktop.
Android AppFunctions — make your app a tool Gemini can call on-device (MCP for the phone's AI). Real code, with my device-info app ANTAR wired up as the example: gradle, @appfunction, the 2026 Android agent stack.

🧩 Android AppFunctions — Make Your App a Tool Gemini Can Call (On-Device MCP)

Your app stops being something the user opens, and becomes something the phone's AI operates

Android Kotlin AppFunctions Status On-Device


⚡ Current on the latest. AppFunctions just landed — Android 16+, rolling out on Galaxy S26 / Pixel 10. Here's how it works end-to-end, and how it slots into my own device-info app ANTAR to make it agent-ready for when the Gemini integration opens past preview. Thoughts and questions welcome in the comments.


📖 What this covers

  • What AppFunctions actually is, and why "on-device MCP" is the right way to think about it
  • The mental model: MCP (cloud tools) vs AppFunctions (on-device tools)
  • Real, copy-pasteable code — Gradle setup, a working @AppFunction, serializable types, the app wiring, runtime enable/disable, and how to verify it landed
  • Where it fits in the bigger 2026 Android agent stack (Gemini Nano, ADK for Android, Firebase AI Logic)
  • The honest gaps — what's public vs private-preview today

🤔 Wait — what's MCP, and why "on-device"?

If you've wired up the Model Context Protocol (MCP), you know the shape: you give an AI assistant a set of tools (functions it can call), and the model decides when to call them. MCP normally runs those tools on a server, over the network.

AppFunctions is the same idea, except the tools live inside your Android app and run on the phone. Google describes it directly: AppFunctions lets your app "act as an on-device Model Context Protocol (MCP) server."

Think of it like this: MCP is a USB port for a cloud AI. AppFunctions is a USB port for the AI that's already on your phone.

Take ANTAR, my device-info app. Normally the user opens it and taps to the Battery screen. With AppFunctions, the phone's AI can just ask ANTAR:

   Without AppFunctions                With AppFunctions
   ────────────────────                ──────────────────
   User → opens ANTAR                  User → "Hey Gemini, what's my
        → taps to Battery                     battery health?"
        → reads the value               Gemini → finds ANTAR's @AppFunction
                                               → calls getBatteryInfo() ON DEVICE
                                               → ANTAR returns level, temp, health

Your app goes from a thing the user navigates to a thing the assistant can drive.

MCP vs AppFunctions, side by side

🧩 AppFunctions ☁️ Standard MCP
Where tools run On the device (OS-level hooks) On a cloud server
Network None — local call Round-trip every call
Latency Low Network-bound
Data Stays on the phone Leaves the device
Platform Android only Platform-agnostic
You maintain Annotated functions in your app A separate service

🏗️ The 2026 Android agent stack (where AppFunctions sits)

AppFunctions is the tools layer. It clicks together with the rest of what Google shipped around Google I/O '26:

        ┌─────────────────────────────────────────┐
        │  ADK for Android (0.1.0)                 │  ← agent orchestration
        │  defines agents, on-device + hybrid       │
        └───────────────┬───────────────────────────┘
                        │ runs on
        ┌───────────────▼───────────────────────────┐
        │  Gemini Nano  (via AICore / ML Kit GenAI)  │  ← the on-device model
        └───────────────┬───────────────────────────┘
                        │ calls your tools via
        ┌───────────────▼───────────────────────────┐
        │  AppFunctions  (@AppFunction in YOUR app)  │  ← YOU build this
        └─────────────────────────────────────────────┘
        ┌─────────────────────────────────────────────┐
        │  Firebase AI Logic — PREFER_ON_DEVICE /       │  ← routes to cloud
        │  PREFER_CLOUD when the local model isn't enough│     when needed
        └─────────────────────────────────────────────┘

The part you own is the bottom box: the functions. Everything above is plumbing Google provides.


🔨 Build it — real code

Everything in this section is from the official AppFunctions guide and is accurate to 1.0.0-alpha09. I've kept it copy-pasteable.

Step 1 — Gradle setup

compileSdk must be 36+. Add the three artifacts and the KSP aggregation flag:

// app/build.gradle.kts

ksp {
    arg("appfunctions:aggregateAppFunctions", "true")
}

dependencies {
    implementation("androidx.appfunctions:appfunctions:1.0.0-alpha09")
    implementation("androidx.appfunctions:appfunctions-service:1.0.0-alpha09")
    ksp("androidx.appfunctions:appfunctions-compiler:1.0.0-alpha09")
}

Step 2 — Declare a function the agent can call

Two annotations do the work:

  • @AppFunction — marks a suspend function as agent-callable. First parameter is always AppFunctionContext.
  • @AppFunctionSerializable — marks the parameter/return data classes so they can cross the boundary.

isDescribedByKDoc = true is the clever bit: your KDoc comment becomes the description the agent reads. So write the doc like you're explaining the function to the assistant.

Here it is for ANTAR — exposing the battery snapshot. The annotations and setup are exactly the official pattern; I've just pointed the function at ANTAR's existing battery repository:

package com.ashes.dev.works.system.core.internals.antar.appfunctions

import androidx.appfunctions.AppFunctionContext
import androidx.appfunctions.AppFunctionSerializable
import androidx.appfunctions.service.AppFunction
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import javax.inject.Inject

class DeviceFunctions @Inject constructor(
    private val batteryRepository: BatteryRepository
) {

    /** A snapshot of the device's battery. */
    @AppFunctionSerializable(isDescribedByKDoc = true)
    data class BatteryInfo(
        /** Charge level, 0–100. */
        val levelPercent: Int,
        /** Battery temperature in Celsius. */
        val temperatureC: Float,
        /** Health, e.g. "Good", "Overheat". */
        val health: String,
        /** True while the device is charging. */
        val isCharging: Boolean
    )

    /**
     * Returns the current battery status of this device — charge level,
     * temperature, health, and whether it's charging.
     */
    @AppFunction(isDescribedByKDoc = true)
    suspend fun getBatteryInfo(
        appFunctionContext: AppFunctionContext
    ): BatteryInfo = withContext(Dispatchers.IO) {
        batteryRepository.snapshot()   // ANTAR's existing battery read
    }
}

That's the whole provider side. The KDoc on getBatteryInfo is what tells Gemini when to call it; the BatteryInfo fields are what it gets back. (For functions that take input, the framework also gives you typed exceptions like AppFunctionInvalidArgumentException so a bad call returns a structured error, not a stack trace.)

Step 3 — Wire it into your Application

The compiler aggregates your functions; you hand the framework a factory for the enclosing class. Here it is with Hilt:

import android.app.Application
import androidx.appfunctions.service.AppFunctionConfiguration
import dagger.hilt.android.HiltAndroidApp
import javax.inject.Inject

@HiltAndroidApp
class AntarApp : Application(), AppFunctionConfiguration.Provider {

    @Inject lateinit var deviceFunctions: DeviceFunctions

    override val appFunctionConfiguration: AppFunctionConfiguration
        get() = AppFunctionConfiguration.Builder()
            .addEnclosingClassFactory(DeviceFunctions::class.java) { deviceFunctions }
            .build()
}

Step 4 — Turn functions on/off at runtime

You can ship a function disabled and flip it on when a feature/entitlement unlocks. The compiler generates an Ids constant per function (e.g. DeviceFunctionsIds.GET_BATTERY_INFO_ID):

import androidx.appfunctions.AppFunctionManager

suspend fun enableBatteryInfo(context: Context) {
    AppFunctionManager.getInstance(context).setAppFunctionEnabled(
        DeviceFunctionsIds.GET_BATTERY_INFO_ID,
        AppFunctionManager.APP_FUNCTION_STATE_ENABLED
    )
}

Declare the default state on the annotation itself with @AppFunction(isEnabled = false, isDescribedByKDoc = true).

Step 5 — Verify it actually registered ✅

Before you wonder why Gemini can't see it, confirm the OS indexed it:

adb shell cmd app_function list-app-functions | grep --after-context 10 <your.package.name>

If your function shows up here, the provider side is done.


🤖 Who calls it?

Today the caller is Gemini. That integration is rolling out on Android 16+ (Galaxy S26, Pixel 10) with an Early Access Program open — so you build and register your functions now, and Gemini calls them on supported devices. The public docs focus on this provider side (the code above), which is the part you own.


🎯 How you'd actually apply it

The natural first candidates are the clean, typed "do one thing" functions you probably already have in a repository layer. They map almost one-to-one onto @AppFunction:

Your app Functions you'd expose What the user can now say
📝 Notes/tasks createTask, searchTasks "Add a task to call the dentist"
🔋 Device-info getBatteryHealth, getStorageUsage "How's my battery health?"
🖼️ Gallery searchPhotos, sharePhoto "Find my photos from Goa"
🚕 Rides bookRide, getEta "Book a ride home"

You're not building a chatbot. You're exposing the actions your app already does, and letting the phone's assistant trigger them.


📊 Status — June 2026 (this is moving fast)

Thing State
Library androidx.appfunctions:1.0.0-alpha09
compileSdk 36+
Min device OS Android 16+
Gemini integration Private preview · Galaxy S26 / Pixel 10 · EAP open
API stability Experimental — expect changes

❓ FAQ

Does my app need the CAMERA / network / special permissions to use this? No special permission to declare functions. The function runs as your app, with whatever access your app already has.

Is this the same as App Actions / App Shortcuts? AppFunctions is the modern successor. App Actions used shortcuts.xml + built-in intents to map voice phrases to features; AppFunctions exposes typed, agent-discoverable functions instead.

Can I use this without Hilt? Yes — Hilt is just how the sample provides the factory. You implement AppFunctionConfiguration.Provider and return the enclosing-class factory however you construct it.

Will it run on my non-Pixel/non-S26 phone? You can build/register against the API on Android 16+, but the Gemini-calls-your-function loop is private preview on those flagships for now.


🔗 Sources (all checked June 2026)


🧩 Build the function. Let the phone's AI press the button.

Ashok Varma Matta

On-device AI on Android · adopting the latest as it ships

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment