Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ plugins {
id("com.google.cloud.artifactregistry.gradle-plugin") version "2.2.4" apply false
kotlin("plugin.serialization") version "2.3.21" apply false
alias(libs.plugins.gradle.test.retry) apply false
alias(libs.plugins.google.services) apply false
}

val jdkVersion = providers.gradleProperty("jdkVersion").getOrElse("17").toInt()
Expand Down
5 changes: 5 additions & 0 deletions examples/firebase/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignored in THIS sample so forks point at their own Firebase project instead of ours -- the file
# just identifies a specific project. Its contents are not secret (the Firebase config/API key is
# public by design and ships inside the APK), so your own app is free to commit it if you prefer.
# Drop your own file here (see README.md); the google-services Gradle plugin picks it up.
google-services.json
82 changes: 82 additions & 0 deletions examples/firebase/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# ADK Kotlin — Firebase AI example

A minimal Android app showing an ADK [`LlmAgent`] backed by the Firebase AI
(Gemini) model from the [`:google-adk-kotlin-firebase`](../../firebase) module.
The chat screen demonstrates the two things the module's
`FirebaseIntegrationTest` proves work:

- **plain chat** — e.g. *"Tell me about the planet Earth"*.
- **tool calling** — e.g. *"What is the current temperature in Mountain
View?"*, which makes the model call the `get_current_temperature` tool (see
[`WeatherTools.kt`](src/main/kotlin/com/google/adk/kt/examples/firebase/WeatherTools.kt)).

Because Firebase AI needs an Android `Context`, this is a runnable Android app
rather than a JVM `main()` sample.

## Configure Firebase

The app needs to know which Firebase project to talk to. Two ways, in order of
preference:

### 1. `google-services.json` (standard Firebase setup — recommended)

This mirrors what a normal Firebase Android developer does.

