Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

Google AI Edge Gallery — Administrator & Developer Guide

Generated May 2026 from google-ai-edge/gallery at 2ccb603

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

1. Architecture Overview

Google AI Edge Gallery is an Android application that runs large language models entirely on-device using the LiteRT-LM and MediaPipe LLM Inference runtimes. The app is built with Kotlin, Jetpack Compose, and Hilt dependency injection, following a single-activity architecture with Compose navigation.

High-Level Component Map

┌─────────────────────────────────────────────────────────┐
│  MainActivity (SingleActivity)                          │
│  ├── GalleryNavGraph (Compose Navigation)               │
│  │   ├── HomeScreen (categories → tasks → models)       │
│  │   ├── LlmChatScreen / LlmSingleTurnScreen           │
│  │   ├── BenchmarkScreen                                │
│  │   ├── ModelManagerScreen                             │
│  │   └── CustomTask.MainScreen (plugin screens)         │
│  └── ModelManagerViewModel (shared model lifecycle)     │
├─────────────────────────────────────────────────────────┤
│  Runtime Layer                                          │
│  ├── LlmModelHelper (model init, inference, cleanup)    │
│  ├── AICoreModelHelper (Google AI Core integration)     │
│  └── ModelHelperExt (extension functions)               │
├─────────────────────────────────────────────────────────┤
│  Data Layer                                             │
│  ├── ModelAllowlist (remote JSON → curated model list)  │
│  ├── SkillAllowlist (skill discovery + management)      │
│  ├── DataStoreRepository (protobuf-backed persistence)  │
│  ├── DownloadRepository + DownloadWorker (WorkManager)  │
│  └── Config / ConfigValue (runtime tuning)              │
├─────────────────────────────────────────────────────────┤
│  External Services                                      │
│  ├── Hugging Face (OAuth 2.0 + model file downloads)    │
│  ├── Firebase Analytics + FCM                           │
│  └── Google AI Core (optional, device-dependent)        │
└─────────────────────────────────────────────────────────┘

Data Flow

  1. On launch, the app loads the model allowlist for its version from GitHub raw content or a cached copy.
  2. The home screen renders tasks grouped by CategoryInfo. Each task declares which model names it supports.
  3. When a user selects a model, DownloadWorker fetches the .litertlm or .task file from Hugging Face via HttpURLConnection, using OAuth tokens when required.
  4. LlmModelHelper.initializeModelFn() loads the downloaded model into the LiteRT-LM or MediaPipe runtime.
  5. Inference runs entirely on-device (CPU, GPU, or AI Core accelerator). Results stream back to the Compose UI.

Module Dependency Graph

app (module)
 ├── :data          (Task, Model, Config, Allowlist, DataStore)
 ├── :common        (ProjectConfig, Types, Utils)
 ├── :runtime       (LlmModelHelper, AICoreModelHelper)
 ├── :di            (AppModule — Hilt bindings)
 ├── :customtasks   (CustomTask interface + implementations)
 ├── :ui            (Compose screens, navigation, theme)
 └── :worker        (DownloadWorker — WorkManager)

Note

This is a single-module Gradle project. The packages above are logical modules within the app module, not separate Gradle modules. All code lives under com.google.ai.edge.gallery.

Key Design Decisions

  • No backend server. All inference is on-device. The only network calls are model downloads from Hugging Face and allowlist fetches from GitHub.
  • Model allowlists are versioned per app release. Old app versions receive deprecation warnings in model descriptions rather than losing access entirely.
  • The CustomTask plugin system allows adding new task types without modifying core navigation or model management code. Tasks register via Hilt @IntoSet injection.
  • Skills extend LLM capabilities with JavaScript executed in a sandboxed WebView. The LLM decides when to invoke a skill via function calling.

2. System Requirements

Build Environment

Requirement Version
Android Studio Ladybug or later (2024.2+)
JDK 17
Kotlin Version catalog managed (see libs.versions.toml)
Gradle Kotlin DSL, wrapper included
Android SDK (compile) 35 (Android 15)
Android SDK (target) 35
Android SDK (min) 31 (Android 12)

Device Requirements

Requirement Minimum Recommended
Android version 12 (API 31) 14+ (API 34+)
RAM 6 GB 8-12 GB
Free storage 2 GB 5+ GB (multiple models)
GPU Optional (CPU fallback) GPU with OpenCL support

Important

Models with minDeviceMemoryInGb: 12 (Gemma 3n E4B, Gemma 4 E4B) will not load on devices with less than 12 GB of RAM. The app filters the model list based on device memory to prevent out-of-memory crashes.

Model Size Reference (v1.0.11 Allowlist)

Model File Size Min RAM Accelerators
Gemma-4-E2B-it 2.4 GB 8 GB gpu,cpu
Gemma-4-E4B-it 3.4 GB 12 GB gpu,cpu
Gemma-3n-E2B-it 3.4 GB 8 GB cpu,gpu
Gemma-3n-E4B-it 4.6 GB 12 GB cpu,gpu
Gemma3-1B-IT 557 MB 6 GB gpu,cpu
Qwen2.5-1.5B-Instruct 1.5 GB 6 GB gpu,cpu
DeepSeek-R1-Distill-Qwen-1.5B 1.7 GB 6 GB gpu,cpu
TinyGarden-270M 276 MB 6 GB cpu
MobileActions-270M 276 MB 6 GB cpu

