Foyer Design Guide¶
What Foyer is, the principles behind it, and the technical rules the codebase actually follows. The feature direction synthesizes the best of Niagara Launcher (efficiency, single-list UX) and Minimalist Phone (distraction reduction, text-based focus), plus the thing neither has: a fully on-device AI assistant.
1. Principles¶
- Text-first, lowercase, calm. App labels as plain text, no icon grid, no widgets, no color noise. The launcher should feel like a quiet foyer, not a casino floor.
- AI decides, code executes. Natural-language commands (typed or spoken) are interpreted by an on-device model that returns a structured action; plain code parses and executes it. Obvious commands short-circuit through a deterministic resolver and never touch the model. No hardcoded regex/keyword intent rules in the semantic path.
- Everything stays on the phone. No server, no account, no analytics. The model runs locally; airplane mode changes nothing.
- Bring your own model. Foyer never ships or hosts model weights. The user
pastes a MediaPipe
.taskURL (one-tap suggestions provided) and downloads it under the model's own license. - Friction where it helps, nowhere else. Distraction features (limits, focus mode, nudges) add deliberate pauses; everything else is instant.
2. Feature Map¶
Shipped¶
- Single-list home: favorites (max 10) + two corner shortcuts, drawer one swipe away.
- Alphabetical fast scroll in the drawer, configurable: icon-preview card or simple rail.
- App hiding, and user-taught aliases (also used by the AI resolver).
- Distraction tools: mark-distracting apps, per-app daily limits with a conscious 5-minute bypass and a limit-reached notification nudge (inexact alarm + fire-time usage re-check — no service, no foreground watching), focus mode with a countdown, usage summary (screen time, top apps, unlocks).
- On-device assistant: voice ("ask me") + typed command bar; multi-turn conversation, disambiguation questions, inventory answers from the local app index.
- Work/private profile apps, backup/restore to a user-chosen JSON file, curated background tones, digital/analogue clock, gestures (swipe up/down, double tap, lock screen via accessibility service).
Inspiration backlog (not implemented, not committed to)¶
- Embedded notification previews under app names (Niagara) — would require
NotificationListenerService; weigh the privacy story first. - Notification filtering (Minimalist Phone) — same dependency.
- Grayscale/monochrome icon rendering where icons appear at all.
3. Architecture (as built — deliberate choices)¶
com.foyerlauncher.foyer/
├── MainActivity, LockService # manifest components stay at the root
├── ui/screens, ui/components, ui/theme
├── data/ # LauncherStore, Apps, UsageTracker
└── ai/
├── engine/ # Assistant, LlmEngine, ModelDownloader, AiMetrics
└── router/ # deterministic tier: FastRouter, CommandRouter,
# AppIndex, AppCatalog, ActionRegistry, Normalizer
- Single activity, Compose-only, unidirectional data flow. State flows
down, events go up as lambdas. Home surfaces are foundation-only Compose;
settings pages use Material 3 Expressive components themed entirely from
the Foyer palette via
FoyerMaterialTheme(ui/theme) — never default M3 purple. - Plain state holder, no ViewModel.
LauncherStoreis a snapshot-state class persisted to SharedPreferences. This is intentional: the activity handlesconfigChangesitself (standard launcher practice), a home app's process lives as long as the device, and persisted state reloads on process death. Don't add ViewModel/Hilt/Room/DataStore to "modernize" — they solve problems this app doesn't have. Revisit only if a concrete need appears (e.g. multi-screen navigation with per-screen state). - Dependency direction:
ui → data/ai,ai → data,data → nothing internal. Keep it that way; the model types (AppEntry) live indata. - AI layering:
ai/routeris the deterministic tier (no model, instant);ai/engineis the model tier (LlmEnginewraps MediaPipeLlmInferencefor whatever.taskmodel the user downloaded).Assistantorchestrates: router first, model as semantic fallback, web search as last resort.
4. Technical Dos¶
- Do enumerate apps via
LauncherApps+UserManager(not barePackageManager) — it's the only way to see work/private-profile apps — and reload on package callbacks, off the main thread. - Do keep heavy work (usage stats, model inference, downloads) in
coroutines on
Dispatchers.IO; the home screen must never jank. - Do gate sensitive permissions behind an explanation at point of use (usage access, microphone, accessibility), never up-front.
- Do keep persisted encodings stable (
AppEntry.key, prefs, backup JSON schema) — migrate explicitly when they must change. - Do keep memory low: the launcher is resident. Prefer lazy init (model weights load on first AI use, not at launch).
- Do log AI decisions under the
FoyerAItag so routing (deterministic vs model) stays debuggable:adb logcat -s FoyerAI:V.
5. Technical Don'ts¶
- Don't reintroduce a bundled or self-hosted model, or a second AI backend. One engine, user-supplied model.
- Don't add hardcoded intent regexes to the semantic path — teach the deterministic router or improve the prompt instead.
- Don't block the main thread with package queries, prefs migration, or file IO.
- Don't add persistent foreground services. The one service (
LockService) is an accessibility service that processes no events; keep it that honest. - Don't introduce gestures that fight system back/home navigation.
- Don't clutter the home surface: no widgets, no icon grids, no wallpaper rendering of our own.
- Don't use Material components on home/drawer surfaces — M3 is scoped to settings and utility pages; the home screen stays foundation-only.
6. Actual Tech Stack¶
| Component | Choice |
|---|---|
| Language / UI | Kotlin, Jetpack Compose (foundation for home; Material 3 Expressive for settings, themed from the Foyer palette) |
| State | Compose snapshot state in a plain store class |
| Persistence | SharedPreferences (+ user-exported JSON backup) |
| App enumeration | LauncherApps across all user profiles |
| On-device LLM | MediaPipe tasks-genai LlmInference, BYO .task model |
| Speech input | Android SpeechRecognizer (on-device where available) |
| Usage data | UsageStatsManager (opt-in usage access) |
7. Known Gaps (pre-release checklist material)¶
- Localization: almost all UI text is hardcoded lowercase English;
extraction to
strings.xmlis required before any localization pass (only the app name and accessibility-service strings are externalized today). LauncherScreenholds ten independent overlay booleans; collapse into one sealedOverlaystate when next touched.- Qwen/Gemma chat templating in
LlmEngineuses Gemma-style turn tokens; verify template behavior per model family as more suggestions are added.