Skip to content

Latest commit

 

History

History
279 lines (228 loc) · 19.8 KB

ARCHITECTURE.md

File metadata and controls

279 lines (228 loc) · 19.8 KB

Boxlore architecture

Reference for how the Android app is structured: module boundaries, dependency direction, composition root, and upgrade-safe identity. Module-local detail lives in each folder’s README.md.

Overview

Boxlore is a multi-module Gradle project. :app is the application shell. Shared behavior lives under :core:*. Screen and flow UI lives under :feature:*. There is no Hilt or Koin: a single AppContainer is the composition root.

The graph is layered so playback and features depend inward on catalog and lower cores. Features do not depend on other features. Catalog does not depend on the design system. Feature modules never talk to PostHog directly; they use :core:analytics.

Decoupling and module granularity

Keep cohesive domains and flows in focused modules: settings belongs in :feature:settings, listening history in :feature:history, and authentication in :core:auth. Avoid placing unrelated capabilities in general-purpose modules. Obtain explicit maintainer approval before extracting modules or changing architectural boundaries.

Identity and storage

These values are part of the shipping product. Renames or recreations break upgrades, WorkManager, Media3, and deep links.

Concern Contract
applicationId cx.aswin.boxlore
Code packages / Android namespaces cx.aswin.boxlore.*
SharedPreferences files Opened as boxlore_*; PrefsFileMigrator copies from legacy boxcast_* when needed. Key strings inside those files stay stable.
DataStore File name user_preferences; the API session uses its own file boxlore_api_session (:core:network), excluded from backup and device transfer
API session device key Android Keystore alias boxlore_api_session_device_key (non-exportable EC P-256 that signs session renewals)
Room (main) Database filename and tables stay stable
Room (ranking) Separate adaptive database owned by :core:ranking
WorkManager LegacyWorkerFactory plus permanent core.data.* stubs resolve historical FQCNs
Deep links boxlore:// and boxcast://, plus both HTTPS share-path prefixes
BuildConfig Prefer BOXLORE_*; Gradle still dual-reads BOXCAST_* fallbacks
Episode / media IDs rss: prefixes, negative RSS IDs, mediaId prefixes, and customCacheKey schemes stay as implemented. PI episode supplements also use negative episode IDs (generated under an rss: feed namespace) but keep Episode.podcastId as the Podcast Index show id — they are not rss: library rows. Episode identity (show ID, catalog key), recipe IDs, collisions and aliases follow docs/EPISODE_IDS.md
Playback instance One UI-scoped PlaybackRepository; routes and workers must not construct a second one
Object graph order DB → PodcastRepository → QueueRepository → PlaybackRepository → QueueManager → SmartDownloadManager
Smart Queue refill Owned by BoxLorePlaybackService only

Module map

:app
:core:model | :core:auth | :core:network | :core:domain | :core:database | :core:prefs
:core:analytics | :core:catalog | :core:rss | :core:downloads | :core:playback | :core:ranking
:core:designsystem | :core:testing
:feature:home | :feature:settings | :feature:history | :feature:player | :feature:info | :feature:explore
:feature:library | :feature:onboarding | :feature:briefing | :feature:widgets

On disk, the folder path matches the Gradle id (core/playback → :core:playback).

Ownership