3. Configuration Reference

Build Configuration

The root build file is Android/src/build.gradle.kts. The app module build file is Android/src/app/build.gradle.kts. Version management uses a Gradle version catalog at Android/src/gradle/libs.versions.toml.

Gradle properties (Android/src/gradle.properties):

# Standard Android/Kotlin properties managed by the version catalog.
# No custom properties are required for a basic build.

App Auth redirect scheme -- the build.gradle.kts file configures the OAuth redirect scheme via:

android {
    defaultConfig {
        manifestPlaceholders["appAuthRedirectScheme"] = "your-redirect-scheme"
    }
}

This must match the redirectUri constant in ProjectConfig.kt.

OAuth Configuration (ProjectConfig.kt)

Located at Android/src/app/src/main/java/com/google/ai/edge/gallery/common/ProjectConfig.kt:

Constant Purpose Default
clientId Hugging Face OAuth client ID REPLACE_WITH_YOUR_CLIENT_ID_IN_HUGGINGFACE_APP
redirectUri OAuth redirect URI REPLACE_WITH_YOUR_REDIRECT_URI_IN_HUGGINGFACE_APP
authEndpoint HF authorization URL https://huggingface.co/oauth/authorize
tokenEndpoint HF token exchange URL https://huggingface.co/oauth/token

Warning

The default clientId and redirectUri are placeholders. You must register an application on Hugging Face and replace these values before building a release. Without valid OAuth credentials, gated model downloads will fail.

Model Runtime Configuration

Each model in the allowlist carries a defaultConfig block. These values are user-adjustable at runtime via the config dialog in the app bar.

Config Key Type Description
topK Int Number of top-K candidates for sampling (1-128)
topP Float Nucleus sampling probability threshold (0.0-1.0)
temperature Float Sampling temperature. 0.0 = greedy/deterministic
maxTokens Int Maximum output tokens per response
accelerators String Comma-separated accelerator preference: cpu, gpu
maxContextLength Int Maximum context window (Gemma 4 models: 32000)
visionAccelerator String Accelerator for vision processing (Gemma 4: gpu)

The ConfigValue class in data/ConfigValue.kt handles typed config values with validation bounds.

App Bundle Configuration

Android/src/app/src/main/bundle_config.pb.json controls Android App Bundle settings for Play Store distribution.

Protobuf Schemas

Three protobuf schemas define the DataStore persistence format:

Schema File Purpose
Benchmark proto/benchmark.proto Stores benchmark run results
Settings proto/settings.proto User preferences (theme, accepted TOS)
Skill proto/skill.proto Installed skill metadata and state

Serializers for these schemas (BenchmarkResultsSerializer, SettingsSerializer, SkillSerializer, UserDataSerializer, CutoutsSerializer) live in the root package alongside GalleryApplication.kt.

Firebase Configuration

Firebase Analytics and FCM are integrated via standard google-services.json (not checked into the repository). FcmMessagingService.kt handles push notification tokens. Analytics events are defined in Analytics.kt.

4. Deployment & Scaling (Build & Distribution)

Building from Source

# Clone the repository
git clone https://github.com/google-ai-edge/gallery.git
cd gallery/Android/src

# Build debug APK
./gradlew assembleDebug

# Build release APK (requires signing config)
./gradlew assembleRelease

# Build Android App Bundle for Play Store
./gradlew bundleRelease

Tip

The debug build does not require OAuth credentials for basic functionality. Model downloads from public (non-gated) Hugging Face repositories work without authentication.

Signing Configuration

Release builds require a signing keystore. Configure in Android/src/app/build.gradle.kts:

android {
    signingConfigs {
        create("release") {
            storeFile = file("path/to/keystore.jks")
            storePassword = "..."
            keyAlias = "..."
            keyPassword = "..."
        }
    }
    buildTypes {
        release {
            signingConfig = signingConfigs.getByName("release")
        }
    }
}

Distribution Channels

Channel Format Notes
Google Play AAB (App Bundle) Primary distribution. Play Store handles splitting by ABI and density
GitHub Releases APK Direct install. Debug-signed unless configured otherwise
Apple App Store IPA iOS version has its own allowlist (ios_1_0_0.json)

Version Management

App versions map directly to model allowlist files. The naming convention is {major}_{minor}_{patch}.json in the model_allowlists/ directory:

model_allowlists/
├── 1_0_4.json      # Oldest supported Android version
├── 1_0_5.json
├── 1_0_6.json
├── 1_0_7.json
├── 1_0_8.json
├── 1_0_9.json
├── 1_0_10.json
├── 1_0_11.json     # Latest Android version
└── ios_1_0_0.json  # iOS version

