# Technical Decisions Record > Vibe Reader - Architecture Decision Records (ADRs): the "why" behind significant technical choices > Last Updated: 2026-08-26 · Ordered newest first (add new TDRs at the top) ## Format Each TDR: **Context** → **Options Considered** → **Decision** → **Consequences**, with **Status:** `Accepted` | `Superseded` | `Proposed`. --- ## TDR-014: Durable Local Data Across Dev Builds (Migrations + Signing Parity) **Date:** 2026-08-26 · **Status:** Accepted (applied 2026-08-26; first-build verification pending) · **Related:** [[#TDR-001]], [[#TDR-011]], [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-012|Bug Tracker > BUG-012]] ### Context Routine dogfooding and a growing tester population mean the app is now accumulating data worth keeping - and rapid iteration keeps destroying it. Two independent causes wipe the Room database between builds: 1. **Destructive migration fallback.** TDR-001 shipped with `fallbackToDestructiveMigration()`: every schema version bump nukes the database and rebuilds it empty. Fine for a solo debug loop; fatal for anyone's accumulated library. 2. **Signing mismatch between install sources.** Android Studio signs with the local debug keystore; CI (TDR-011) signs with the runner's generated one. Installing one over the other fails the signature check, forcing an uninstall - which wipes app data. The two redeploy paths silently cannot coexist on one device. A normal reinstall (`Run` / `adb install -r`) preserves `/data/data/` - persistence is the default. We opted out of it twice without noticing. ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Status quo** | Keep destructive fallback; testers re-seed | Zero effort | Kills every tester's library on schema change; trust-ending | | **B. Room auto-migrations** | `exportSchema = true` + `autoMigrations`; hand-written `Migration` only for complex changes | Room generates ALTERs for additive changes; low ongoing cost | Schema JSON files must be committed; discipline required | | **C. Export/import layer only** | JSON backup/restore, no migrations | Also covers device swaps | Manual step; does not fix the default path | ### Decision **Option B, plus signing parity, plus C as the escape hatch:** 1. **Real migrations.** Enable `exportSchema = true` (schema location wired into KSP args, JSON committed to the repo) and declare `autoMigrations` for additive changes. `fallbackToDestructiveMigration()` is removed. The upcoming `weekly_vibes` table is the first migration written under the new regime. 2. **One debug keystore everywhere.** The local `debug.keystore` goes into a CI secret (base64); the workflow signs with it, so CI artifacts and Android Studio builds update over each other cleanly. 3. **Debug-only export/import.** A JSON dump/restore of the full database as the disaster-recovery path (and the seed of the user-facing "Export Session" backlog item). ### Consequences - ✅ "Reupload without losing the library" becomes true in both directions (local ↔ CI builds) - ✅ Tester data survives schema evolution - a precondition for expanding the tester population - ✅ Committed schema history doubles as documentation of the data model's evolution - ⚠️ Never reuse a schema version number once shipped to any tester; version bumps become deliberate acts - ⚠️ Keystore-in-CI is a (low-value, debug-only) secret to manage - 📋 Verification: build N → capture data → build N+1 with a schema change → data intact, from both install paths --- ## TDR-013: Bubbles API for the Roaming Capture Surface **Date:** 2026-08-12 · **Status:** Accepted (Slice 2; not yet built) · **Related:** [[#TDR-008]], [[#TDR-012]] ### Context Roaming needs a capture trigger that lives outside the app while the phone is *unlocked* and in use (a Whispr Flow-style floating chip). TDR-008 dismissed the Bubble API for the *reading* flow because bubbles collapse when the device locks. Roaming inverts that constraint: out and about, the phone is typically unlocked, and lock screen support is not the requirement. ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Bubbles API (API 30+)** | Conversation notification + bubble metadata; the OS renders the floating chip; tapping expands the capture activity in a small window | Play-Store-safe; OS-managed UX; the stronger portfolio story | API 30+ only; OEM/launcher inconsistency risk | | **B. SYSTEM_ALERT_WINDOW overlay** | Draw-over-apps floating button | Works everywhere | Scary permission; Play Store scrutiny; hand-rolled window management | ### Decision **Option A: Bubbles API.** The overlay route is held only as a fallback experiment if bubbles prove flaky on real devices. Implementation shape (Slice 2): conversation shortcut, bubble metadata on the roam notification, capture activity presented in the bubble window. The toggle lives in the new Settings pane. ### Consequences - ✅ Sanctioned API; no draw-over-apps permission; C2C 1 while the phone is in use - ✅ Complements rather than replaces the reading-session triggers (tile / MediaStyle) - ⚠️ Requires the Settings pane to exist (roaming/bubble toggle; also the natural migration home for code-level constants like `USE_MEDIA_STYLE`) - ⚠️ Bubble behavior varies by OEM and launcher; needs real-device validation --- ## TDR-012: Roaming as a Reserved Book (Day-Rollup Sessions) **Date:** 2026-08-12 · **Status:** Accepted (Slice 1 built; pending device test + commit) · **Related:** [[#TDR-001]], [[Project - Vibe Reader/Build & Iterate/Feature Backlog|Feature Backlog]], [[Project - Vibe Reader/Build & Iterate/User Feedback#Session-006|User Feedback > Session-006]], Product Log 2026-08-12 ### Context Roaming ("Background Vibe") extends capture beyond the book: a word in a podcast, a phrase in conversation, a line from an article. The question was how to store captures that do not belong to a book without fracturing the Book → Session → Captures hierarchy (TDR-001). ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Nullable book_id** | Captures allowed to exist with no book | Minimal concept count | Every query and view grows a null branch; breaks drill-down assumptions | | **B. Parallel storage path** | Separate roam tables and views | Clean separation | Duplicated UI + DAO logic; two systems to maintain | | **C. Reserved Book row** | Roaming is a special Book; its sessions are calendar days | Entire existing hierarchy works untouched | Reserved title needs routing and shelf exclusions | ### Decision **Option C: Reserved Book row with day-rollup sessions.** - `ROAMING_BOOK_TITLE` reserved row; `startRoam()` implements day rollup: one roam session per calendar day, and pausing + resuming on the same day rejoins that day's roam - Sessions are days ("Aug 12"); days become stops on the trail; book detail view, session metrics, and drill-down all work unchanged - No schema migration needed: Words/Quotes already carry `book_id` - Library entry points: slim Roaming bar + All Captures bar pinned topmost, visually separated from book cards; Roaming excluded from the book shelf and Resume chips ### Consequences - ✅ Zero migration; zero parallel code paths; full reuse of hierarchy, metrics, and drill-down - ✅ All Captures view falls out naturally (`getAllWordsWithBook` / `getAllQuotesWithBook` joins, book pills on cards) - ⚠️ Reserved title requires routing (typing "Roaming" as a book title routes to the roam flow) and shelf exclusions - ⚠️ Duration is misleading for roams (spans include pause gaps): hide or reframe for the Roaming shelf - 📋 Open: does a roam "end," or is it ambient until midnight? Current build: manual end, same-day restart resumes - 📋 Open: roams appear in the History trail (they are sessions); consider distinct styling --- ## TDR-011: Remote Iteration, CI Compilation (The Build Pipeline) **Date:** 2026-08-01 · **Status:** Accepted · **Related:** [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-R011|Bug Tracker > BUG-R011]], Product Log 2026-08-01 ### Context Code iteration happens in a cloud workspace that cannot compile Android: Google's SDK and Maven servers are blocked at the network layer. Verification there is static only (code review, ktlint parse-checks). A reliable compile-and-deploy loop was needed that does not depend on a hand-configured local toolchain being awake. ### Decision - **GitHub Actions CI:** every push to main builds a debug APK and uploads it as an artifact. The redeploy loop is push → wait → sideload. - **Static gate in the cloud:** ktlint parse-checks before push (they caught two real errors from the SessionComponents split; the gate earned its keep). - **Repo hygiene to protect the loop:** `.gitattributes` + autocrlf, ending the CRLF phantom-diff class of problem for good (see [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-R011|Bug Tracker > BUG-R011]]). ### Consequences - ✅ Fast iteration where iteration is cheap; guaranteed compilation where it is reliable - ✅ Every push produces an installable artifact, no local build required - ⚠️ The compile gate is deferred: type errors surface at first local build or CI run, not edit time - 📋 Optional: add `GEMINI_API_KEY` as a repo secret so CI builds ship with a working Weekly Vibe --- ## TDR-010: MediaStyle "Tuxedo" Reinstated as a Flagged Experiment **Date:** 2026-08-01 · **Status:** Accepted (device verification pending) · **Related:** [[#TDR-002]], [[#TDR-008]], Product Log 2026-08-01 ### Context TDR-008 concluded the MediaSession "Trojan Horse" was unsustainable and moved the app to the Quick Settings Tile + standard notification. That remains the proven baseline. But the C2C math never stopped nagging: the tile costs 2 taps; a lock screen ▶ costs 1. The February failures may have had a specific, addressable cause: the notification was not a *convincing enough* media player for the OS to render its controls. ### Decision Re-implement the MediaStyle notification with the full costume, behind a one-line fallback flag: - Full metadata: title (book name), artist line ("tap ▶ to capture"), album art - Active session held at `PlaybackState.STATE_PAUSED` so the lock screen shows a prominent ▶ - ▶ launches Smart Capture; Stop ends the session - `USE_MEDIA_STYLE` flag falls back to the proven standard notification if the widget goes invisible again ### Consequences - ✅ Restores a credible path to C2C 1 from the lock screen without betting the product on it - ✅ One-line rollback; the tile remains the reliable floor either way - ⚠️ Unverified on device: the dogfood test (does ▶ render on the Pixel lock screen?) decides the capture entry-point story - ⚠️ Still platform-adjacent territory; Google may tighten further (TDR-008's analysis stands) - 📋 The flag belongs in the upcoming Settings pane rather than in code (see [[#TDR-013]]) --- ## TDR-009: Asymmetric Verification (Words vs. Quotes) **Date:** 2026-02-09 · **Status:** Accepted · **Related:** [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Skip Confirmation|Feature Backlog > Skip Confirmation]], [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Auto-Dismiss Definition|Feature Backlog > Auto-Dismiss Definition]], [[Project - Vibe Reader/Build & Iterate/User Feedback#Session-001|User Feedback > Session-001]] ### Context Smart Capture mode required verification (VERIFYING state) for all inputs, whether 1-word definitions or multi-word quotes. User feedback (Session-001) flagged this as unnecessary friction for words. The question: should verification be skipped for all modes, or only for words? ### Decision **Asymmetric verification: skip for words, keep for quotes.** Words skip the VERIFYING screen and go directly to `defineWord()`. The definition screen auto-dismisses after 5 seconds with an opt-out "Keep Open" button. Quotes (3+ words) retain the full verification flow with TTS playback. ### Rationale The error consequences are asymmetric: - **Wrong word definition:** User sees it immediately and the cost is low (glance, recognize the error, retry next time). A wrong definition does not persist as useful data. - **Wrong quote transcription:** User may not notice until later. A misheard sentence saved to the Library looks like a real capture. TTS playback catches these errors in-flow. Since error visibility and cost differ, the friction investment should differ too. Spending 3 taps to verify "hegemony" wastes time. Spending 3 taps to verify a 15-word quote prevents bad data. ### Implementation - `handleSpeechResult()`: SMART_CAPTURE branch calls `defineWord(text)` directly for wordCount <= 2 - DEFINING composable: `LaunchedEffect` countdown (5s) with `autoDismissActive` state - `startListening()` resets `autoDismissActive = true` on each new capture cycle ### Consequences - ✅ Word definition C2C drops from 4 interactions to 0 taps - ✅ Quote verification unchanged (TTS playback still catches transcription errors) - ✅ "Keep Open" button preserves user control for edge cases - ⚠️ If speech recognition mishears a word, user sees the wrong definition briefly before auto-dismiss. Acceptable because the word is still saved and can be re-looked up from the Library. --- ## TDR-008: Lock Screen Trigger Architecture (The Platform Constraint) **Date:** 2026-02-06 · **Status:** Accepted (2026-02-07); amended by TDR-010 (2026-08-01) · **Related:** [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-R008|Bug Tracker > BUG-R008]], [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Quick Settings Tile|Feature Backlog > Quick Settings Tile]] ### Context The core product hypothesis requires capturing words/quotes from the lock screen with minimal friction. The current "Trojan Horse" MediaSession approach (TDR-002) has proven unreliable due to Android platform restrictions that are actively tightening. **The Recurring Problem:** 1. Android 12 removed `ACTION_CLOSE_SYSTEM_DIALOGS` (notification shade does not collapse) 2. Android 13+ introduced strict Background Activity Launch Restrictions 3. Google continues to crack down on "fake media players" that abuse MediaSession The MediaSession approach may work today, break tomorrow. This is not a sustainable foundation for the product's core interaction pattern. ### North Star Alignment The product mantra is "Keep your vibe, no interruptions." The key metric is **C2C (Clicks-to-Capture)**: | Trigger Method | C2C | Flow | |----------------|-----|------| | MediaSession (ideal) | 1 | Lock screen → Tap ▶ → Listening | | MediaSession (actual) | 1-∞ | Lock screen → Tap ▶ → Maybe works? | | Quick Settings Tile | 2 | Pull shade → Tap tile → Listening | | Bubble (unlocked) | 1 | Tap bubble → Listening | | Full-screen Intent | 1 | Lock screen → Tap notification → Full takeover | ### Options Analysis ``` LOCK SCREEN TRIGGER ARCHITECTURE ═══════════════════════════════ Current State The Fork in the Road ───────────── ──────────────────── ┌─────────────────┐ ┌─────────────────────────────┐ │ MediaSession │ │ OPTION A: Full-Screen │ │ "Trojan Horse" │──────┬────────────▶│ Intent (Alarm-Style) │ │ │ │ │ C2C: 1 | Lock: ✓ │ │ ⚠️ Unreliable │ │ │ Maintenance: Low │ │ ⚠️ Shade stays │ │ │ UX: Intrusive takeover │ │ ⚠️ Google │ │ └─────────────────────────────┘ │ locking down │ │ └─────────────────┘ │ ┌─────────────────────────────┐ │ ├────────────▶│ OPTION B: Quick Settings │ │ │ │ Tile │ ▼ │ │ C2C: 2 | Lock: ✓ │ ┌─────────────────┐ │ │ Maintenance: Very Low │ │ WE ARE HERE │ │ │ UX: Reliable, standard │ │ BUG-001 │ │ └─────────────────────────────┘ │ Critical │ │ └─────────────────┘ │ ┌─────────────────────────────┐ ├────────────▶│ OPTION C: Bubble API │ │ │ (Android 11+) │ │ │ C2C: 1 | Lock: ✗ │ │ │ Maintenance: Medium │ │ │ UX: Floating, persistent │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ ├────────────▶│ OPTION D: Hybrid │ │ │ MediaSession + QS Fallback │ │ │ C2C: 1-2 | Lock: ✓ │ │ │ Maintenance: High │ │ │ UX: Best when works │ │ └─────────────────────────────┘ │ │ ┌─────────────────────────────┐ └────────────▶│ OPTION E: Accessibility │ │ Service │ │ C2C: 0 | Lock: ✓ │ │ Maintenance: Low │ │ UX: Scary permissions │ │ ⚠️ Play Store risk │ └─────────────────────────────┘ ``` ### Detailed Option Breakdown #### Option A: Full-Screen Intent (Alarm-Style) **How it works:** Use `Notification.Builder.setFullScreenIntent(pendingIntent, true)` with a high-priority notification channel. This mimics incoming call/alarm behavior. | Attribute | Assessment | |-----------|------------| | Lock Screen | ✓ Works reliably, system-guaranteed | | Shade Collapse | ✓ Auto-dismisses notification shade | | C2C | 1 (tap notification) | | Maintenance | Low (stable API, used by system apps) | | Scalability | High (no ongoing platform battles) | | UX Trade-off | Feels intrusive; full screen takeover | | Permissions | Requires `USE_FULL_SCREEN_INTENT` (already have) | **Verdict:** Most technically sound. UX concern is the takeover feel, but this is how alarm clocks and phone apps work. Users understand the pattern. #### Option B: Quick Settings Tile **How it works:** Register a `TileService` that appears in the notification shade quick settings. User taps tile to trigger capture. | Attribute | Assessment | |-----------|------------| | Lock Screen | ✓ Accessible from lock screen shade | | Shade Collapse | ✓ Tile tap launches activity, shade collapses | | C2C | 2 (pull shade + tap tile) | | Maintenance | Very Low (stable since Android 7.0) | | Scalability | High (no platform restrictions) | | UX Trade-off | Extra step; user must add tile manually | | Permissions | None special required | **Verdict:** Most reliable fallback. The extra tap is a real cost, but it *always works*. Could be positioned as "power user mode" or default for users who experience MediaSession issues. #### Option C: Bubble API **How it works:** Use `Notification.BubbleMetadata` to create a floating overlay that persists across apps. | Attribute | Assessment | |-----------|------------| | Lock Screen | ✗ Bubbles collapse when device locks | | Shade Collapse | N/A (not in shade) | | C2C | 1 (when visible) | | Maintenance | Medium (API evolving) | | Scalability | Medium (Android 11+ only) | | UX Trade-off | Persistent floating icon may feel intrusive | | Permissions | None special required | **Verdict:** Does NOT solve lock screen problem. Useful only for quick capture while actively using phone (between reading sessions). Not a primary solution. #### Option D: Hybrid (MediaSession + Quick Settings Fallback) **How it works:** Keep MediaSession as primary with Quick Settings Tile as documented fallback. Guide users to tile if they experience issues. | Attribute | Assessment | |-----------|------------| | Lock Screen | ⚠️ Partial (depends on device state) | | C2C | 1-2 (varies) | | Maintenance | High (two systems to maintain) | | Scalability | Low (MediaSession may break further) | | UX Trade-off | Inconsistent experience | **Verdict:** Pragmatic short-term but not sustainable. Technical debt accumulates as Google continues tightening restrictions. #### Option E: Accessibility Service **How it works:** Register as an accessibility service, gain ability to launch activities from any state without restrictions. | Attribute | Assessment | |-----------|------------| | Lock Screen | ✓ Works anywhere, no restrictions | | C2C | 0 (could intercept gestures) | | Maintenance | Low (stable API) | | Scalability | High (no platform battles) | | UX Trade-off | Scary permission dialog; user trust issue | | Play Store | ⚠️ Policy risk; must justify accessibility use | **Verdict:** Nuclear option. Ultimate power but real trust and policy risks. Only consider if all else fails and user research shows willingness to grant permission. ### Recommendation **Short-term (Next Sprint):** 1. Implement **Full-Screen Intent** with `highPriority=true` as primary fix for BUG-001 2. Test shade collapse behavior and lock screen launch reliability 3. If intrusive feel is problematic, add brief countdown before auto-launch **Medium-term (V1 Release):** 1. Implement **Quick Settings Tile** as first-class alternative 2. Add onboarding prompt: "Add Vibe Reader to Quick Settings for reliable capture" 3. Deprecate MediaSession play button in favor of tile **Long-term (Post-V1):** 1. Monitor Google's platform direction 2. If MediaSession continues degrading, remove it entirely 3. Consider Accessibility Service only if user research supports it ### Consequences - ✅ Unblocks critical lock screen reliability issue - ✅ Reduces maintenance burden (stop fighting Android) - ⚠️ C2C increases from 1 to 2 for Quick Settings path - ⚠️ Full-screen intent may feel jarring to some users - 📋 TODO: User test both approaches to measure perceived friction --- ## TDR-007: Weekly Vibe Redesign - Structured Output & Safeguards **Date:** 2026-02-05 · **Status:** Accepted · **Related:** [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Weekly Vibe Redesign|Feature Backlog > Weekly Vibe Redesign]] ### Context The initial Weekly Vibe implementation (TDR not documented) used prose-based output from Gemini: - **Problem 1:** Verbose 3-paragraph prose was hard to scan; did not feel like a "vibe" - **Problem 2:** High token usage (~1024 output tokens, ~200+ input tokens for definitions) - **Problem 3:** No safeguard against redundant API calls (user could spam "Generate" with no new data) Goal: Create a Spotify Wrapped-style experience that is visually engaging, token-efficient, and prevents wasteful API calls. ### Options Considered **Output Format:** | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Prose** | Free-form paragraphs (current) | Natural language; flexible | Hard to style; inconsistent length | | **B. Structured JSON** | Request specific fields in JSON format | Predictable UI; constrained output | Requires parsing; model may hallucinate structure | | **C. Markdown with headers** | Semi-structured with `##` sections | Easy to parse | Still variable length; harder to style | **Safeguard Strategy:** | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Cooldown timer** | Disable button for X minutes after generation | Simple | Arbitrary; blocks legitimate re-generation | | **B. Data fingerprint** | Hash captures; only regenerate if hash changes | Precise | Complex; hash computation overhead | | **C. Session count tracking** | Track session count at generation time; enable if new sessions | Simple; meaningful | Does not catch new captures within same session | ### Decision **Output: Option B (Structured JSON)** New prompt strategy: - Input: Top 10 word terms (no definitions), top 5 quotes (truncated to 50 chars) - Output: Strict JSON schema with constrained fields - Config: `maxOutputTokens: 256` (down from 1024), `temperature: 0.5` (down from 0.7) JSON schema requested: ```json { "vibe_title": "2-3 word theme title", "vibe_emoji": "single emoji", "theme_tags": ["tag1", "tag2", "tag3"], "insights": ["insight1 (max 12 words)", "insight2"], "word_spotlight": "most interesting word", "quote_spotlight": "evocative quote (max 60 chars)" } ``` **Safeguard: Option C (Session count tracking)** Implementation: - `lastVibeSessionCount: Int` - stores session count at last generation - `lastVibeTimestamp: Long` - stores generation timestamp - `canGenerateVibe: StateFlow<Boolean>` - derived state for UI Button states: - `CAN_GENERATE` → Primary button, enabled - `GENERATING` → Disabled with spinner - `UP_TO_DATE` → Muted outline with checkmark ("Vibe is current") - `NO_DATA` → Disabled with lock icon ("Start reading to unlock") ### Consequences - ✅ ~60-70% token reduction per API call - ✅ Consistent, predictable UI layout - ✅ Prevents accidental API spam - ✅ Visual feedback on button state communicates system status - ⚠️ JSON parsing requires try-catch fallback (model may output malformed JSON) - ⚠️ Session count safeguard does not detect new captures within existing sessions (acceptable trade-off) - 📋 Future: Could add "Force Regenerate" option for power users ### Files Modified - `WeeklyVibe.kt` - New structured data model - `GeminiClient.kt` - JSON prompt + parsing + VibeResponse data class - `SessionViewModel.kt` - Safeguard state tracking + canGenerateVibe flow - `SessionComponents.kt` - Wrapped-style card UI + button states - `ReviewScreen.kt` - Wire canGenerateVibe to LibraryView --- ## TDR-004: Dependency Injection Pattern **Date:** 2026-01-10 · **Status:** Accepted · **Related:** Product Log 2025-11-12 ### Context Initial implementation used Hilt for dependency injection. Build failures occurred due to KSP/Kotlin version mismatches, consuming significant development time. ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Hilt** | Google's recommended DI framework | Powerful; scalable; Android-aware | Complex setup; version sensitivity; slower builds | | **B. Koin** | Lightweight Kotlin DI | Simpler than Hilt; less boilerplate | Still a dependency; runtime vs compile-time | | **C. Manual DI / Singleton** | Hand-rolled dependency management | Zero dependencies; full control; fast builds | More boilerplate; must manage lifecycle manually | ### Decision **Option C: Manual DI / Singleton Pattern** Implementation: - `AppDatabase.getDatabase(context)` - Singleton Room instance - `SessionViewModelFactory` - Manual factory for ViewModel injection - Direct instantiation in `MainActivity` and `ReadingSessionService` ### Consequences - ✅ Build stability restored immediately - ✅ Simpler codebase; easier to understand - ✅ Faster build times - ⚠️ Must manually ensure singleton lifecycle - ⚠️ May need to revisit if app grows significantly - 📝 Lesson: For MVPs, prefer simplicity over "best practices" --- ## TDR-003: Dictionary API Strategy **Date:** 2026-01-18 · **Status:** Accepted · **Related:** [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Multi-Word Concept Support|Feature Backlog > Multi-Word Concept Support]] ### Context Users want to define: 1. **Single words** (e.g., "hegemony") - standard dictionary lookup 2. **Multi-word phrases** (e.g., "carpe diem", "ad hoc") - may exist in dictionary 3. **Concepts/entities** (e.g., "United States", "Pythagorean theorem") - not in dictionary Current implementation uses Free Dictionary API which handles (1) and some of (2), but fails on (3). ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Dictionary API only** | Free Dictionary API for all lookups | Simple; fast | Fails on concepts and many phrases | | **B. Dictionary → Wikipedia fallback** | Try dictionary first; if 404, try Wikipedia API | Covers most cases | Two API calls for concepts; latency | | **C. Wikipedia only** | Use Wikipedia for everything | Comprehensive | Overkill for simple words; slower | | **D. AI-powered definition** | Send to LLM for definition | Handles anything | Latency; cost; requires API key | | **E. User choice** | Show "Define" vs "Look up" buttons | User controls intent | Adds friction; more UI complexity | ### Decision **Option B: Dictionary → Wikipedia fallback** ### Implementation (2026-01-18) New files created: - `WikipediaApiService.kt` - Retrofit interface for Wikipedia REST API - `WikipediaClient.kt` - Singleton client for `https://en.wikipedia.org/api/rest_v1/` Modified `SpeechCaptureActivity.kt`: - Added "Define This Instead" TextButton (only visible for detected quotes) - Updated `defineWord()` with fallback chain: 1. Try Free Dictionary API 2. If fails → Try Wikipedia API `/page/summary/{title}` 3. If 404 → Show "Definition not found" - Wikipedia results prefixed with `(Wikipedia)` for source clarity ### Consequences - ✅ Covers words, phrases, and concepts - ✅ No additional user friction for common case (single words) - ✅ User can explicitly choose "Define This Instead" for phrases - ⚠️ Wikipedia summaries may be longer than dictionary definitions (truncated at 200 chars) - ⚠️ Need to handle Wikipedia disambiguation pages gracefully - 📋 Future: Consider caching frequent lookups locally --- ## TDR-002: Lock Screen Notification Strategy **Date:** 2026-01-18 · **Status:** Superseded by TDR-008 (2026-02-07); MediaStyle revived as a flagged experiment in TDR-010 (2026-08-01) · **Related:** [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-R008|Bug Tracker > BUG-R008]], [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Smart Capture|Feature Backlog > Smart Capture]] ### Context The core product hypothesis requires capturing words/quotes from the lock screen with minimal friction. Android provides several notification styles with different lock screen behaviors. ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Standard Notification** | Basic notification with action buttons | Reliable; always renders | Small buttons; not prominent on lock screen | | **B. MediaStyle Notification** | Mimics music player with large controls | Prominent lock screen placement; single-tap capture | Android 13+ aggressively hides "fake" media players; requires metadata | | **C. Full-Screen Intent** | Like incoming calls; takes over screen | Guaranteed visibility | Too intrusive; user must dismiss | | **D. Quick Settings Tile** | Custom tile in notification shade | Always accessible | Requires swipe down; 2 taps minimum | | **E. Accessibility Service** | Intercept gestures/buttons | Zero-tap possible | Play Store policy concerns; user trust issues | ### Decision **Option B (MediaStyle) as primary, with Option A as fallback** Implementation approach ("Trojan Horse"): 1. Create `MediaSessionCompat` to register as media app 2. Set metadata (book title as "artist", "Tap ▶ to Capture" as title, app icon as album art) 3. Set `PlaybackState.STATE_PAUSED` so play button (▶) shows 4. Play button action → launches `SpeechCaptureActivity` 5. Fallback: If activity launch fails, show high-priority notification with `fullScreenIntent` Key learnings: - Android 13+ requires sufficient metadata or it hides the player - `FOREGROUND_SERVICE_MEDIA_PLAYBACK` permission required - Activity launch from background requires `USE_FULL_SCREEN_INTENT` permission - PendingIntent must target Activity directly (not Service → Activity trampoline) ### Consequences - ✅ Prominent lock screen placement when it works - ✅ Single-tap to capture (lowest C2C achievable) - ⚠️ Inconsistent behavior on some devices/Android versions (see [[Project - Vibe Reader/Build & Iterate/Bug Tracker#BUG-R008|Bug Tracker > BUG-R008]]) - ⚠️ Feels like a "hack" - may break with future Android updates - 📋 TODO: Consider Option D (Quick Settings Tile) as user-selectable alternative --- ## TDR-001: Relational Database Schema **Date:** 2026-01-10 · **Status:** Accepted · **Related:** [[Project - Vibe Reader/Build & Iterate/Feature Backlog#Completed|Feature Backlog > Completed]] ### Context The initial MVP used a flat logging structure where Words and Quotes were stored independently. As the product evolved, we needed to: 1. Associate captures with specific reading sessions (time-based grouping) 2. Associate captures with books (entity-based grouping) 3. Support queries like "show all captures from The Great Gatsby" regardless of session ### Options Considered | Option | Description | Pros | Cons | |--------|-------------|------|------| | **A. Flat Tables** | Words and Quotes as independent tables with session_id | Simple | Cannot query by book without joins; no book-level aggregation | | **B. Nested JSON** | Store captures as JSON blob per session | Flexible schema | Poor query performance; no relational integrity | | **C. Relational Hierarchy** | Book → Session → Captures with foreign keys | Normalized; flexible queries; data integrity | More complex schema; requires migrations | ### Decision **Option C: Relational Hierarchy** Schema structure: ``` Book (book_id, title, created_at) └── Session (session_id, book_id, display_name, start_time, end_time, status) ├── Word (word_id, book_id, session_id, term, definition, timestamp) └── Quote (quote_id, book_id, session_id, content, timestamp) ``` Key insight: Captures are **dual-tagged** to both Session (when) and Book (what). This enables: - "Show captures from this session" (time-scoped) - "Show all captures from this book" (entity-scoped) ### Consequences - ✅ Supports Book Layer view in Archive - ✅ Enables future features like "book-level stats" - ✅ Foreign key constraints prevent orphaned data - ⚠️ Requires `CASCADE` delete rules (deleting a Session deletes its captures) - ⚠️ Schema changes require migration strategy (using `fallbackToDestructiveMigration` for now) --- ## Pending Decisions ### TDR-005: Offline Support Strategy **Status:** Not yet decided · **Context:** Dictionary API requires network. What happens when user is offline? Options under consideration: - A. Fail gracefully with "No connection" message - B. Queue words for later definition (store as "pending") - C. Bundle offline dictionary (large app size) - D. Cache previous lookups for repeat words ### TDR-006: iOS Port Approach **Status:** Not yet decided · **Context:** When/if we port to iOS, the lock screen strategy will differ significantly. Options under consideration: - A. Live Activities (iOS 16+) - B. Widget with Siri Shortcuts - C. Apple Watch companion app - D. Wait for iOS to offer better lock screen APIs