Module Responsibility README
:app Application, AppContainer, navigation host, FCM, WorkerFactory app/README.md
:core:model Shared models and enums core/model/README.md
:core:auth Authentication domain, AuthRepository, and Firebase Auth implementation core/auth/README.md
:core:network HTTP client (BoxLoreApi / NetworkModule) and network DTOs core/network/README.md
:core:domain Thin ports and small result types (no Room or repositories) core/domain/README.md
:core:database Main Room database, entities, DAOs, migrations core/database/README.md
:core:prefs DataStore and SharedPreferences façades (UserPreferencesRepository, BoxcastPrefs) core/prefs/README.md
:core:analytics Analytics façade (AnalyticsHelper, Analytics, RecordingAnalytics); PostHog init stays in :app. Event names and properties: docs/ANALYTICS_EVENT_GLOSSARY.md core/analytics/README.md
:core:catalog Catalog orchestration: PodcastRepository, subscriptions, content sections, backup/restore core/catalog/README.md
:core:rss RSS fetch/parse, RssPodcastRepository, PI episode supplement (EpisodeSupplementRepository), rss: / negative IDs core/rss/README.md
:core:ranking Adaptive scoring, LinUCB, feedback, AdaptiveRankingDatabase. Behavior detail: docs/recommendation-system.md core/ranking/README.md
:core:downloads DownloadRepository, Smart Downloads, related WorkManager workers core/downloads/README.md
:core:playback PlaybackRepository, queue, Media3 services, smart-queue helpers core/playback/README.md
:core:designsystem Theme and shared composables; no data or network ownership core/designsystem/README.md
:core:testing Shared fixtures, dispatcher helpers, architecture guards core/testing/README.md
:feature:home Home feed presentation, Daily/Offline mix, and local Debug feature/home/README.md
:feature:settings Unified Settings hub, Account & Cloud Sync, Appearance, Privacy, Playback, and Download policies feature/settings/README.md
:feature:history Listening history timeline, activity charts, pattern insights, and streak cards feature/history/README.md
:feature:player Player overlay (PlayerSheetScaffold); not a NavHost destination feature/player/README.md
:feature:info Podcast and episode detail, including deep links feature/info/README.md
:feature:explore Explore plus Learn / LearnHistory feature/explore/README.md
:feature:library Library hub, subscriptions, downloads, liked feature/library/README.md
:feature:onboarding First-run flows (AI, genre, search, import) feature/onboarding/README.md
:feature:briefing Daily briefing screen feature/briefing/README.md
:feature:widgets Home-screen Now Playing, compact bar, playback-control, subscriptions, and new-episodes widgets (RemoteViews) feature/widgets/README.md

Core package roots

Module Package
:core:prefs cx.aswin.boxlore.core.prefs
:core:analytics cx.aswin.boxlore.core.analytics
:core:rss cx.aswin.boxlore.core.rss
:core:ranking cx.aswin.boxlore.core.ranking
:core:downloads cx.aswin.boxlore.core.downloads
:core:playback cx.aswin.boxlore.core.playback
:core:database cx.aswin.boxlore.core.database
:core:catalog cx.aswin.boxlore.core.catalog

Dependency direction

flowchart TB
  app[:app]
  subgraph features [feature]
    home[:feature:home]
    player[:feature:player]
    info[:feature:info]
    explore[:feature:explore]
    library[:feature:library]
    onboarding[:feature:onboarding]
    briefing[:feature:briefing]
    widgets[:feature:widgets]
  end
  playback[:core:playback]
  catalog[:core:catalog]
  rss[:core:rss]
  analytics[:core:analytics]
  downloads[:core:downloads]
  ranking[:core:ranking]
  prefs[:core:prefs]
  domain[:core:domain]
  database[:core:database]
  network[:core:network]
  design[:core:designsystem]
  model[:core:model]

  app --> features
  app --> playback
  app --> catalog
  app --> downloads
  app --> design
  features --> playback
  features --> catalog
  features --> downloads
  features --> design
  features --> model
  playback --> catalog
  playback --> downloads
  downloads --> catalog
  catalog --> rss
  catalog --> prefs
  catalog --> domain
  catalog --> database
  catalog --> ranking
  catalog --> network
  catalog --> model
  features --> analytics
  features --> ranking
  playback --> ranking
  playback --> analytics
  design --> analytics
  rss --> database
  rss --> domain
  rss --> model
  ranking --> database
  ranking --> domain
  ranking --> prefs
  ranking --> model
  analytics --> model
  prefs --> model
  domain --> network
  domain --> model
  database --> network
  database --> model
  design --> model
  network --> model
Loading

The primary runtime stack is playback → catalog → prefs / domain / database / network / model.