When a new app version is released, a corresponding allowlist file is created. Older versions can be updated to show deprecation warnings (models with descriptions starting with "Your current app version is too old...").

5. Operations

Model Allowlist Lifecycle

The model allowlist is the central control mechanism for which models are available to which app versions. The lifecycle is:

  1. Author: Create or update a JSON file in model_allowlists/ matching the target app version.
  2. Publish: Push to the GitHub repository. The app fetches allowlists from the raw GitHub content URL.
  3. Cache: The app caches the fetched allowlist locally. ModelAllowlist.kt handles fetching and parsing.
  4. Filter: Models are filtered by device memory (minDeviceMemoryInGb) before display.

Allowlist JSON schema -- each model entry:

{
  "name": "Display Name",
  "modelId": "huggingface-org/repo-name",
  "modelFile": "filename.litertlm",
  "description": "Markdown-enabled description",
  "sizeInBytes": 2583085056,
  "minDeviceMemoryInGb": 8,
  "commitHash": "full-sha-of-hf-commit",
  "llmSupportImage": true,
  "llmSupportAudio": true,
  "llmSupportThinking": true,
  "defaultConfig": {
    "topK": 64,
    "topP": 0.95,
    "temperature": 1.0,
    "maxTokens": 4000,
    "maxContextLength": 32000,
    "accelerators": "gpu,cpu",
    "visionAccelerator": "gpu"
  },
  "taskTypes": ["llm_chat", "llm_prompt_lab", "llm_agent_chat"],
  "bestForTaskTypes": ["llm_chat"]
}

Field reference:

Field Required Description
name Yes Display name shown in the model picker
modelId Yes Hugging Face repository ID (org/repo)
modelFile Yes Exact filename within the HF repo to download
description Yes Markdown description shown on the task detail screen
sizeInBytes Yes File size for download progress calculation
minDeviceMemoryInGb No Minimum device RAM. Models above device capacity are hidden
commitHash No Pinned HF commit hash for reproducible downloads
llmSupportImage No Whether the model accepts image input
llmSupportAudio No Whether the model accepts audio input
llmSupportThinking No Whether the model supports thinking/reasoning mode
defaultConfig Yes Default inference parameters (see Configuration Reference)
taskTypes Yes Array of BuiltInTaskId values this model supports
bestForTaskTypes No Array of task IDs where this model is preferred (sorted first)

Allowlist Version Evolution

The allowlist system has evolved across app versions:

Version Range Runtime File Extension Key Changes
1.0.4 - 1.0.6 MediaPipe LLM Inference .litertlm 3 models (Gemma 3n E2B/E4B, Gemma3-1B)
1.0.7 - 1.0.9 LiteRT-LM .litertlm Added Qwen2.5, DeepSeek-R1, extended models
1.0.10 LiteRT-LM .litertlm Added TinyGarden-270M, MobileActions-270M
1.0.11 LiteRT-LM .litertlm Added Gemma 4 (E2B/E4B) with thinking mode, 32K context

The legacy root-level model_allowlist.json uses the older MediaPipe format with .task file extensions and estimatedPeakMemoryInBytes instead of minDeviceMemoryInGb.

Model Download Pipeline

DownloadWorker.kt uses Android WorkManager for reliable background downloads:

  1. User taps a model's download button on the task detail screen.
  2. DownloadRepository.kt enqueues a DownloadWorker with the model's Hugging Face URL constructed from modelId, commitHash, and modelFile.
  3. DownloadWorker opens an HttpURLConnection to the Hugging Face CDN, streaming the file to local storage.
  4. If the model is gated, the OAuth token from AppAuth is attached as an Authorization: Bearer header.
  5. Progress updates are reported via WorkManager's setProgress() mechanism.
  6. On completion, the model file path is stored in the DataStore, and the model status transitions to "downloaded."

Task System

Tasks are the primary organizational unit on the home screen. Each task represents a use case (chat, image analysis, benchmarking, etc.) and declares which models it supports.

Built-in task IDs (defined in BuiltInTaskId object):

Task ID Label Description
llm_chat Chat Multi-turn conversational AI
llm_prompt_lab Prompt Lab Single-turn prompt testing
llm_ask_image Ask Image Multimodal image analysis
llm_ask_audio Ask Audio Audio transcription/analysis
llm_agent_chat Agent Chat LLM with skill/function calling
llm_mobile_actions Mobile Actions Device automation via FunctionGemma
llm_tiny_garden Tiny Garden Natural-language game control
mp_scrapbook Scrapbook MediaPipe-based image tasks

Legacy tasks: llm_chat, llm_prompt_lab, llm_ask_image, llm_ask_audio, and llm_agent_chat are marked as legacy via isLegacyTasks(). This distinction affects UI rendering and model initialization paths.

Thinking mode: Only llm_chat, llm_ask_image, and llm_ask_audio support thinking mode, gated by Task.allowThinking() which checks the task ID. Thinking mode is further gated by the model's llmSupportThinking capability flag.

Monitoring and Analytics

Firebase Analytics tracks user interactions. Events are defined in Analytics.kt. Key tracked events include:

  • Link clicks (model documentation, source code URLs)
  • Task selection and model selection
  • Download initiation and completion
  • Skill installation and invocation

