TandemDocs

Reference7 min read

Tandem architecture

Kotlin and Jetpack Compose, one main activity, no DI framework, and no network unless a feature the user turns on needs it. Paths below are relative to app/src/main/java/io/github/zahakj/tandem/.

On this pageCoreDataScrapingUI foundationsScreens and featuresThe second screenConventions

Core

  • MainActivity.kt: hosts the top-screen UI, maps controller buttons (A select, B back; X, Y, Start, Select, L1, R1 go to Buttons.events), shows the bottom screen and records play time on return.
  • Platforms.kt + assets/platforms.json: systems and their emulators ("players"). The JSON is generated by tools/platforms.py (edit that, then run it); users can override it with ROMs/tandem/platforms.json. Platform.players lists installed emulators, default first; allPlayers lists all.
  • CocoonPlatforms.kt: reads Cocoon/Daijisho platform files from ROMs/platforms/*.json and turns their am start lines into players.
  • LaunchIntents.kt: builds a player's intent (templates like {file.uri}, {file.id}, {tags.x}, typed extras), GameFiles (ids inside .dpt/.pcgame), LaunchDisplay, LaunchProblem.
  • Launcher.kt: Launcher.launch(ctx, game) checks the game can start, plays the launch animation (ui/LaunchOverlay.kt) and starts it off the main thread: resume via Shizuku, document-URI grants, launch display, play sessions (Sessions).
  • Emulators.kt: installed apps, kept current by package broadcasts. AndroidApps.kt: launchable apps and the Android Games system.
  • Library.kt: scans ROMs/<folder> (subfolders become groups); ArtIndex reads ROMs/downloaded_media/<system>/<kind>/.
  • Collections.kt: favorites, most played, genres, smart folder contents and other lists from metadata, cached per library change.
  • Selection.kt: what is highlighted (drives the bottom screen), button events, shared Libraries state.
  • GameDelete.kt: which files deleting a game removes and the root/symlink safety checks (popup in ui/DeleteGame.kt).
  • Running.kt, RunningTasks.kt: one cached Shizuku call (ps plus dumpsys activity activities) says which emulators have a game open; RunningGames maps them to games. A leftover process alone never counts as running.
  • Chords.kt: Select/Start holds and combos, Idle, Screensaver, Privacy (private mode).
  • PrivacyPin.kt: the private mode PIN (PinCode hashing and waits, PinVault in the no-backup folder) and PrivateMode (on, off and reveal behind it); the keypad is ui/PinPad.kt. Offline.kt: NetGate (nothing goes online in private mode) and airplane mode while a hidden game plays (OfflinePlan, through Shizuku).
  • HiddenFolders.kt: hidden ROM folders and systems (paths relative to ROMs) and hidden icon-grid folders. Ask Privacy.isHidden / canShow, never GameMeta.hidden directly.
  • DiscordPresence.kt: the optional Discord status (play session in, Rich Presence out), behind DiscordSdk; the backend is src/discord (Kotlin plus a small JNI bridge to Discord's C++ Social SDK) when the SDK is in third_party/discord, else the no-op src/nodiscord. Settings in ui/DiscordSettings.kt. Its picture is the game's cover address (data/Covers.kt: GameMeta.coverUrl from the scrape, else a lazy lookup; CoverUrl.safe lets only credential-free addresses out), or the Tandem icon.
  • Achievements.kt: RetroAchievements client and ROM hashing (RaHash); network only when signed in, never for hidden games.

Data

  • data/Store.kt: persistent JSON in filesDir (library.json per-game GameMeta, settings.json). Both are Compose state; update with Store.update(key) { it.copy(...) } / Store.updateSettings { ... }.
  • data/Look.kt: every appearance, layout, prompt, screen and sound setting (Settings.look); null fields follow the theme.
  • data/SystemPrefs.kt (systems.json): per-system switches, default emulator, bottom screen, extra folders, single-screen mode.
  • data/ScanCache.kt (scan-cache.json): folder listings, so startup shows the last scan at once and rescans only changed folders.
  • data/Importer.kt: imports gamelist.xml (ES-DE, Cocoon backups).
  • data/HomeStore.kt: home.json, sessions.json (play history), achievements.json (excluded from Android backups). data/ExtrasStore.kt: extras.json, journal.json. data/IconGridStore.kt: icon-grid.json. data/WidgetStore.kt: widgets.json. data/ControllerStore.kt: controllers.json. Each store has reload().
  • data/Backup.kt: versioned backups in ROMs/tandem/backup/<yyyy-MM-dd_HH-mm-ss>/ with a manifest.json, in four categories, restored category by category; automatic ones before big changes (last five kept). UI in ui/BackupScreen.kt.

Scraping

data/Scraper.kt runs the job (singleton scope, pauses in the background) and the per-game worker, which asks sources in the user's order, each only for what is still missing. ScrapeHits.kt (RunSources: lookup and pace per source), ScrapeSources.kt (network, saving media, libretro, ScreenScraper, SteamGridDB, matching, hashing), ScrapeApis.kt (IGDB, HowLongToBeat), LaunchBox.kt (pure Kotlin streaming parser and on-device index) and LaunchBoxDb.kt (download, monthly refresh), DetailsFill.kt, ScrapePlatforms.kt, SourceTests.kt, ScrapeConfig.kt (settings and keys, in their own private file). GameMeta.lockedFields marks user edits a scrape keeps. UI: ui/ScrapeScreen.kt, ui/ArtSetup.kt, ui/GameEditor.kt. See SCRAPING.md and PRIVACY.md.

UI foundations

  • ui/App.kt: routes (Route, Nav), library scans (LibraryScan, run by ui/ScanRunner.kt), toasts, setup screen, top bar.
  • ui/StartFlow.kt: the cold start. MainActivity.onCreate starts the scan early (LibraryScan.startEarly). The first draw waits briefly for the cached library, and both screens hold their content until the home is composed and then play the swap's arrival (SwapFx.introHold / introPlay). The splash (StartSplash) shows over the wallpaper only while the library isn't ready. StartLook.kt keeps the starting window (@style/Tandem.Start*) in the last theme's colour. Details in PERFORMANCE.md.
  • ui/Design.kt: Palette, Nunito type, TandemTheme, the Pip mascot, FocusCard (the focusable card everything is built from), ButtonHint, Pill.
  • ui/Focus.kt: FocusMemory, rememberFocusTarget, Modifier.takesFocus, KeepInView, popup Layers (only the top layer takes focus), InterceptButtons.
  • ui/Edges.kt: FocusRoom, FocusRoomSide, fadingEdges, PopupRoom, bleed, onArt. Follow UI-RULES.md for every new screen and popup.
  • ui/Overlays.kt: Y game menu, view menu, X search, Confirm.ask, OverlayPanel. ui/ActionMenu.kt: every Y menu (the item lifts out over a blurred screen). ui/TextEntry.kt: the controller keyboard.
  • ui/Theme.kt: ThemeSpec, BuiltInThemes, Themes (live theme with cross-fade), Motion.
  • Smoothness (PERFORMANCE.md): ui/Warm.kt (Facts, AfterFrames), ui/Ambient.kt (the shared ambient clock, AmbientFrames), ui/QuietSemantics.kt (no accessibility tree for services that can't read it), ui/Perf.kt (frame timing overlay, logcat tag TandemJank).

Screens and features

  • ui/Screens.kt: home, system and folder grids, collections, genres. ui/Cards.kt: GameArt, GameCard, GroupCard, SystemCard, ShrinkText.
  • ui/Layouts.kt: carousel, list and console homes, games views, DockBar, folder popups. ui/LayoutPrefs.kt: grid sizes and card shapes.
  • Icon grid: ui/IconGrid.kt (GridCatalog, IconGridHome, move mode, tile menu, user folders, FolderSheet), ui/IconArt.kt (TileArt), ui/IconPacks.kt (pack art and smart-folder art), ui/PanelMorph.kt (folder popups grow from their tile), ui/Spotlight.kt (the other screen in this mode). Smart folders: data/SmartRule.kt (the rule and its matching, pure), ui/SmartFolders.kt (tile, Y menu rows, launch screen), ui/SmartFolderForm.kt (editor and the other screen's preview).
  • Themes and art: ui/ThemePacks.kt (Tandem and Cocoon theme packs; THEMES.md), ui/PackImport.kt, ui/ThemePacksScreen.kt, ui/SystemIcons.kt, ui/Backdrops.kt and ui/GameBackdrops.kt (wallpapers and drawn backdrops), ui/Video.kt (LoopingVideo), ui/HeroArt.kt, ui/ArtPick.kt (which picture fills a screen), ui/LogoFx.kt (logo pop, sway and shine), ui/Appearance.kt, ui/ThemeGallery.kt, ui/Glyphs.kt, ui/TandemIcons.kt.
  • Controls: ui/Controls.kt (Prompts, Clock), Gamepads.kt (connected pads, per-controller Nintendo layout, HomeButton), ControllerHomeService.kt (optional accessibility service for a controller's Home button; key events only), ui/ControllerSettings.kt. Controller passthrough (an external pad plays as the built-in one): PadLink.kt runs the helper src/padlink/padlink.c through Shizuku or su, PadMap.kt (translation table), PadPlan.kt (which pad, state machine), ui/PadLinkUi.kt (the offer on the companion).
  • Sound: audio/ is pure Kotlin (runs on a desktop JVM too): DSP, synth and acoustic voices, drums, a tracker-style song DSL, sequencer and mixer, the Meadow Tales and Pastel Arcade soundtracks, UI sound packs and jingles. ui/Sound.kt (Sounds, Music), ui/SoundEngine.kt (audio thread while music plays), ui/SoundSettings.kt.
  • Motion: ui/Flair.kt (floating, riseIn, drift, PipMind; graphics-layer only, still with Reduce motion), ui/SwapFx.kt (the X screen swap).
  • Settings (SETTINGS.md): ui/SettingsCatalog.kt (pages and groups), ui/Settings.kt (row types, two-pane layout), ui/SettingsHome.kt, ui/SettingsSearch.kt, ui/Presets.kt (Quick looks).
  • Home extras: ui/HomeHost.kt (popups and polling while resumed), ui/Switcher.kt, ui/HomeMode.kt (Home app, dock, apps drawer), ui/QuickPanel.kt, ui/Stats.kt, ui/AchievementsUi.kt, ui/ScreensaverUi.kt, ui/HomeExtras.kt (Resume page, Recently played strip), ui/StartPanel.kt, ui/Welcome.kt, ui/QuickFind.kt, ui/QuickJump.kt, ui/CocoonSetup.kt (Import my Cocoon setup, with undo).
  • Built-in apps: ui/BuiltInApps.kt (registry, Route.Tool), ui/GalleryApp.kt, ui/JournalApp.kt, ui/FilesApp.kt, ui/ControllerTestApp.kt.
  • Widgets: ui/Widgets.kt (framework, packing, WidgetHost), ui/WidgetKinds.kt, ui/WidgetEditor.kt, DeviceInfo.kt (storage, battery, memory, temperature). Widgets refresh with rememberNow / rememberPolled, which tick only while resumed.

The second screen

  • ui/BottomScreen.kt: the second display as a Presentation, showing details for the current Selection. With the screens swapped it draws the library and MainActivity forwards the D-pad and A to it; ControlsGuard (ui/ScreenRoles.kt) swaps back if it cannot take the controls.
  • GameCompanion.kt + ui/CompanionScreen.kt: the bottom screen during play. CompanionActivity opens on the second display before the game (own task, out of Recents, FLAG_NOT_FOCUSABLE so the game keeps the controller) and is dismissed once the presentation is back, or closes by itself once its game's task is gone (GameExit.kt). Modes: art, tandem and off (the default for dual-screen emulators). CompanionRestore.kt reopens it if Tandem's process restarts mid-game; one left behind by a restart goes with the next dismissal. GameResume.kt brings a running game back by its own task (never a new one). HomeBack.kt: Home with another app over a running game brings the game and its companion back first; a second Home goes to Tandem.
  • SecondaryHome.kt + ui/SecondaryHomeScreen.kt: Tandem's secondary-display Home, shown when nothing else is on the second display and Tandem is the default Home. It is singleTop (Android refuses singleTask for a secondary home), not focusable and out of Recents. It shows the app in front or Pip over a still wallpaper, and dims after a minute. Android's own launcher opens only when the user picks it (openStockHome, openStockBottom).
  • TandemDream.kt: Tandem as Android's screensaver. On the Thor, controller input goes to the presentation of the app behind a dream, so the dream shows its own Presentation on the second display (DreamBottom) that wakes on any input, and every Tandem window checks Dreaming.onKey / onMotion first. WakeGate (DreamWake.kt) swallows the rest of the waking press.

Conventions

  • Controller first. Every screen is fully usable with the D-pad and A/B. Every focusable thing is a FocusCard; screens pick the target with rememberFocusTarget, attach takesFocus and keep it in view with KeepInView. Popups use OverlayPanel, which makes them a focus layer.
  • Sizes. The top screen is about 830×470 dp, the bottom about 470×540 dp (see UI-RULES.md). Keep text readable at arm's length.
  • Style. Pastel Palette colours on the night background, rounded 14–26 dp corners, spring animations, Pip for empty states and messages. Friendly, plain text.
  • Privacy. No analytics or telemetry. Network only for features the user turns on. Never send hidden games anywhere.
  • New features go in new files; keep edits to shared files small. A feature's settings are a @Composable fun xxxSettings(): SettingSection in its own file, added to settingsCatalog().
  • Build with ./gradlew assembleDebug (on NixOS, nix-shell --run './gradlew assembleDebug') before committing. Judge smoothness on assembleRelease; see PERFORMANCE.md.

Last updated Edit this page on GitHub

Tandem is free software under the GNU GPL v3 or later. These pages are built from the Markdown in the repository; no cookies, no tracking.