LocalLLM v1.1.0
Production-readiness pass + tab-by-tab UX overhaul. Inference layer is
unchanged; this is all the operational and visual scaffolding around it.
Added
Release & build
- R8 + resource shrinking on the release buildType. ProGuard rules
already covered LiteRT-LM, Ktor, Netty, Gson, Compose — no new keep
rules surfaced.:app:assembleReleaseand:app:bundleReleaseboth
green. - Per-ABI APK splits.
arm64-v8aonly (LiteRT-LM 0.11.0 ships JNI
.sofiles forarm64-v8a+x86_64only — noarmeabi-v7a). The
arm64-v8a release APK is ~28 MB, the universal is ~39 MB, the
.aabis ~33 MB. signingConfigs.releasereading from~/.gradle/gradle.properties
or environment (LOCALLLM_KEYSTORE_PATH/_PASSWORD/_ALIAS/
_PASSWORD). Gracefully falls back to the debug signing key when any
of the four is missing — so contributors run:app:assembleRelease
without needing the production keystore.scripts/release.sh— one-command release: assembleDebug + mkdocs
gh-deploy + tag + push +gh release createwith notes scraped from
this CHANGELOG..github/workflows/docs.yml— Material site build + Pages deploy,
triggered ondocs//mkdocs.ymlchanges. Pages currently sourced
from thegh-pagesbranch (legacy mode) because GitHub Actions is
administratively restricted on the hosting account — the workflow
auto-resumes once Actions is re-enabled..github/dependabot.yml— weekly Monday updates for gradle,
github-actions, and the pip-based docs requirements; Compose / Kotlin /
Ktor each in their own update group; LiteRT-LM explicitly pinned
(manual bumps only — model-side smoke test required).
Performance & lifecycle
- Baseline Profiles via a new
:macrobenchmarkmodule
(com.android.test+androidx.baselineprofile).StartupBenchmark
measures cold-start underCompilationMode.None / Partial / Full;
BaselineProfileGeneratorwalks Catalog → Dashboard → Console → Chat
→ Settings. Run on a device with
./gradlew :app:generateReleaseBaselineProfile. onTrimMemoryengine eviction.RUNNING_LOW / MODERATEshrinks
the engine LRU to 1;RUNNING_CRITICAL / COMPLETEevicts everything.
Both gated byinferenceMutex.tryLockso eviction never interrupts
an active request.Lifecycle.Event.ON_STARTre-kick inMainActivity. If the OS
killed the foreground service while the Activity was backgrounded
and autostart is on, the service comes back up the next time the
user returns to the app.START_STICKYcontract documented ononStartCommand.
AUTO backend with real fallback
-
AUTO now tries
Backend.GPUfirst; onEngine.initialize()failure
(the common case on stock Pixel images missinglibvndksupport.so),
logs a warning and rebuilds onBackend.CPU. Explicit CPU / GPU
selections stay strict (no fallback) so the user can debug them. -
New
enginesarray inGET /healthsurfaces the backend each
cached engine actually initialized on:"engines": [ { "key": "gemma-4-e2b_model_AUTO", "backend": "CPU" } ]
Settings layer
SettingsRepositorybacked byandroidx.datastore.preferences: 1.1.1withSharedPreferencesMigration("settings")so existing prefs
carry over. Compose UI observesStateFlows instead of re-reading
SharedPreferences on every recomposition (slider drag was triggering
~60 disk reads/sec before).- Public
Settings.xxx(context)API preserved byte-for-byte — every
existing caller (LLMServerService, BootReceiver, etc.) keeps working
unchanged.
Debug-build hygiene
StrictModethread + VM policies installed underBuildConfig. DEBUG.detectDiskReads / detectDiskWrites / detectNetwork / detectLeakedClosableObjects / detectActivityLeaks, all with
penaltyLogonly — neverpenaltyDeath.
Catalog tab (UX overhaul)
LinearProgressIndicatorwith"X.X MB / Y.Y GB"subtitle and
inlineCancel(Icons.Outlined.Close) — replaces the text-only
percentage.- SHA-256 verified badge (
Icons.Outlined.Verifiedfor built-ins
with a known hash;Icons.Outlined.Infofor custom URLs). - File size + last-used relative time on installed models, via
Formatter.formatShortFileSizeandDateUtils.getRelativeTimeSpanString. - "Get started" hero card when nothing is installed yet.
- OutlinedCard hierarchy with proper M3 spacing, icons on every
action (Download,Delete,UploadFile,Close).
Chat tab (markdown + visual polish)
MarkdownTextcomposable backed byorg.commonmark:commonmark: 0.22.0— renders assistant messages with code blocks, lists (capped
at depth 2), inline code, headings, bold/italic, block quotes, and
links. Code blocks have a copy-to-clipboard icon. No WebView.- Bubble overhaul: role icons (
Icons.Outlined.Person/
Icons.Outlined.AutoAwesome), right-aligned timestamps, asymmetric
rounded corners, 90% max-width,primaryContainervssurfaceVariant
backgrounds. - Streaming reveal animation:
Animatablefades trailing delta
characters from 0.5α to full opacity overtween(200ms). Swaps to
MarkdownTextrendering once streaming completes. - Empty-state hero:
Icons.Outlined.AutoAwesome56dp + title + body- 4
AssistChipsample prompts. Tap a chip to fill the input — never
auto-sends.
- 4
- Send / Stop buttons get icons (
AutoMirrored.Outlined.Send,
Icons.Outlined.StopwitherrorContainercolors). UiMessage.timestampMsfield added (default-valued, backwards
compatible).
Settings tab (restructure + Pixel-6 awareness)
- Six collapsible domain sections with leading icons: Server
(Dns, expanded by default), Inference (Memory), Security (Lock),
Background (Battery5Bar), Limits (Speed), Startup
(PowerSettingsNew). Animated chevron rotation. - Per-row Help expandables (
Icons.Outlined.HelpOutline) — tap to
toggle inline description without crowding the surface. - Backend description rewrite: removed the old MediaPipe / Pixel 10 /
Tensor G5 / "NPU auto" claims. New copy describes AUTO as
GPU-first-then-CPU fallback, CPU as ~6–12 tok/s on Pixel-class
hardware for Gemma 4 E2B, GPU as strict-no-fallback. The selected
mode's line gets a primary-container-tinted background. - Chipset hint above the backend selector, driven by
Build.SOC_MODEL
(API 31+). Renders "Your device: Pixel 6 (Tensor). GPU delegate often
fails; AUTO will fall back to CPU." on Tensor SoCs (gs101+),
"…(Snapdragon). NPU variant.litertlmfiles in the catalog should
work." on Snapdragon, otherwise "AUTO is the safe choice." - Port-in-use validator:
ServerSocket(port).also{close}attempt
500 ms after the port field changes. OnIOExceptionthe field
shows error-tinted helper text without blocking save.
Dashboard tab
- 2×2 stat-card grid with leading icons: Total / Avg latency / Avg
tok/s / Error rate. Error rate severity-colored (green <1%, amber <5%,
error >5%). - Tok/s sparkline via pure Compose
Canvas— no chart library
added. Catmull-Rom → cubic Bezier smoothing, 20%-alpha fill under
the line, max-Y label top-right. Handles empty history / single
point / NaN / all-zeros cleanly. - Promoted in-flight card with rotating
Icons.Outlined.Boltand
indeterminateLinearProgressIndicator. Collapses to "Idle" with
Icons.Outlined.Pausewhen nothing is running. - Status-icon history rows:
CheckCircle/Cancel/Error
leading icons. Tap to expand and see full request details inline.
Console tab
- Debounced search (300 ms via
snapshotFlow + debounce) with
Icons.Outlined.Searchleading icon andIcons.Outlined.Close
clear-query trailing icon. - Level FilterChips (DEBUG / INFO / WARN / ERROR) — each chip's
leading dot is colored to match its corresponding log-level text
color. - Top-5 tag FilterChips parsed from
[tag] messageprefixes, with
a "More…" overflow dropdown when the buffer has more than 5 distinct
tags. - Auto-scroll toggle (
Icons.Outlined.VerticalAlignBottom). - Color-coded log lines by level.
- Long-press copy writes the full
[time] LEVEL messageline to
the clipboard with a "Copied" toast. - "No matching log entries" empty state with a "Clear filters"
TextButton.
Chrome restructure (header + tabs + theme)
Scaffoldlayout replacing the bespokeColumn { Header + ScrollableTabRow + Box }. The old 2-row LIVE banner (~120dp of
vertical chrome) is gone.- Compact
CenterAlignedTopAppBar(56dp): status dot in the
leading slot, middle-ellipsized URL as the title, context-aware
trailing actions (Tune + Refresh on Chat tab; Copy URL elsewhere). - Top
ScrollableTabRow→ bottomNavigationBarwith proper M3
icons (FolderOpen/BarChart/Terminal/
AutoMirrored.Outlined.Chat/Settings). Better one-handed reach
on a 6.4" phone, more content above the fold. - Palette overhaul: primary desaturated
#4ECDC4 → #6BD3CC,
full M3 surface tonal scale (background#0E1113, surface
#14181A, surfaceVariant#222729), brand teal reserved for the
status dot, primary CTAs, progress, and user-message bubbles.
WCAG-AA contrast verified. Header.kttrimmed to aStatusDot(status)helper used by the
app bar's leading slot.- Chat bubble redo: assistant messages are now borderless
full-bleed text with a 3dp primary-tinted left rail (no card
outline); user messages are tighter right-aligned pills (80%
max-width, 20dp radius). Role icons removed — alignment + tint
carry the signal. - Chat input row: rounded
Surfacecontaining a borderless
BasicTextFieldand one circular Send/Stop button that swaps icon- tint based on
isChatting. No moreOutlinedTextFieldchrome.
- tint based on
- System prompt moved out of the chat body into a
ModalBottomSheet, reachable from either the app-barTuneicon
or an inline edit icon on a persistent strip (only shown when a
prompt is set). - Model selector: replaced the labelled
OutlinedTextFieldwith
a compactAssistChip+DropdownMenu. Live tok/s collapses to a
chip on the same row when streaming.
General
androidx.compose.material:material-icons-extendeddep
(BOM-managed, no version pin) — now available app-wide for the new
iconography across every tab.CHANGELOG.mdintroduced (Keep a Changelog 1.1.0 format).
Changed
- JDK source / target bumped 1.8 → 11 to silence AGP 8.7 deprecation
warnings (Kotlin source target was already 11). AndroidManifest.xmlforegroundServiceType:dataSync→
specialUsewith the Play-required
PROPERTY_SPECIAL_USE_FGS_SUBTYPEjustification declaring on-device
inference. TheFOREGROUND_SERVICE_DATA_SYNCpermission swapped for
FOREGROUND_SERVICE_SPECIAL_USE.- ProGuard rules retargeted from MediaPipe keeps to LiteRT-LM
(com.google.ai.edge.litertlm.**). Old MediaPipe + protobuf entries
removed. - Pages deploy mode flipped from "GitHub Actions" to "Deploy from a
branch (gh-pages)" — works around the account-level Actions
restriction. Thedocs.ymlworkflow stays in tree for when Actions
is re-enabled.
Removed
- Unused imports:
com.google.ai.edge.litertlm.Roleand
kotlinx.coroutines.flow.collectinLLMServerService.kt;
kotlinx.coroutines.flow.mapinSettingsRepository.kt. - Unused
kotlinx-serialization-jsondependency and the corresponding
kotlinSerializationGradle plugin alias (the project uses Gson
exclusively).
Fixed
FAILED_PRECONDITION: A session already existson
engine.createConversation(). LiteRT-LM's Engine enforces at most
one activeConversationper engine, but our sessions LRU (size 4)
could hold multiple cached conversations against the same engine,
and stateless conversations whoseclose()didn't fully propagate
before the next request would also leave the slot occupied. Both
manifested as HTTP 500 on the second or third inference request.
NewactiveConversations: ConcurrentHashMap<String, Conversation>
tracks the single live conversation per engine; the new
purgeConversationsOnEngine(engineKey)helper closes every
conversation we know about on a given engine before constructing a
new one. Defensive: ifcreateConversationstill throws "session
already exists" (race), force-evict the engine and surface a
retry-able error.- Stale source-code references to MediaPipe /
tasks-genai/
Pixel 10 / Tensor G5 / NPU-as-universal /LlmInferenceSession/
addQueryChunk. Final grep acrossapp/src/main/java/com/localllm/ app/**returns zero hits. Settings.ktKEY_BACKENDcomment — replaced false claim that
AUTO passesBackend.DEFAULTto MediaPipe (it doesn't, since the
migration) with accurate GPU-first-then-CPU fallback semantics.ApiTypes.ktsessionIdKDoc — replaced
LlmInferenceSession/addQueryChunkdescription with the
LiteRT-LMConversation-based reality.
Known issues
- GitHub Actions administratively restricted on the hosting account
pending Trust & Safety review.build.ymlanddocs.ymlworkflows
are dormant; CI parity is enforced locally
(./gradlew lint testDebugUnitTest assembleDebug). Docs deploy works
manually viamkdocs gh-deploy --force --remote-branch gh-pages. - GPU backend init fails on stock Pixel images missing
libvndksupport.so. AUTO transparently falls back to CPU. On Pixel 6
/ Tensor G1 this is the expected path. CPU + XNNPACK gives ~6–12
tok/s on Gemma 4 E2B. - Multi-process isolation (
android:process=":server"for
LLMServerService) deferred — would require IPC for the cross-process
singletons (ServerState,RequestTracker,LogManager). Tracked
but not in this release.