Skip to content

Add LLM Service: fully-offline on-device Android chat app#310

Merged
bernardladenthin merged 2 commits into
mainfrom
claude/java-llama-android-example-q7hiuv
Jul 9, 2026
Merged

Add LLM Service: fully-offline on-device Android chat app#310
bernardladenthin merged 2 commits into
mainfrom
claude/java-llama-android-example-q7hiuv

Conversation

@bernardladenthin

Copy link
Copy Markdown
Owner

Summary

  • New standalone Android app (android-llmservice/) — a KISS, fully-offline local-AI chat demo consuming the llama-android AAR and llama-kotlin coroutines façade
  • Single-screen Compose UI: pick a GGUF model via Storage Access Framework, stream chat replies fully on-device, switch UI language via a 13-language flag picker, and save/load conversations to private local storage
  • No permissions: no INTERNET, no storage permission — nothing leaves the device
  • CI integration: new build-android-llmservice job builds a release App Bundle, runs end-to-end instrumented UI tests on the emulator, and validates the full "real app" story

Details

The app demonstrates the complete integration of the llama-android AAR and llama-kotlin façade in a production-shaped Android application:

  • MainActivity.kt — Compose UI entry point with SAF file picker, language menu, and session save/load buttons
  • ChatViewModel.kt — Holds the loaded LlamaModel, drives streaming chat via generateChatFlow, manages model lifecycle and error handling
  • SessionStore.kt — Persists conversations to private app-internal storage (JSON, no upload)
  • Languages.kt — 13 supported languages (English, German, Spanish, French, Italian, Portuguese, Russian, Turkish, Arabic, Hindi, Chinese, Japanese, Korean) with flag emoji picker and per-app locale persistence
  • Localized resourcesvalues-*/strings.xml for all 13 languages, locales_config.xml for Android 13+ per-app language integration
  • Build config — Plain Gradle/AGP (not a Maven reactor module), minSdk 28, release signing support via env vars / properties, R8 minification with AAR consumer ProGuard rules
  • Instrumented testChatFlowInstrumentedTest.kt drives the actual UI (type prompt, tap Send) against real on-device inference, validates token streaming

The app is not published to Maven Central (it is a runnable app, not a library) but is built and tested by CI on every push/PR to validate the AAR + façade integration end-to-end.

Test plan

  • CI build-android-llmservice job passes: builds release AAB, runs instrumented UI test on emulator
  • Instrumented test validates token streaming from real on-device inference
  • No new unit tests needed (app is UI + integration; the core inference logic is tested in llama-kotlin)

Related issues / PRs

Completes the "real app" story for the llama-android AAR and llama-kotlin façade.

Checklist

  • I have read CONTRIBUTING.md and CODE_OF_CONDUCT.md
  • My commits follow Conventional Commits
  • No security-sensitive changes

https://claude.ai/code/session_01HTS5FpMtBLJENTGoRUrp5m

claude added 2 commits July 9, 2026 11:33
…I emulator UI test

A standalone plain-Gradle/AGP example app (llama-android-example/) that
consumes the shipped llama-android AAR + llama-kotlin coroutines facade:
one Jetpack Compose screen that picks a GGUF from the file system (Storage
Access Framework, copied to filesDir since llama.cpp mmaps a real path) and
streams an on-device chat reply via generateChatFlow. No Android Studio
required — plain Gradle/AGP builds a Play-shaped signed release .aab.

Signing: release signingConfig reads an upload keystore from env/-P (Play
App Signing model — the Maven Central GPG key cannot sign an APK/AAB), with
a debug-signing fallback so forks/PRs/local builds stay green without secrets.

CI: new build-android-example job in publish.yml builds the signed .aab and
runs a real on-device UI test on the KVM x86_64 emulator (type a prompt, tap
Send, assert a non-empty streamed reply), reusing the cached draft model. Not
yet a publish gate — promote after it runs flake-free (same policy as
test-android-emulator).

Docs: example README (build/sign/Play/test walkthrough), root README pointer
under 'Importing in Android', and a CLAUDE.md section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTS5FpMtBLJENTGoRUrp5m
…r + private save/load

Rebrand the Android example app to a generic, trademark-safe identity:
- App label "LLM Service"; applicationId/namespace/package net.ladenthin.android.llmservice
- Folder llama-android-example/ -> android-llmservice/ (CI job build-android-llmservice,
  run-android-llmservice-test.sh, android-llmservice-aab artifact, gitignore/docs updated)

Add three features:
- Internationalization: every UI string externalized; translations for 13 languages
  (en + de/es/fr/it/pt/ru/tr/ar[RTL]/hi/zh-CN/ja/ko), locales_config.xml.
- In-app flag language picker (top-bar dropdown) via AppCompatDelegate.setApplicationLocales,
  persisted across restarts (AppCompat autoStoreLocales; MainActivity now AppCompatActivity).
  The ViewModel survives the locale-change recreation, so the chat is not lost.
- Private local save/load session: Save/Load buttons persist the conversation + model path
  to filesDir/session.json (org.json, no new dep); readable only by the app, nothing uploaded.

Docs note Gemma 3 4B Instruct as the recommended real on-device model. The instrumented
Compose UI test moves to the new package and still drives the real on-device flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HTS5FpMtBLJENTGoRUrp5m
@bernardladenthin bernardladenthin merged commit f6878d8 into main Jul 9, 2026
9 of 13 checks passed
@bernardladenthin bernardladenthin deleted the claude/java-llama-android-example-q7hiuv branch July 9, 2026 20:07
Comment on lines +15 to +44
<application
android:allowBackup="true"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:supportsRtl="true"
android:theme="@style/Theme.LlmService">

<!--
Enables AppCompat's per-app language persistence (AppCompatDelegate
.setApplicationLocales): the flag language picker's choice survives restarts and
integrates with the Android 13+ system per-app language setting.
-->
<service
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
android:enabled="false"
android:exported="false">
<meta-data
android:name="autoStoreLocales"
android:value="true" />
</service>

<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants