diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..9ca7a80
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,180 @@
+name: CI
+
+on:
+ push:
+ branches: ["**"]
+ pull_request:
+ branches: ["**"]
+
+jobs:
+ build:
+ name: Build APK
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "temurin"
+
+ - name: Cache Gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: gradle-
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug --no-daemon
+
+ - name: Upload APK
+ uses: actions/upload-artifact@v4
+ with:
+ name: app-debug
+ path: app/build/outputs/apk/debug/app-debug.apk
+
+ test:
+ name: API Integration Tests
+ runs-on: ubuntu-latest
+ needs: build
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Enable KVM (hardware-accelerated emulator)
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' \
+ | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "temurin"
+
+ - name: Cache Gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: gradle-
+
+ - name: Download APK artifact
+ uses: actions/download-artifact@v4
+ with:
+ name: app-debug
+ path: apk/
+
+ - name: Install Android SDK components
+ run: |
+ yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses > /dev/null
+ $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager \
+ "platform-tools" \
+ "emulator" \
+ "system-images;android-31;google_apis;x86_64"
+
+ - name: Create AVD
+ run: |
+ echo no | $ANDROID_HOME/cmdline-tools/latest/bin/avdmanager create avd \
+ --name pixel10_test \
+ --package "system-images;android-31;google_apis;x86_64" \
+ --device "pixel_6"
+
+ - name: Start emulator
+ run: |
+ $ANDROID_HOME/emulator/emulator \
+ -avd pixel10_test \
+ -no-window -no-audio -no-snapshot \
+ -gpu swiftshader_indirect &
+ $ANDROID_HOME/platform-tools/adb wait-for-device
+
+ - name: Wait for emulator boot
+ run: |
+ timeout 300 bash -c '
+ until $ANDROID_HOME/platform-tools/adb shell \
+ getprop sys.boot_completed 2>/dev/null | grep -q "1"; do
+ sleep 3
+ done
+ '
+ $ANDROID_HOME/platform-tools/adb shell input keyevent 82
+
+ - name: Install APK
+ run: $ANDROID_HOME/platform-tools/adb install apk/app-debug.apk
+
+ - name: Write Gemini API key to SharedPreferences
+ env:
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
+ run: |
+ $ANDROID_HOME/platform-tools/adb shell run-as com.pixel10.ai sh -c \
+ 'mkdir -p /data/data/com.pixel10.ai/shared_prefs && cat > /data/data/com.pixel10.ai/shared_prefs/pixel10_prefs.xml' << EOF
+
+
+ EOF
+
+ - name: Start API server
+ run: |
+ $ANDROID_HOME/platform-tools/adb shell am startservice \
+ -n com.pixel10.ai/.server.ApiServerService \
+ -a com.pixel10.ai.START_SERVER \
+ --ei port 8080
+ sleep 5
+
+ - name: Forward device port
+ run: $ANDROID_HOME/platform-tools/adb forward tcp:8080 tcp:8080
+
+ - name: Wait for server to be ready
+ run: |
+ timeout 30 bash -c '
+ until curl -sf http://localhost:8080/health > /dev/null 2>&1; do
+ sleep 2
+ done
+ '
+
+ - name: Test /health
+ run: |
+ RESPONSE=$(curl -sf http://localhost:8080/health)
+ echo "Health response: $RESPONSE"
+ echo "$RESPONSE" | grep -q '"status"' || \
+ (echo "FAIL: /health did not return expected JSON" && exit 1)
+
+ - name: Test /v1/models
+ run: |
+ RESPONSE=$(curl -sf http://localhost:8080/v1/models)
+ echo "Models response: $RESPONSE"
+ echo "$RESPONSE" | grep -q '"data"' || \
+ (echo "FAIL: /v1/models did not return expected JSON" && exit 1)
+
+ - name: Test /v1/chat/completions (non-streaming)
+ run: |
+ RESPONSE=$(curl -sf http://localhost:8080/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{"model":"gemini","messages":[{"role":"user","content":"Reply with one word: hello"}]}')
+ echo "Chat response: $RESPONSE"
+ echo "$RESPONSE" | grep -q '"choices"' || \
+ (echo "FAIL: /v1/chat/completions did not return choices" && exit 1)
+
+ - name: Test /v1/chat/completions (streaming)
+ run: |
+ # Collect SSE chunks with a 20-second timeout
+ CHUNKS=$(curl -sf --max-time 20 http://localhost:8080/v1/chat/completions \
+ -H "Content-Type: application/json" \
+ -d '{"model":"gemini","stream":true,"messages":[{"role":"user","content":"Say hi"}]}')
+ echo "Streaming chunks received:"
+ echo "$CHUNKS"
+ echo "$CHUNKS" | grep -q "data:" || \
+ (echo "FAIL: streaming returned no SSE chunks" && exit 1)
+
+ - name: Print logcat on failure
+ if: failure()
+ run: $ANDROID_HOME/platform-tools/adb logcat -d -s Pixel10AI ApiServerService GeminiCloudModel
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..d813c99
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,45 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - "v*"
+
+jobs:
+ release:
+ name: Build and Release APK
+ runs-on: ubuntu-latest
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Set up JDK 17
+ uses: actions/setup-java@v4
+ with:
+ java-version: "17"
+ distribution: "temurin"
+
+ - name: Cache Gradle
+ uses: actions/cache@v4
+ with:
+ path: |
+ ~/.gradle/caches
+ ~/.gradle/wrapper
+ key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
+ restore-keys: gradle-
+
+ - name: Build debug APK
+ run: ./gradlew assembleDebug --no-daemon
+
+ - name: Rename APK with version tag
+ run: |
+ TAG="${{ github.ref_name }}"
+ cp app/build/outputs/apk/debug/app-debug.apk "pixel10-ai-${TAG}.apk"
+
+ - name: Create GitHub Release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: "pixel10-ai-${{ github.ref_name }}.apk"
+ generate_release_notes: true
diff --git a/app/src/main/java/com/pixel10/ai/inference/GeminiCloudModel.kt b/app/src/main/java/com/pixel10/ai/inference/GeminiCloudModel.kt
new file mode 100644
index 0000000..443f2f7
--- /dev/null
+++ b/app/src/main/java/com/pixel10/ai/inference/GeminiCloudModel.kt
@@ -0,0 +1,165 @@
+package com.pixel10.ai.inference
+
+import android.util.Log
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.json.JSONArray
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.HttpURLConnection
+import java.net.URL
+
+/**
+ * Cloud backend that proxies inference requests to the Gemini 2.0 Flash API.
+ *
+ * Works from any context (foreground service, background) because it uses
+ * standard HTTPS rather than the AICore system service. This is the fallback
+ * when Gemini Nano is unavailable (emulator, background inference blocked, etc.).
+ *
+ * Requires a Gemini API key (free tier available at ai.google.dev).
+ */
+class GeminiCloudModel(private val apiKey: String) : OnDeviceModel {
+
+ override val backendName = "Gemini 2.0 Flash (Cloud)"
+
+ override val isReady: Boolean = true
+
+ override suspend fun generate(
+ prompt: String,
+ maxTokens: Int,
+ temperature: Float
+ ): String = withContext(Dispatchers.IO) {
+ val url = URL("$BASE_URL:generateContent?key=$apiKey")
+ val connection = url.openConnection() as HttpURLConnection
+ try {
+ connection.requestMethod = "POST"
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.doOutput = true
+ connection.connectTimeout = 30_000
+ connection.readTimeout = 60_000
+
+ val body = buildRequestBody(prompt, maxTokens, temperature)
+ connection.outputStream.use { it.write(body.toByteArray()) }
+
+ val responseCode = connection.responseCode
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ val error = connection.errorStream?.bufferedReader()?.readText() ?: "Unknown error"
+ throw OnDeviceModel.InferenceException("Gemini API error $responseCode: $error")
+ }
+
+ val responseText = connection.inputStream.bufferedReader().readText()
+ parseGenerateResponse(responseText)
+ } finally {
+ connection.disconnect()
+ }
+ }
+
+ override suspend fun generateStreaming(
+ prompt: String,
+ onToken: (String) -> Unit
+ ): String = withContext(Dispatchers.IO) {
+ val url = URL("$STREAMING_URL?key=$apiKey")
+ val connection = url.openConnection() as HttpURLConnection
+ try {
+ connection.requestMethod = "POST"
+ connection.setRequestProperty("Content-Type", "application/json")
+ connection.setRequestProperty("Accept", "text/event-stream")
+ connection.doOutput = true
+ connection.connectTimeout = 30_000
+ connection.readTimeout = 120_000
+
+ val body = buildRequestBody(prompt, maxTokens = 1024, temperature = 0.7f)
+ connection.outputStream.use { it.write(body.toByteArray()) }
+
+ val responseCode = connection.responseCode
+ if (responseCode != HttpURLConnection.HTTP_OK) {
+ val error = connection.errorStream?.bufferedReader()?.readText() ?: "Unknown error"
+ throw OnDeviceModel.InferenceException("Gemini streaming API error $responseCode: $error")
+ }
+
+ val fullText = StringBuilder()
+ BufferedReader(InputStreamReader(connection.inputStream)).use { reader ->
+ var line: String?
+ while (reader.readLine().also { line = it } != null) {
+ val l = line!!
+ if (!l.startsWith("data: ")) continue
+ val data = l.removePrefix("data: ").trim()
+ if (data == "[DONE]") break
+ try {
+ val token = parseChunkText(data)
+ if (token.isNotEmpty()) {
+ onToken(token)
+ fullText.append(token)
+ }
+ } catch (e: Exception) {
+ Log.w(TAG, "Failed to parse SSE chunk: $data", e)
+ }
+ }
+ }
+
+ fullText.toString()
+ } finally {
+ connection.disconnect()
+ }
+ }
+
+ override fun close() {
+ // No resources to clean up
+ }
+
+ private fun buildRequestBody(prompt: String, maxTokens: Int, temperature: Float): String {
+ return JSONObject().apply {
+ put("contents", JSONArray().apply {
+ put(JSONObject().apply {
+ put("role", "user")
+ put("parts", JSONArray().apply {
+ put(JSONObject().apply {
+ put("text", prompt)
+ })
+ })
+ })
+ })
+ put("generationConfig", JSONObject().apply {
+ put("maxOutputTokens", maxTokens)
+ put("temperature", temperature.toDouble())
+ })
+ }.toString()
+ }
+
+ private fun parseGenerateResponse(json: String): String {
+ return try {
+ JSONObject(json)
+ .getJSONArray("candidates")
+ .getJSONObject(0)
+ .getJSONObject("content")
+ .getJSONArray("parts")
+ .getJSONObject(0)
+ .getString("text")
+ } catch (e: Exception) {
+ throw OnDeviceModel.InferenceException("Failed to parse Gemini response: ${e.message}", e)
+ }
+ }
+
+ private fun parseChunkText(json: String): String {
+ return try {
+ JSONObject(json)
+ .getJSONArray("candidates")
+ .getJSONObject(0)
+ .getJSONObject("content")
+ .getJSONArray("parts")
+ .getJSONObject(0)
+ .optString("text", "")
+ } catch (_: Exception) {
+ ""
+ }
+ }
+
+ companion object {
+ private const val TAG = "GeminiCloudModel"
+ private const val BASE_URL =
+ "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash"
+ private const val STREAMING_URL =
+ "$BASE_URL:streamGenerateContent?alt=sse"
+ }
+}
diff --git a/app/src/main/java/com/pixel10/ai/inference/OnDeviceModel.kt b/app/src/main/java/com/pixel10/ai/inference/OnDeviceModel.kt
index 0489430..3036d43 100644
--- a/app/src/main/java/com/pixel10/ai/inference/OnDeviceModel.kt
+++ b/app/src/main/java/com/pixel10/ai/inference/OnDeviceModel.kt
@@ -8,16 +8,18 @@ import kotlinx.coroutines.withContext
/**
* Unified interface for on-device AI inference on the Pixel 10.
*
- * Supports two backends:
+ * Supports three backends, tried in order:
* 1. **Gemini Nano** via ML Kit Prompt API — uses the system-managed model
* through AICore, accelerated by the Tensor G5 TPU. Zero setup needed
- * on supported Pixel devices.
- * 2. **MediaPipe LLM** — for custom open-weight models (Gemma 2B/3n, etc.)
+ * on supported Pixel devices. Requires foreground context.
+ * 2. **Gemini Cloud** — proxies to Gemini 2.0 Flash via HTTPS. Works from
+ * any context (background, emulator). Requires an API key.
+ * 3. **MediaPipe LLM** — for custom open-weight models (Gemma 2B/3n, etc.)
* that you supply yourself. Place the .bin/.task file in the app's
* files directory.
*
- * The factory method tries Gemini Nano first (preferred), then falls back
- * to MediaPipe if a local model file is found.
+ * Pass [apiKey] to enable the cloud backend. If an API key is provided,
+ * cloud is preferred over Nano to guarantee background operation.
*/
interface OnDeviceModel {
val backendName: String
@@ -40,41 +42,58 @@ interface OnDeviceModel {
private const val TAG = "OnDeviceModel"
/**
- * Create the best available on-device model.
- * Tries Gemini Nano (AICore) first, falls back to MediaPipe.
+ * Create the best available model.
+ *
+ * Fallback chain:
+ * 1. GeminiNano — on-device, best quality, foreground only
+ * 2. GeminiCloud — network, always works (background + CI); needs [apiKey]
+ * 3. MediaPipe — local model file, fully offline
+ *
+ * If [apiKey] is non-empty, cloud is tried *before* Nano so the server
+ * stays responsive after the user navigates away from the app.
*/
- suspend fun create(context: Context): OnDeviceModel = withContext(Dispatchers.IO) {
- // Try Gemini Nano via ML Kit Prompt API first
- try {
- Log.i(TAG, "Attempting Gemini Nano via ML Kit Prompt API...")
- val nano = GeminiNanoModel.create(context)
- Log.i(TAG, "Gemini Nano ready!")
- return@withContext nano
- } catch (e: Exception) {
- Log.w(TAG, "Gemini Nano not available: ${e.message}")
- }
+ suspend fun create(context: Context, apiKey: String = ""): OnDeviceModel =
+ withContext(Dispatchers.IO) {
+ val hasKey = apiKey.isNotBlank()
- // Fall back to MediaPipe with a local model file
- try {
- Log.i(TAG, "Attempting MediaPipe LLM with local model...")
- val mediapipe = MediaPipeModel.create(context)
- Log.i(TAG, "MediaPipe model ready!")
- return@withContext mediapipe
- } catch (e: Exception) {
- Log.w(TAG, "MediaPipe model not available: ${e.message}")
- }
+ // Prefer cloud when an API key is available — guarantees background operation
+ if (hasKey) {
+ Log.i(TAG, "API key set — using Gemini Cloud for background-safe inference")
+ return@withContext GeminiCloudModel(apiKey)
+ }
- throw InferenceException(
- "No AI model available.\n\n" +
- "Option 1: Use a Pixel device with Gemini Nano support " +
- "(Pixel 10/9/8 series)\n\n" +
- "Option 2: Place a MediaPipe-compatible model (.bin or .task) in:\n" +
- " ${context.filesDir.absolutePath}/\n" +
- " Supported: gemma-3n-E2B.task, gemma-2b-it-gpu-int4.bin, etc.\n\n" +
- "Download models from:\n" +
- " https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/android"
- )
- }
+ // Try Gemini Nano via ML Kit Prompt API
+ try {
+ Log.i(TAG, "Attempting Gemini Nano via ML Kit Prompt API...")
+ val nano = GeminiNanoModel.create(context)
+ Log.i(TAG, "Gemini Nano ready!")
+ return@withContext nano
+ } catch (e: Exception) {
+ Log.w(TAG, "Gemini Nano not available: ${e.message}")
+ }
+
+ // Fall back to MediaPipe with a local model file
+ try {
+ Log.i(TAG, "Attempting MediaPipe LLM with local model...")
+ val mediapipe = MediaPipeModel.create(context)
+ Log.i(TAG, "MediaPipe model ready!")
+ return@withContext mediapipe
+ } catch (e: Exception) {
+ Log.w(TAG, "MediaPipe model not available: ${e.message}")
+ }
+
+ throw InferenceException(
+ "No AI model available.\n\n" +
+ "Option 1: Enter a Gemini API key in the app (works everywhere)\n\n" +
+ "Option 2: Use a Pixel device with Gemini Nano support " +
+ "(Pixel 10/9/8 series)\n\n" +
+ "Option 3: Place a MediaPipe-compatible model (.bin or .task) in:\n" +
+ " ${context.filesDir.absolutePath}/\n" +
+ " Supported: gemma-3n-E2B.task, gemma-2b-it-gpu-int4.bin, etc.\n\n" +
+ "Download models from:\n" +
+ " https://ai.google.dev/edge/mediapipe/solutions/genai/llm_inference/android"
+ )
+ }
}
class InferenceException(message: String, cause: Throwable? = null) :
diff --git a/app/src/main/java/com/pixel10/ai/server/ApiServerService.kt b/app/src/main/java/com/pixel10/ai/server/ApiServerService.kt
index 59bb199..3110172 100644
--- a/app/src/main/java/com/pixel10/ai/server/ApiServerService.kt
+++ b/app/src/main/java/com/pixel10/ai/server/ApiServerService.kt
@@ -3,6 +3,7 @@ package com.pixel10.ai.server
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
+import android.content.Context
import android.content.Intent
import android.os.Binder
import android.os.IBinder
@@ -64,9 +65,14 @@ class ApiServerService : Service() {
scope.launch {
try {
- // Load the on-device AI model
+ // Load the AI model — prefer cloud if an API key is configured
+ val apiKey = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ .getString(PREF_API_KEY, "") ?: ""
notifyLog("Loading AI model...")
- model = OnDeviceModel.create(applicationContext)
+ if (apiKey.isNotBlank()) {
+ notifyLog("API key configured — using cloud backend")
+ }
+ model = OnDeviceModel.create(applicationContext, apiKey)
notifyLog("Model ready: ${model!!.backendName}")
// Start the HTTP server
@@ -161,5 +167,7 @@ class ApiServerService : Service() {
const val EXTRA_PORT = "port"
const val DEFAULT_PORT = 8080
private const val NOTIFICATION_ID = 1
+ const val PREFS_NAME = "pixel10_prefs"
+ const val PREF_API_KEY = "gemini_api_key"
}
}
diff --git a/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt b/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
index b6b0305..12f9102 100644
--- a/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
+++ b/app/src/main/java/com/pixel10/ai/ui/MainActivity.kt
@@ -18,6 +18,8 @@ import androidx.core.content.ContextCompat
import com.pixel10.ai.R
import com.pixel10.ai.databinding.ActivityMainBinding
import com.pixel10.ai.server.ApiServerService
+import com.pixel10.ai.server.ApiServerService.Companion.PREF_API_KEY
+import com.pixel10.ai.server.ApiServerService.Companion.PREFS_NAME
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
@@ -65,6 +67,10 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermission()
+ // Restore saved API key
+ val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ binding.etApiKey.setText(prefs.getString(PREF_API_KEY, ""))
+
binding.btnToggle.setOnClickListener {
if (service?.isRunning == true) {
stopServer()
@@ -113,6 +119,13 @@ class MainActivity : AppCompatActivity() {
private fun startServer() {
val port = binding.etPort.text.toString().toIntOrNull() ?: 8080
+ // Persist API key before starting the service
+ val apiKey = binding.etApiKey.text.toString().trim()
+ getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
+ .edit()
+ .putString(PREF_API_KEY, apiKey)
+ .apply()
+
val intent = Intent(this, ApiServerService::class.java).apply {
action = ApiServerService.ACTION_START
putExtra(ApiServerService.EXTRA_PORT, port)
@@ -146,6 +159,7 @@ class MainActivity : AppCompatActivity() {
binding.btnToggle.text = getString(R.string.btn_start)
binding.btnToggle.isEnabled = true
binding.etPort.isEnabled = true
+ binding.etApiKey.isEnabled = true
}
ApiServerService.ServerState.LOADING_MODEL -> {
binding.tvServerStatus.text = getString(R.string.server_status_starting)
@@ -153,6 +167,7 @@ class MainActivity : AppCompatActivity() {
binding.tvModelStatus.text = getString(R.string.model_loading)
binding.btnToggle.isEnabled = false
binding.etPort.isEnabled = false
+ binding.etApiKey.isEnabled = false
}
ApiServerService.ServerState.RUNNING -> {
val port = binding.etPort.text.toString()
@@ -164,6 +179,7 @@ class MainActivity : AppCompatActivity() {
binding.btnToggle.text = getString(R.string.btn_stop)
binding.btnToggle.isEnabled = true
binding.etPort.isEnabled = false
+ binding.etApiKey.isEnabled = false
}
ApiServerService.ServerState.ERROR -> {
binding.tvServerStatus.text = getString(R.string.server_status_error)
@@ -172,6 +188,7 @@ class MainActivity : AppCompatActivity() {
binding.btnToggle.text = getString(R.string.btn_start)
binding.btnToggle.isEnabled = true
binding.etPort.isEnabled = true
+ binding.etApiKey.isEnabled = true
}
}
}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 0163ff0..28e7bdb 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -138,6 +138,37 @@
android:gravity="center" />
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 768c684..6843130 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -22,6 +22,8 @@
Stop Server
Port:
8080
+ Gemini API Key (optional, for background use)
+ AIza…
Requests served: %d
Request Log