Rules enforced in the graph

  • Features do not declare Gradle dependencies on other features (Konsist).
  • Features do not import other feature packages (Konsist).
  • :core:playback depends on :core:catalog; catalog does not depend on playback (Konsist).
  • :core:catalog does not depend on :core:designsystem (Konsist).
  • :core:catalog must not api( :core:analytics or :core:ranking (Konsist; ranking may be implementation only).
  • Features that need analytics or ranking depend on :core:analytics / :core:ranking directly.
  • Feature sources do not import PostHog (scripts/ci/check-feature-no-posthog.sh).
  • No Hilt, Koin, Dagger, or MockK in production sources or Gradle test deps (Konsist).
  • Enums shared by catalog and UI (for example AutoTranscriptState) live in :core:model.
  • :core:domain holds ports; :core:catalog implements catalog-facing ports.
  • :core:network is HTTP/API only. RssFeedClient lives in :core:rss and reaches callers through :core:catalog’s api(rss) edge.
  • getInstance call sites stay on the AppContainer / Room / WorkManager / Calendar / MessageDigest / JCA key (KeyStore, KeyPairGenerator, Signature) / Firebase allowlist (Konsist).
  • Extracted core modules keep package equal to the module root (permanent core.data.* stubs allowlisted).

Composition root

AppContainer (in :app) owns the shared object graph and is created only in BoxLoreApplication. MainActivity, BoxLoreNavHost, and feature assemblers read application.container. They do not build repositories or a second graph.

Home, Settings, and Info ViewModels are built through assemblers (HomeViewModelAssembler, SettingsViewModelAssembler, InfoViewModelAssembler). Source-aware SubscribedEpisodeCatalog composes the PI and true RSS adapters behind the existing LocalEpisodeCatalogPort, preserving their separate tables and show identities. Narrow ports under core.domain.ports let ViewModels and workers take fakes without depending on full repositories or BoxLoreDatabase. Production wiring uses RoomLocalCatalog and RoomEpisodeOfflineLookup from AppContainer. ListeningHistoryBackupPort lives in core.catalog.ports.

Product surfaces

Surface Module Notes
Home, Settings hub, Add RSS :feature:home Settings hosts the RSS dialog
Learn / LearnHistory :feature:explore Learn is a bottom-nav tab
Player overlay :feature:player PlayerSheetScaffold, not a NavHost route
Podcast / episode detail :feature:info Dual episode routes and deep links
Playback, queue, Media3 :core:playback Includes permanent core.data.service stubs
HTTP API :core:network Separate from RSS
Ranking :core:ranking Own adaptive Room database; personalization detail in docs/recommendation-system.md
RSS catalog :core:rss Negative / rss: IDs; exposed through catalog
PI episode supplement :core:rss + :core:database Feed-only extras under PI podcast id; not a subscription

The app shell owns adaptive player policy. Only Floating navigation with available player chrome can compact. The main Lore route temporarily forces compact presentation and restores the prior browsing state on exit; feature screens use the shared designsystem progress without importing the player feature. Lore may reclaim the released row in its bounded card viewport, while scrollable feed/list tail clearance remains stable. Playback ownership and persisted settings are unchanged.

Adaptive UI and state

Every Android UI or behavior change must support the affected phone, tablet, foldable, and resizable-window states, including changes initially requested for phones and shared components used by multiple screens. Assess these states before editing and document the affected behavior and verification in the module README. Documentation-only changes do not require unrelated UI tests.

  • Use the shared BoxloreWindowLayout and fold/posture policy rather than assuming a device category or duplicating breakpoints. The current tablet gate is width at least 600dp and height at least 480dp; split layout additionally requires width at least 840dp and landscape dimensions (widthDp > heightDp). Narrow or short tablet windows must retain the compact behavior.
  • Resizing, rotation, split-screen, and fold/posture changes must adapt presentation without losing pending work. Retain content and its lifecycle/ViewModel/saved-state owners, unsaved drafts and reorderings, selection/navigation state, and scroll position across the affected transitions. Modal hosting must preserve content state and apply the current action, Back, and outside-dismiss callbacks/properties without duplicate invocations or unintended dismissal.
  • Adapt pane bounds, insets, keyboard clearance, and fold-safe placement while keeping controls reachable and preserving accessibility and reduced-motion behavior. Presentation changes must preserve the existing playback, data, navigation, analytics, and ranking ownership; never create another object graph or playback repository to support a layout.

Changed adaptive behavior requires hermetic JVM regression coverage under src/test, using the existing Robolectric/Compose JVM fixtures when composition lifetime matters. Do not add Compose instrumentation, Espresso, or emulator CI. Select relevant cases from this mandatory coverage contract:

  • Exercise the same live content/state owner across compact-to-tablet and tablet-to-compact transitions, including both the 600dp width gate and the 480dp height gate when affected. Exercise the 840dp split-layout threshold, portrait/landscape changes, and fold/posture transitions when the changed surface uses them. Include boundary values on either side of the affected gates.
  • Assert retained drafts/reorderings, selection, scroll, and owner identity as applicable, then invoke the current callbacks and verify save, cancel, Back, and outside-dismiss behavior. Cover relevant loading, empty, error, and recovery states as well as populated content. Bug fixes must reproduce the actual failure and shared consumers where applicable.
  • Separate static geometry/policy checks from state-transition regressions: mounting fresh content independently at several sizes does not prove continuity. Policy tests alone do not prove that modal content survives recomposition or host changes.

Report source inspection, executed JVM checks, and device verification separately. Include a manual acceptance path for the affected phone/tablet/foldable states and transitions; an untested state or unavailable device remains an explicit verification limitation.

Upgrade failsafes

Historical FQCNs and preference file names remain reachable so upgrades from older installs keep working. These bridges are permanent.

Workers

Historical FQCN Implementation Bridge
cx.aswin.boxcast.core.data.SmartDownloadWorker cx.aswin.boxlore.core.downloads.SmartDownloadWorker LegacyWorkerFactory
cx.aswin.boxcast.core.data.AutoDownloadWorker cx.aswin.boxlore.core.downloads.AutoDownloadWorker LegacyWorkerFactory
cx.aswin.boxcast.core.data.PurgeSmartDownloadsWorker cx.aswin.boxlore.core.downloads.PurgeSmartDownloadsWorker LegacyWorkerFactory
cx.aswin.boxlore.core.data.SmartDownloadWorker cx.aswin.boxlore.core.downloads.SmartDownloadWorker factory + stub
cx.aswin.boxlore.core.data.AutoDownloadWorker cx.aswin.boxlore.core.downloads.AutoDownloadWorker factory + stub
cx.aswin.boxlore.core.data.PurgeSmartDownloadsWorker cx.aswin.boxlore.core.downloads.PurgeSmartDownloadsWorker factory + stub

Services / providers

Historical FQCN Implementation Bridge
cx.aswin.boxlore.core.data.service.BoxLorePlaybackService cx.aswin.boxlore.core.playback.service.BoxLorePlaybackService Manifest + stub
cx.aswin.boxlore.core.data.service.MediaDownloadService cx.aswin.boxlore.core.playback.service.MediaDownloadService Manifest + stub
cx.aswin.boxlore.core.data.service.AutoCollageProvider cx.aswin.boxlore.core.playback.service.AutoCollageProvider Manifest + stub

SharedPreferences files

Legacy file Current file Opened by
boxcast_prefs boxlore_prefs BoxcastPrefs via PrefsFileMigrator
boxcast_theme_fast_cache boxlore_theme_fast_cache UserPreferencesRepository
boxcast_analytics_prefs boxlore_analytics_prefs AnalyticsHelper
boxcast_player boxlore_player PlaybackRepository / PodcastRepository
— android_auto_artwork_sources AutoArtworkSourceStore / AutoCollageProvider (Android Auto art URI map; do not rename)
— boxlore_now_playing_widget NowPlayingWidgetSnapshotStore (:feature:widgets; last rendered Now Playing snapshot JSON; do not rename)
— boxlore_library_widget LibraryWidgetSnapshotStore (:feature:widgets; last rendered Subscriptions / New Episodes snapshot JSON; do not rename)
boxcast_api_config boxlore_api_config BoxLoreAppRoot
boxcast_referrer_prefs boxlore_referrer_prefs InstallReferrerManager

R8 keeps worker names/constructors and permanent core.data.service.* component aliases, plus the JSON field/default-constructor contracts used by caches and backups. Library consumer rules protect generated Room implementations and SDK entry points. Core implementations are otherwise eligible for optimization. The release-artifact gate verifies these contracts after optimization.

Source file size

Kotlin sources under */src/main/** stay under 1000 lines. Larger units are split into same-package helpers or composables without changing public APIs or the identity/FQCN contracts above.

Verification

How the tree is tested — commands, coverage floors, and layer status — is in docs/TESTING.md.

Layer Role Where
JVM unit Logic, ports, ViewModel slices docs/TESTING.md
Maestro Device E2E smoke (local; YAML validated in CI) maestro/README.md
Architecture-as-code Konsist and filesystem guards :core:testing
Screenshots Visual baselines (optional local Roborazzi) docs/screenshots/README.md

Shared test fixtures live in :core:testing. The project does not use MockK or Hilt.

Related documents

Document Contents
docs/MODULE_README_TEMPLATE.md Module README shape
maestro/README.md Local Maestro flows