1. In the [Firebase console](https://console.firebase.google.com/), open your
project (or create one) and enable the **Firebase AI Logic** / Gemini API.
2. Register an **Android app** with the package name
**`com.google.adk.kt.examples.firebase`** (this is the `applicationId` in
[`build.gradle.kts`](build.gradle.kts); change both if you prefer your own).
3. Download the generated `google-services.json` and place it in **this
directory** (`examples/firebase/google-services.json`).

This file just points the app at a specific Firebase project; its contents are
**not secret** — the Firebase config/API key is
[public by design](https://firebase.google.com/docs/projects/api-keys) and ships
inside the APK anyway, so a normal app can commit it (and often does, especially
in a private repo). It's **git-ignored here only** so that forks of this sample
use their own Firebase project instead of ours. When present, the
`com.google.gms.google-services` Gradle plugin is applied automatically and
initializes the default `FirebaseApp` for you.

### 2. Build-time Firebase config (fallback)

If you don't have a `google-services.json`, supply the three values directly and
they are baked into the APK's manifest at build time. Pass them as Gradle
properties:

```shell
./gradlew :google-adk-kotlin-examples-firebase:installDebug \
-PFIREBASE_API_KEY=your_api_key \
-PFIREBASE_APP_ID=your_app_id \
-PFIREBASE_PROJECT_ID=your_project_id
```

or export the matching `FIREBASE_API_KEY` / `FIREBASE_APP_ID` /
`FIREBASE_PROJECT_ID` environment variables before building. (These are read at
build time, not from the device's environment.)

If neither method provides a configuration, the Gradle build prints a warning,
and the installed app starts but shows a message explaining what to add instead
of calling Firebase with a blank config.

## Build & run

With a device or emulator connected:

```shell
./gradlew :google-adk-kotlin-examples-firebase:installDebug
```

Then launch **"ADK Firebase Example"** from the launcher. To change the model,
edit `MODEL_NAME` in
[`FirebaseChatAgent.kt`](src/main/kotlin/com/google/adk/kt/examples/firebase/FirebaseChatAgent.kt).

> Note: the `FIREBASE_*` values (like those in `google-services.json`) are
> project *identifiers*, not secrets — they're public by design and always end
> up inside the APK. What you must keep out of the app and the repo is a
> genuinely secret key, such as a Gemini Developer API key or an Admin SDK
> service-account key; this sample uses neither (it talks to the model through
> Firebase AI Logic).

[`LlmAgent`]: ../../core/src/main/kotlin/com/google/adk/kt/agents/LlmAgent.kt
114 changes: 114 additions & 0 deletions examples/firebase/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

plugins {
id("com.android.application")
alias(libs.plugins.ksp)
}

// Standard Firebase developer setup: the `com.google.gms.google-services` plugin reads a
// `google-services.json` from this module's root and generates the default-FirebaseApp
// configuration so `FirebaseApp.getInstance()` works with zero code. The plugin is applied only
// when a developer has dropped their own file in. When it is absent the module still builds and the
// sample falls back to the FIREBASE_* environment variables (see FirebaseChatActivity). Read
// README.md for setup.
if (file("google-services.json").exists()) {
apply(plugin = "com.google.gms.google-services")
}

// The build-time fallback config keys, read as Gradle properties or environment variables. Shared
// by the manifest-placeholder loop and the "no configuration" diagnostic below.
val firebaseConfigKeys = listOf("FIREBASE_API_KEY", "FIREBASE_APP_ID", "FIREBASE_PROJECT_ID")

android {
namespace = "com.google.adk.kt.examples.firebase"

compileSdk { version = release(36) { minorApiLevel = 1 } }

defaultConfig {
applicationId = "com.google.adk.kt.examples.firebase"
minSdk = rootProject.extra["androidMinSdk"] as Int
targetSdk = 36
versionCode = 1
versionName = "0.1.0"

// Fallback Firebase-config path (see FirebaseChatActivity): when no google-services.json is
// present, the Firebase config can instead be baked into the APK at build time as manifest
// metadata, sourced from Gradle properties (-PFIREBASE_API_KEY=...) or the matching
// environment variables.
for (name in firebaseConfigKeys) {
manifestPlaceholders[name] =
providers
.gradleProperty(name)
.orElse(providers.environmentVariable(name))
.getOrElse("\${$name}")
}
}

packaging {
resources {
merges += "**/META-INF/INDEX.LIST"
merges += "**/META-INF/DEPENDENCIES"
}
}
}

// Build-time diagnostic: if neither a google-services.json nor a complete set of FIREBASE_* values
// is present, the produced APK has no usable Firebase configuration and the app just shows a "no
// configuration" message at runtime.
run {
val hasGoogleServicesJson = file("google-services.json").exists()
val missingKeys = firebaseConfigKeys.filter { name ->
providers
.gradleProperty(name)
.orElse(providers.environmentVariable(name))
.orNull
.isNullOrBlank()
}
if (!hasGoogleServicesJson && missingKeys.isNotEmpty()) {
val projectPath = project.path
val log = logger
val message =
"examples/firebase: building without a usable Firebase configuration. The app will " +
"build and launch but show a \"No Firebase configuration found\" message instead of " +
"calling Firebase. Add a google-services.json to examples/firebase/, or pass " +
"-PFIREBASE_API_KEY=... -PFIREBASE_APP_ID=... -PFIREBASE_PROJECT_ID=... " +
"(missing: ${missingKeys.joinToString()}). See examples/firebase/README.md."
// Fire only when this app's build/install tasks are actually scheduled (not on every Gradle
// configuration).
gradle.taskGraph.addTaskExecutionGraphListener { graph ->
val buildingThisApp =
graph.allTasks.any { task ->
task.path.startsWith("$projectPath:") &&
listOf("assemble", "install", "bundle", "package").any { task.name.startsWith(it) }
}
if (buildingThisApp) log.warn("WARNING: $message")
}
}
}

dependencies {
implementation(project(":google-adk-kotlin-core"))
implementation(project(":google-adk-kotlin-firebase"))
implementation(platform(libs.google.firebase.platform))
implementation(libs.google.firebase.ai)
implementation(libs.kotlinx.coroutines.core)
// ViewCompat / WindowInsetsCompat for edge-to-edge inset handling across all API levels.
implementation(libs.androidx.core)

// Generates the `@Tool` FunctionTools used by the weather agent (WeatherTools.generatedTools()).
ksp(project(":google-adk-kotlin-processor"))
}
56 changes: 56 additions & 0 deletions examples/firebase/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright 2026 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- The application id / namespace is declared via `namespace` in build.gradle.kts, not with a
`package` attribute here (that attribute is deprecated and warns at build time). -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<!-- Required: the agent talks to the cloud Firebase AI (Gemini) backend. -->
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:label="ADK Firebase Example"
android:theme="@android:style/Theme.DeviceDefault.Light">

<!--
Fallback Firebase configuration, used only when this APK is built WITHOUT a
google-services.json (the standard path). These values are baked in at build time from
the FIREBASE_* manifest placeholders (see examples/firebase/build.gradle.kts), sourced
from Gradle properties or environment variables. When google-services.json is present the
default FirebaseApp is initialized from it and these entries are ignored.
-->
<meta-data
android:name="com.google.adk.FIREBASE_API_KEY"
android:value="${FIREBASE_API_KEY}" />
<meta-data
android:name="com.google.adk.FIREBASE_APP_ID"
android:value="${FIREBASE_APP_ID}" />
<meta-data
android:name="com.google.adk.FIREBASE_PROJECT_ID"
android:value="${FIREBASE_PROJECT_ID}" />

<activity
android:name=".FirebaseChatActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* Copyright 2026 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.google.adk.kt.examples.firebase

import android.content.Context
import android.content.pm.PackageManager
import com.google.firebase.FirebaseApp
import com.google.firebase.FirebaseOptions

/**
* Resolves the [FirebaseApp] this sample talks to. This is Firebase setup plumbing, kept out of
* [FirebaseChatActivity] so the activity shows only how ADK is used (build an agent, run it).
*
* Resolution order:
* 1. Standard setup: a `google-services.json` in this module's root, processed by the
* `com.google.gms.google-services` Gradle plugin, auto-initializes the default [FirebaseApp] at
* process start. In a normal Firebase app this is all you need — [resolve] just returns it and
* the rest of this class is irrelevant.
* 2. Fallback: the `FIREBASE_API_KEY` / `FIREBASE_APP_ID` / `FIREBASE_PROJECT_ID` values baked into
* the APK manifest at build time (see `build.gradle.kts` / `AndroidManifest.xml`), used to build
* a named [FirebaseApp] on demand.
*
* Returns null when neither is available; see README.md for setup.
*/
internal object FirebaseAppResolver {

/** Prefix of the manifest metadata keys holding the build-time fallback config. */
private const val META_DATA_PREFIX = "com.google.adk."

/** Non-default FirebaseApp name used for the build-time-config fallback path. */
private const val FALLBACK_APP_NAME = "adk-firebase-example"

/**
* Resolves a usable [FirebaseApp], or null if neither the standard nor fallback config exists.
*/
fun resolve(context: Context): FirebaseApp? {
// 1) Standard path: google-services.json + the google-services plugin auto-initialize the
// default FirebaseApp at process start (via Firebase's init ContentProvider).
runCatching { FirebaseApp.getInstance() }
.getOrNull()
?.let {
return it
}

// 2) Fallback path: reuse the named app if an earlier call already built it, otherwise assemble
// one from the FIREBASE_* manifest meta-data baked in at build time.
runCatching { FirebaseApp.getInstance(FALLBACK_APP_NAME) }
.getOrNull()
?.let {
return it
}
val apiKey = bakedMetaData(context, "FIREBASE_API_KEY") ?: return null
val appId = bakedMetaData(context, "FIREBASE_APP_ID") ?: return null
val projectId = bakedMetaData(context, "FIREBASE_PROJECT_ID") ?: return null

return FirebaseApp.initializeApp(
context.applicationContext,
FirebaseOptions.Builder()
.setApiKey(apiKey)
.setApplicationId(appId)
.setProjectId(projectId)
.build(),
FALLBACK_APP_NAME,
)
}

/**
* Reads the `${[META_DATA_PREFIX]}${[token]}` application manifest metadata entry. Returns null
* if it is missing, blank, or still holds its unresolved `${[token]}` build placeholder (i.e. no
* value was supplied at build time).
*/
private fun bakedMetaData(context: Context, token: String): String? {
val raw =
try {
val appInfo =
context.packageManager.getApplicationInfo(
context.packageName,
PackageManager.GET_META_DATA,
)
appInfo.metaData?.getString(META_DATA_PREFIX + token)
} catch (_: PackageManager.NameNotFoundException) {
null
}
return raw?.takeIf { it.isNotBlank() && !it.contains(token) }
}
}
Loading
Loading