FCM push notifications are handled by FcmMessagingService.kt for remote messaging capabilities.

Known Operational Issues

Issue Symptom Workaround
GPU not recognized (GitHub #682) Pixel 10 does not use GPU acceleration Force cpu in accelerator config
Memory after long chat (#82) App becomes unresponsive after extended conversations Restart the app; context accumulation exhausts RAM
Model download crash (#668) RuntimeException after download completes Clear app data and re-download
INVALID_ARGUMENT engine error (#585) Failed to create engine on model initialization Ensure model file matches expected format for app version
Only answers "over" (#601) Model produces single-word responses Re-download the model; possible corrupt download
Custom skill install failure (#619) Skills fail to install from URL or local import Verify skill manifest format and JavaScript entry point

6. Performance

Inference Configuration Tuning

Model performance is primarily controlled by the accelerator selection and sampling parameters.

Accelerator selection strategy:

Accelerator Best For Trade-off
gpu,cpu General-purpose models (Gemma 4, Gemma3-1B) Fastest inference; higher power consumption
cpu,gpu Multimodal models (Gemma 3n) More stable on devices with limited GPU memory
cpu Small specialized models (TinyGarden, MobileActions) Lowest latency for small models; avoids GPU init overhead

Temperature and determinism:

  • temperature: 0.0 forces greedy decoding. Used by TinyGarden and MobileActions for consistent function-calling output.
  • temperature: 1.0 with topK: 64 and topP: 0.95 provides diverse, creative responses. Default for conversational models.
  • Qwen2.5 uses temperature: 0.7, topK: 20, topP: 0.8 for more focused output.

Model Default Configurations (Complete Reference)

Config Gemma 4 E2B Gemma 3n E2B Gemma3-1B Qwen2.5-1.5B DeepSeek-R1 TinyGarden MobileActions
topK 64 64 64 20 64 64 64
topP 0.95 0.95 0.95 0.8 0.95 0.95 0.95
temperature 1.0 1.0 1.0 0.7 1.0 0.0 0.0
maxTokens 4000 4096 1024 4096 4096 1024 1024
accelerators gpu,cpu cpu,gpu gpu,cpu gpu,cpu gpu,cpu cpu cpu

Benchmarking

The built-in benchmark system measures model performance on the specific device. Results are persisted via protobuf DataStore (benchmark.proto). The benchmark UI is in ui/benchmark/.

Benchmark metrics include:

  • Time to first token (prefill latency)
  • Tokens per second (decode throughput)
  • Total inference time
  • Memory usage during inference

Memory Management

  • LlmModelHelper.cleanUpModelFn() must be called to release native resources when switching models or tasks.
  • GalleryLifecycleProvider.kt hooks into Android lifecycle events to manage model state across app backgrounding.
  • Models with estimatedPeakMemoryInBytes (legacy allowlist format) or minDeviceMemoryInGb (current format) provide memory budgeting guidance.

Caution

Running multiple models simultaneously is not supported. The app initializes one model at a time per task. Attempting to load a model while another is active in a different task will cause memory pressure and potential crashes.

Context Window Limits

Model Family Max Context Length
Gemma 4 (E2B, E4B) 32,000 tokens
Gemma 3n (E2B, E4B) 4,096 tokens
Gemma3-1B-IT 1,024 tokens (maxTokens)
Qwen2.5-1.5B 4,096 tokens
DeepSeek-R1 4,096 tokens
TinyGarden/MobileActions 1,024 tokens

7. Security

OAuth Token Handling

Hugging Face OAuth 2.0 is implemented via the AppAuth library (net.openid.appauth). The flow:

  1. User initiates login from the model download screen.
  2. AuthorizationServiceConfiguration directs to https://huggingface.co/oauth/authorize.
  3. After consent, the redirect URI returns the authorization code to the app.
  4. The app exchanges the code for an access token via https://huggingface.co/oauth/token.
  5. Tokens are stored in-memory via the AppAuth AuthState object.

Warning

The clientId and redirectUri in ProjectConfig.kt are shipped as placeholders. Any fork or custom build must replace these with credentials from a registered Hugging Face OAuth application. The redirect URI scheme in ProjectConfig.kt must match the appAuthRedirectScheme manifest placeholder in build.gradle.kts.

Skill Sandbox Security

Skills are JavaScript packages that execute inside an Android WebView. The security boundary is:

  • Execution context: Skills run within GalleryWebView, which loads HTML/JS from either bundled assets or imported packages.
  • Local URL base: All local asset URLs are served under https://appassets.androidplatform.net via Android's WebViewAssetLoader. This avoids file:// protocol restrictions.
  • Data passing: The LLM constructs a JSON data payload and an optional secret string. These are passed to the skill's entry point function.
  • Secret management: Skills that require API keys (e.g., Gemini API key for Restaurant Roulette, Loudly API key for Mood Music) use the SecretEditorDialog to store secrets per-skill. Secrets are passed as the second argument to window['ai_edge_gallery_get_result'].
  • Return contract: Skills return JSON with constrained fields: result (text), error (error text), image (base64-encoded), webview (URL/iframe for rich rendering).

Caution

WebView JavaScript execution is not fully sandboxed at the OS level. A malicious skill with a valid manifest could exfiltrate data accessible to the WebView context. Only install skills from trusted sources. The app does not perform code signing verification on imported skills.

Data Storage Security

  • DataStore (Protobuf): User settings, benchmark results, and skill metadata are stored in protobuf-serialized DataStore files in the app's private data directory. These are protected by Android's app sandbox.
  • Model files: Downloaded models are stored in the app's local files directory, accessible only to the app process.
  • No remote data transmission: All inference runs on-device. No user prompts, model outputs, or conversation history are transmitted to any server.

Permissions

The AndroidManifest.xml declares:

Permission Purpose
INTERNET Model downloads from Hugging Face, allowlist fetching, skill network calls
FOREGROUND_SERVICE Background model downloads via WorkManager
CAMERA Ask Image: capture photos for multimodal analysis
RECORD_AUDIO Voice input and Ask Audio features

8. Developer Internals

Custom Task System

The CustomTask interface (customtasks/common/CustomTask.kt) is the extensibility mechanism for adding new task types.

Interface contract:

interface CustomTask {
    val task: Task

    fun initializeModelFn(
        context: Context,
        coroutineScope: CoroutineScope,
        model: Model,
        onDone: (error: String) -> Unit,
    )

    fun cleanUpModelFn(
        context: Context,
        coroutineScope: CoroutineScope,
        model: Model,
        onDone: () -> Unit,
    )

    @Composable
    fun MainScreen(data: Any)
}

Implementing a custom task:

  1. Create a class implementing CustomTask in a new package under customtasks/.
  2. Define the task property with a Task data class specifying: id, label, category, icon, description, models, and modelNames.
  3. Implement initializeModelFn() -- this runs on Dispatchers.Default. Call onDone("") on success or onDone("error message") on failure.
  4. Implement cleanUpModelFn() -- release native resources, call onDone() when complete.
  5. Implement MainScreen() -- the Composable receives a CustomTaskData instance (cast from the Any parameter) containing the ModelManagerViewModel and layout callbacks.
  6. Register via Hilt:
@Module
@InstallIn(SingletonComponent::class)
object YourTaskModule {
    @Provides
    @IntoSet
    fun provideYourTask(): CustomTask = YourCustomTask()
}

CustomTaskData provides access to the shared model management layer:

data class CustomTaskData(
    val modelManagerViewModel: ModelManagerViewModel,
    val bottomPadding: Dp = 0.dp,
    val setAppBarControlsDisabled: (Boolean) -> Unit = {},
    val setTopBarVisible: (Boolean) -> Unit = {},
    val setCustomNavigateUpCallback: ((() -> Unit)?) -> Unit = {},
)

The setAppBarControlsDisabled callback lets your task disable the back button, config button, and model selector during operations that should not be interrupted. setTopBarVisible hides the entire app bar for full-screen experiences. setCustomNavigateUpCallback allows overriding the default back navigation behavior.

A separate CustomTaskDataForBuiltinTask class exists for built-in tasks, containing modelManagerViewModel and an onNavUp callback.

Existing custom task implementations:

Package Task Purpose
customtasks/examplecustomtask/ ExampleCustomTask Reference implementation -- displays model file text content
customtasks/agentchat/ AgentChat Agent Chat with skill/function calling (15 source files)
customtasks/mobileactions/ MobileActions Device automation via FunctionGemma
customtasks/tinygarden/ TinyGarden Natural-language controlled garden game

Task Data Class (Full Field Reference)

The Task data class in data/Tasks.kt defines task metadata:

Field Type Default Description
id String Required Unique task identifier (use BuiltInTaskId constants for built-ins)
label String Required Display label on the home screen
category CategoryInfo Required Determines which tab the task appears under
icon ImageVector? null Compose vector icon for the task tile
iconVectorResourceId Int? null Android vector resource ID (takes precedence over icon)
description String Required Shown at the top of the task detail screen
shortDescription String "" Brief description (6 words or fewer)
docUrl String "" Documentation link shown below description
sourceCodeUrl String "" Source code link shown below description
models MutableList<Model> Required Models assigned to this task
modelNames List<String> [] Model names to match from the allowlist
handleModelConfigChangesInTask Boolean false If true, task handles config changes itself instead of auto-reinitializing
experimental Boolean false Marks the task as experimental
newFeature Boolean false Shows a "new" badge on the home screen
useThemeColor Boolean false Uses theme color instead of task tint color
defaultSystemPrompt String "" System prompt prepended to conversations
agentNameRes Int R.string.chat_generic_agent_name String resource for agent name in chat bubbles
textInputPlaceHolderRes Int R.string.chat_textinput_placeholder String resource for input field placeholder

Skills API

Skills extend Agent Chat's capabilities by providing JavaScript-based tools that the LLM can invoke via function calling.

Skill package structure:

skill-name/
├── SKILL.md              # Manifest (name, description, parameters)
├── scripts/
│   ├── index.html        # WebView entry point
│   └── index.js          # JavaScript logic
└── assets/               # Optional static assets (HTML, images, etc.)

SKILL.md manifest format:

The SKILL.md file is a markdown document that serves as both the skill's documentation and its function-calling schema. The LLM reads this manifest to understand when and how to invoke the skill. It includes the skill name, a description of what the skill does, and the expected JSON input parameters.

JavaScript entry point contract:

window['ai_edge_gallery_get_result'] = async (data, secret) => {
    // data: JSON string with skill-specific parameters
    // secret: optional API key string from SecretEditorDialog
    try {
        const jsonData = JSON.parse(data);
        // ... perform work ...
        return JSON.stringify({
            result: "Text result shown in chat",
            // Optional fields:
            error: null,                          // Error message
            image: { base64: "..." },             // Base64-encoded image
            webview: {                            // Rich rendering
                url: "webview.html?params=...",    // URL (local or remote)
                iframe: false,                    // Whether to use iframe
                aspectRatio: 1.333                // Width/height ratio (default 4:3)
            }
        });
    } catch (e) {
        return JSON.stringify({ error: e.message });
    }
};

Return value types:

Field Type Description
result String Plain text result shown in the chat bubble
error String Error message; displayed as an error state
image.base64 String Base64-encoded image rendered inline
webview.url String URL loaded in an embedded WebView card
webview.iframe Boolean Whether the URL should load in an iframe
webview.aspectRatio Float Width-to-height ratio for the WebView (default: 1.333, i.e., 4:3)

Kotlin types for skill results (in common/Types.kt):

@JsonClass(generateAdapter = true)
data class CallJsSkillResult(
    val result: String?,
    val error: String?,
    val image: CallJsSkillResultImage?,
    val webview: CallJsSkillResultWebview?,
)

@JsonClass(generateAdapter = true)
data class CallJsSkillResultImage(val base64: String?)

@JsonClass(generateAdapter = true)
data class CallJsSkillResultWebview(
    val url: String?,
    val iframe: Boolean?,
    val aspectRatio: Float?,
)

These are parsed from the JavaScript return value using Moshi (@JsonClass(generateAdapter = true)).

Skill sources:

Source Discovery Example
Built-in Bundled in APK assets (assets/skills/) calculate-hash, qr-code, query-wikipedia
Featured Fetched from a remote featured skills list mood-music, restaurant-roulette, virtual-piano
URL import User provides a URL to a skill package Any conforming skill package
Local import User selects a local directory Development/testing workflow

Built-in skills reference:

Skill Function Requires Secret
calculate-hash SHA-1 hash of input text No
interactive-map Renders a location on a map No
kitchen-adventure Text-based cooking adventure game No
mood-tracker Tracks and visualizes mood entries No
qr-code Generates QR codes from text No
query-wikipedia Searches Wikipedia articles No
send-email Composes email via intent No
text-spinner Visual text transformation animations No

Featured skills reference:

Skill Function Requires Secret
mood-music Generates music based on mood (Loudly API) Yes (Loudly API key via loudly.com/developers/apps)
restaurant-roulette Spin wheel of nearby restaurants (Gemini API) Yes (Gemini API key)
virtual-piano Interactive piano keyboard No

Agent Action System

The Agent Chat task uses an action dispatch system to communicate between the LLM inference layer and the UI:

open class AgentAction(val name: AgentActionName)

enum class AgentActionName {
    CALL_JS_SKILL,
    SKILL_PROGRESS,
    ASK_INFO,
}

// Invoke a JavaScript skill
class CallJsAgentAction(
    val url: String,          // Skill's index.html URL
    val data: String,         // JSON data payload
    val secret: String = "",  // API key (from SecretEditorDialog)
    val result: CompletableDeferred<String> = CompletableDeferred(),
)

// Prompt the user for input
class AskInfoAgentAction(
    val dialogTitle: String,
    val fieldLabel: String,
    val result: CompletableDeferred<String> = CompletableDeferred(),
)

// Update skill progress indicator
class SkillProgressAgentAction(
    val label: String,
    val inProgress: Boolean,
    val addItemTitle: String = "",
    val addItemDescription: String = "",
    val customData: Any? = null,
)

Actions use Kotlin CompletableDeferred for asynchronous result delivery. The UI layer observes the action, renders the appropriate UI (WebView, dialog, or progress indicator), and completes the deferred when the result is available.

The SkillInfo data class ties skills to their metadata:

data class SkillInfo(
    val skillMd: String,       // SKILL.md content (function schema for LLM)
    val skillUrl: String?,     // URL to skill's index.html
    val tryoutChip: SkillTryOutChip?,  // Optional quick-try chip for UI
)

LLM Runtime Layer

LlmModelHelper.kt manages the complete model lifecycle:

  1. Initialization: Loads the model file into the LiteRT-LM or MediaPipe runtime. Configures accelerators, context length, and sampling parameters.
  2. Inference: Accepts prompts (text, image, audio) and streams token output via callbacks.
  3. Cleanup: Releases native memory and GPU resources.

AICoreModelHelper.kt provides an alternative initialization path for devices with Google AI Core support. AI Core is Google's on-device AI runtime that provides optimized inference for supported chipsets. The helper checks for AI Core availability and falls back to standard LiteRT-LM when unavailable.

ModelHelperExt.kt contains extension functions that bridge between the model helper and the UI layer, handling response processing and error formatting. The processLlmResponse() utility in Utils.kt normalizes escaped newlines (\\n to \n) in model output. The cleanUpMediapipeTaskErrorMessage() function strips internal stack trace details (everything after === Source Location Trace) from MediaPipe error messages before showing them to users.

Data Layer

DataStoreRepository.kt wraps Android DataStore with protobuf serialization:

  • BenchmarkResultsSerializer -- benchmark run data
  • SettingsSerializer -- user preferences
  • SkillSerializer -- installed skill state
  • UserDataSerializer -- user-specific data
  • CutoutsSerializer -- display cutout information

DownloadRepository.kt manages download state and coordinates with DownloadWorker:

  • Tracks download progress per model
  • Handles pause/resume (via WorkManager constraints)
  • Manages file integrity after download completion

Config.kt and ConfigValue.kt handle runtime configuration:

  • Config aggregates all config values for a model session
  • ConfigValue is a sealed type hierarchy supporting Int, Float, String, and Boolean config values with validation ranges

Networking

The app uses java.net.HttpURLConnection directly (no OkHttp or Retrofit). The getJsonResponse<T>() utility in Utils.kt provides generic JSON fetching:

inline fun <reified T> getJsonResponse(url: String): JsonObjAndTextContent<T>?

JSON parsing uses Gson for general-purpose deserialization (parseJson<T>()) and Moshi for skill results (via generated adapters with @JsonClass(generateAdapter = true)).

Audio Processing

Utils.kt includes audio utilities for the Ask Audio feature:

  • convertWavToMonoWithMaxSeconds() -- converts stereo WAV files to mono, resamples if needed, and truncates to a maximum duration (default 30 seconds)
  • Supports 8-bit to 16-bit upconversion
  • Reads WAV headers to extract channel count, sample rate, and bit depth
  • The constant SAMPLE_RATE (defined in data/Consts.kt) specifies the target sample rate

Dependency Injection (Hilt)

di/AppModule.kt is the single Hilt module providing application-scoped dependencies:

  • Set<CustomTask> -- all registered custom tasks (via @IntoSet)
  • DataStore instances for each protobuf schema
  • Repository singletons

The GalleryApplication.kt class is annotated with @HiltAndroidApp as the Hilt entry point.

UI Architecture

The UI follows Jetpack Compose with Material 3 theming:

  • Navigation: GalleryNavGraph.kt defines the navigation graph. Single-activity architecture with Compose destinations.
  • Home screen: ui/home/ -- categories as tabs, tasks as cards within each tab. Tab bar hidden when only one category exists.
  • Chat UI: ui/common/chat/ -- 36 files implementing the chat interface. Supports multiple message body types: text, image, audio, benchmark results, WebView embeds, loading indicators, and classified results.
  • Model management: ui/modelmanager/ -- ModelManagerViewModel is the shared ViewModel for model state across all screens. Handles initialization, config changes, and model switching.
  • Theme: ui/theme/ -- Material 3 dynamic color support with dark/light mode.
  • Voice input: ui/common/textandvoiceinput/ -- combined text field and voice recording input.
  • Terms of Service: ui/common/tos/ -- first-run TOS acceptance flow.
  • Shared components: ui/common/ includes reusable Composables such as Accordions, AudioAnimation (AGSL shader-based waveform), GalleryWebView, skill management bottom sheets, and model configuration dialogs.

Source File Reference

Root package (com.google.ai.edge.gallery):

File Purpose
GalleryApplication.kt Application class, @HiltAndroidApp entry point
GalleryApp.kt Root Composable
MainActivity.kt Single Activity
GalleryLifecycleProvider.kt Lifecycle observer for model management
Analytics.kt Firebase Analytics event definitions
FcmMessagingService.kt Firebase Cloud Messaging handler
BenchmarkResultsSerializer.kt DataStore serializer for benchmark data
SettingsSerializer.kt DataStore serializer for settings
SkillSerializer.kt DataStore serializer for skills
UserDataSerializer.kt DataStore serializer for user data
CutoutsSerializer.kt DataStore serializer for display cutouts

data package:

File Purpose
Tasks.kt Task data class, BuiltInTaskId constants, isLegacyTasks()
Model.kt Model data class
ModelAllowlist.kt Remote allowlist fetching and parsing
SkillAllowlist.kt Skill discovery and allowlist
Categories.kt CategoryInfo definitions for home screen tabs
Config.kt Runtime config aggregation
ConfigValue.kt Typed config value with validation
Consts.kt App-wide constants (SAMPLE_RATE, etc.)
DataStoreRepository.kt DataStore access layer
DownloadRepository.kt Download state management
Types.kt Data-layer type definitions

common package:

File Purpose
ProjectConfig.kt HF OAuth constants (clientId, redirectUri, endpoints)
Types.kt Core types: AgentAction, SkillInfo, CallJsSkillResult, AudioClip, etc.
Utils.kt Image/audio processing, JSON parsing, HTTP utilities, LOCAL_URL_BASE

runtime package:

File Purpose
LlmModelHelper.kt Model lifecycle management (init, infer, cleanup)
ModelHelperExt.kt Extension functions for model helpers
aicore/AICoreModelHelper.kt Google AI Core integration

worker package:

File Purpose
DownloadWorker.kt WorkManager worker for background model downloads

Note

The LOCAL_URL_BASE constant in Utils.kt is set to https://appassets.androidplatform.net. This is used by WebViewAssetLoader to serve local skill assets over a virtual HTTPS origin, avoiding security restrictions on file:// URLs in WebView.

9. Integration Points

Hugging Face Integration

The app integrates with Hugging Face at two levels:

  1. Model hosting: All model files are hosted on Hugging Face repositories. The modelId field in the allowlist maps to a HF repo (e.g., google/gemma-3n-E2B-it-litert-lm). The commitHash pins downloads to a specific commit for reproducibility.
  2. Authentication: OAuth 2.0 via AppAuth for accessing gated models. The flow uses the authorization code grant.

Download URL construction:

https://huggingface.co/{modelId}/resolve/{commitHash}/{modelFile}

For example:

https://huggingface.co/litert-community/gemma-4-E2B-it-litert-lm/resolve/7fa1d78.../gemma-4-E2B-it.litertlm

Google AI Core

AICoreModelHelper.kt integrates with Google's on-device AI Core runtime. This is an optional accelerator path available on supported devices (primarily Pixel phones with Tensor chipsets). When available, AI Core provides hardware-optimized inference that can outperform the standard LiteRT-LM path.

Tip

AI Core availability is device-dependent and not guaranteed. The app gracefully falls back to LiteRT-LM inference when AI Core is unavailable. No configuration is needed -- the runtime detects AI Core support automatically.

Firebase

  • Firebase Analytics: Event tracking for user interactions. No PII is collected.
  • Firebase Cloud Messaging (FCM): FcmMessagingService handles token management and incoming messages. Used for optional push notification capabilities.

Gemini API (via Skills)

The Restaurant Roulette featured skill calls the Gemini API (generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent) to generate restaurant recommendations. This is not a core app integration -- it is a skill-level dependency that requires the user to provide their own Gemini API key via the Secret Editor. The skill uses structured JSON output mode (responseMimeType: 'application/json') with a response schema to ensure reliable parsing.

Skill WebView Bridge

Skills communicate with the app through a JavaScript-to-Kotlin bridge:

  1. App to Skill: The app calls window['ai_edge_gallery_get_result'](data, secret) via WebView.evaluateJavascript().
  2. Skill to App: The skill returns a JSON string that the app parses into CallJsSkillResult using Moshi.
  3. Asset loading: Local skill assets (HTML, JS, images) are served via WebViewAssetLoader under the appassets.androidplatform.net domain.
  4. Network access: Skills with iframe: true or external URLs can make network requests subject to Android's standard WebView network policies.

Adding a New Model to the Allowlist

To add a new model:

  1. Upload the quantized model file to a Hugging Face repository.
  2. Note the commit hash, file name, and file size in bytes.
  3. Determine the model's capabilities (llmSupportImage, llmSupportAudio, llmSupportThinking).
  4. Test on target devices to establish minDeviceMemoryInGb and optimal accelerators ordering.
  5. Add the model entry to the appropriate version's allowlist JSON in model_allowlists/.
  6. Set taskTypes to the task IDs where this model should appear.
  7. Optionally set bestForTaskTypes to prioritize this model for specific tasks.
  8. Push the updated allowlist to the GitHub repository.

Adding a New Skill

To create and distribute a new skill:

  1. Create a directory with SKILL.md, scripts/index.html, and scripts/index.js.
  2. Write the SKILL.md manifest describing the skill's purpose and expected input parameters.
  3. Implement the window['ai_edge_gallery_get_result'] function in index.js.
  4. Test locally by importing the skill via the local import flow in the app.
  5. To distribute as a built-in skill: add the directory under Android/src/app/src/main/assets/skills/ and rebuild.
  6. To distribute as a featured skill: add the directory under skills/featured/ and update the featured skills index.

Adding a New Custom Task

To add a new custom task to the app:

  1. Create a new package under customtasks/ (e.g., customtasks/yourtask/).
  2. Implement the CustomTask interface with your task metadata, model lifecycle, and UI.
  3. Create a Hilt module in the same package with @Provides @IntoSet binding.
  4. Define which models your task uses via modelNames (matched from the allowlist) or manually populate models.
  5. Choose or create a CategoryInfo for home screen placement.
  6. Build and test -- Hilt auto-discovers your task and adds it to the home screen.