DEV Community

Rizo
Rizo

Posted on Edited on

Bringing Android AppFunctions to Tauri with Rust

Tauri, Rust, and Android AppFunctions

Recently, I decided to explore what the built-in Gemini agent, which replaced Google Assistant, could actually do. I spent almost an hour walking around the room, asking it all sorts of questions and testing its limits.

I started with things like “go to this website and find this for me,” then tried to figure out whether it could interact with the device itself. It turned out it could work with Google apps: notes, the calendar, and the rest of the usual Google ecosystem. It could tell me how much battery was left, but not which Wi-Fi network the device was connected to. Things like that.

What I liked most was discovering that it could work with Spotify: play tracks by voice and add them to my favorites.

That got me thinking: could it interact with other services or apps through some kind of MCP? I started imagining an MCP-like mechanism specifically for apps, including native ones. I wanted to avoid having every app spin up a web server and only being able to use it while the app was running.

Something built into Android itself: a registry that Gemini could query and use according to predefined permissions. That would be pretty useful.

I went looking, and it turned out Google had already thought of it and presented it back in May this year.

I'm a fullstack developer, and I've been building desktop applications for the past two years. I wanted to combine a nice frontend with a fast, safe Rust backend, so I learned Tauri 2.0 and started using it.

Since Tauri is cross-platform, I decided to build a plugin that would make AppFunctions convenient to use from Rust, without having to switch to Kotlin for function declarations. Macros instead of annotations.

The feature is still experimental and requires Android 16 or later. So far, I've tested it through adb.

What it looks like

The plugin is called tauri-plugin-google-app-functions. Add it to an existing Tauri project:

# src-tauri/Cargo.toml
[dependencies]
tauri-plugin-google-app-functions = "0.2"

[build-dependencies]
tauri-plugin-google-app-functions-build = "0.2"
Enter fullscreen mode Exit fullscreen mode

Add schema generation to build.rs:

fn main() {
    tauri_build::build();
    tauri_plugin_google_app_functions_build::build();
}
Enter fullscreen mode Exit fullscreen mode

Then declare a regular Rust function with a macro and register the plugin:

use tauri_plugin_google_app_functions::app_function;

/// Counts words in the supplied text.
///
/// # Arguments
/// * `text` - Text whose words should be counted.
#[app_function]
fn count_words(text: String) -> i64 {
    text.split_whitespace().count() as i64
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_google_app_functions::init())
        .run(tauri::generate_context!())
        .expect("error while running the application");
}
Enter fullscreen mode Exit fullscreen mode

The doc comments become descriptions for the agent, and the arguments and return type become part of the schema. Android sees this function as countWords. Build the app through the Tauri CLI.

Rust → Kotlin

The Rust declaration above is conceptually equivalent to the following native Jetpack AppFunctions declaration:

import androidx.annotation.RequiresApi
import androidx.appfunctions.AppFunctionDeclaration
import androidx.appfunctions.AppFunctionService
import androidx.appfunctions.AppFunctionServiceEntryPoint

@RequiresApi(36)
@AppFunctionServiceEntryPoint(
    serviceName = "TextFunctionsService",
    appFunctionXmlFileName = "text_functions",
)
abstract class TextFunctions : AppFunctionService() {
    /** Counts words in the supplied text. */
    @AppFunctionDeclaration(isDescribedByKDoc = true)
    fun countWords(text: String): Long =
        text.split(Regex("(?U)\\s+"))
            .count { it.isNotEmpty() }
            .toLong()
}
Enter fullscreen mode Exit fullscreen mode

This compares two ways of declaring a function. Internally, the plugin generates an XML schema and uses a JNI bridge; the handler itself stays in Rust. You don't need to write Kotlin declarations manually for ordinary AppFunctions integration.

A music example

For the demo, I built a small Radix-based player: two tracks, play/pause, and track switching. An Android service owns playback, so AppFunctions can control it without opening the interface. The notification has playback controls too.

Switching tracks is exposed as another Rust function. Here's an excerpt from the example:

/// Switches to the next demo track without opening the app.
#[app_function]
fn next_track() -> Result<Playback, AppFunctionError> {
    command("next")
}
Enter fullscreen mode Exit fullscreen mode

Here, command forwards the action to the native player, and Playback contains its playback state. The complete implementation is in music.rs. Android sees this handler as nextTrack.

Watch the demo.

Once the example is installed, you can test track switching from PowerShell:

cd examples/tauri-app
.\test-music.ps1
Enter fullscreen mode Exit fullscreen mode

One important clarification about Gemini: according to Google's documentation, the integration is still in private preview. A successful adb call doesn't automatically mean Gemini will have access to your app's functions.

Source code, example, and setup instructions · crates.io

Top comments (0)