mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
The git-runner VMs' nix-docker/bookworm labels are gone, replaced by the ci-manager's ephemeral pools, so point every Forgejo workflow at them. The ci job needs the Nix devshell, so it runs on the nix-xlarge pool (a 4 CPU / 8 GiB NixOS VM that already has Nix) with Gradle at -Xmx2g; the mirror and PR-import jobs don't need Nix and use the ubuntu pool. The mirror job pins git-filter-repo 2.47.0 itself, since the image's version may differ and another one would move every commit on GitHub.
7.1 KiB
7.1 KiB
AGENTS.md
Guidance for agents (and humans) working in this repository.
What this is
A native Android rewrite of Subtracks, a client for Subsonic-compatible servers (Navidrome, gonic, Airsonic, ...), in Kotlin and Jetpack Compose. The main history still contains the earlier Flutter app; the native app is being built fresh under app/ and ships as com.subtracks. The aim is a clean, F-Droid-friendly client: free/libre dependencies only, no telemetry.
Environment
Everything comes from the Nix flake devshell; do not install toolchains by hand.
- Enter it with
nix develop, or let direnv load it into your shell (.envrcrunsuse flake). - The default devshell provides JDK 21, Gradle 9, the Android SDK (platform 37, build-tools 37.0.0, platform-tools 37.0.1) and nushell.
- The integration harness needs real servers, so it lives in a separate
nix develop .#integrationshell (navidrome, gonic, lms, php, Nextcloud with the Music app, sqlite). Keeping them out of the default shell avoids pulling that closure into everyday builds. - Build with the devshell
gradle, not./gradlew. CI uses the nix Gradle pinned byflake.lock; the wrapper is kept only for people without Nix.
Commands
- Build:
gradle :app:assembleDebug - Release build:
gradle :app:assembleReleaseonly when build config, R8/ProGuard or dependencies change, and once before a merge; not part of routine verification - Unit tests:
gradle :app:testDebugUnitTest - Lint:
gradle :app:ktlintCheck :app:lintDebug - Format:
gradle :app:ktlintFormat - Integration tests:
nix develop .#integration --command nu tools/integration-test.nu(starts navidrome, gonic, lms and a Nextcloud Music instance, then runs:app:integrationTest) - Screenshots:
gradle :app:recordRoborazziDebugrenders the*Screencomposables toapp/src/test/screenshots/(gitignored) plus an HTML report inapp/build/reports/roborazzi/, for local review. No golden images are committed andverifyRoborazziDebugis not part of CI. - Demo screenshots:
gradle :app:demoScreenshotsdoes a real sync of the public Navidrome demo (https://demo.navidrome.org,demo/demo) and renders the store screenshots with that library's art and names intoapp/src/test/screenshots/demo/(gitignored). Needs network; not part of CI or the integration suite.
Testing layout
app/src/test- JVM unit tests (JUnit4, Robolectric, Roborazzi, MockWebServer). Run by:app:testDebugUnitTest.app/src/integrationTest- tests that need real servers. Compiled with the unit test sources but run by the dedicated:app:integrationTesttask, and excluded from every*UnitTesttask. The nushell harness starts the servers for you.- A test earns its place only if it fails when the behaviour it covers breaks, and passes when the behaviour works. A test that passes either way is worse than no test: it looks like coverage while hiding the gap. So when you add one, break the behaviour (or run the test against the pre-fix code), watch it fail, then restore the fix and watch it pass.
CI
- The CI workflows live under
.forgejo/and.github/, and the GitHub mirror and the GitHub PR import under.forgejo/; see.forgejo/README.md.
Conventions
- Prefer self-explanatory code; do not add comments unless the reason cannot be inferred from the code itself (docstrings for functions and exports are fine).
- ktlint (
org.jlleitschuh.gradle.ktlint) enforces style and flags unused imports; rungradle :app:ktlintFormatbefore committing..editorconfigexempts@Composablefunctions from the lowerCamelCase function-naming rule. - Room 3 (
androidx.room3) on AndroidXsqlite-bundled(BundledSQLiteDriver, SQLite 3.50+). Do not reintroduce SQLDelight or the platform SQLite. - Networking uses OkHttp and DOM XML parsing; auth uses the Subsonic token scheme by default.
- Use coroutines and
Flow; library reads are exposed asFlowfrom Room. Large lists page with Paging 3 over RoomPagingSourcerather than loading the whole table. - The library is mirrored in full at sync and must work offline. Never fetch library metadata on demand for browsing, filtering or sorting; only media bytes (streams, downloads) and images are on-demand. A server-derived sort key must be synced into a local column, not requested lazily when the sort is selected.
- Auto-sync is a foreground-only, in-process timer (
AutoSync, driven by the Activity lifecycle), not a background worker: do not move it to WorkManager or an alarm.SyncManagercoalesces a request that arrives while a sync is running into a single follow-up; the library and Home sync actions no-op while one is running. - Playback is Media3:
PlaybackServiceis aMediaSessionServiceowning anExoPlayer, andPlaybackController(app-scoped Koin singleton) is the only thing the UI talks to. The queue is aqueue_entrieslist of references with optional ranges plus aplayback_cursorrow; keep only a bounded window in memory (never the whole queue, and never load a whole table to build one). Stream URLs are freshly salted per request, so attach artwork byCoverArtRefcache key, not by URL. - UI state lives in
androidx.lifecycle.ViewModel, wired with Koin (koinViewModel()); screens split into a stateful*Routeand a stateless*Screenfor screenshot tests. - UI and playback preferences go in DataStore (
UserPreferences), not Room. - Verify current dependency versions and their compatibility before adding or pinning anything.
Gotchas
- Robolectric runs at SDK 35 while
targetSdkis 37; raising it changes the on-demand Roborazzi renders (there are no committed goldens to re-record). - Room 3 does not map
PagingSourceautomatically: a DAO that returns it needs@DaoReturnTypeConverters(PagingSourceDaoReturnTypeConverter::class). - Material icons are not pulled in by
material3; use the BOM-managedmaterial-icons-core/material-icons-extended(frozen at 1.7.8). Extended is large in debug builds but R8 strips unused icons from release. - Robolectric creates a fresh
Applicationper test in one JVM, soSubtracksApp.onCreatestops any running Koin beforestartKoin. - Integration servers: navidrome on 4533 (
admin/password), gonic on 4747 (admin/admin), lms on 5082 (admin/subtracks-lms), Nextcloud Music on 8090 (admin/subtracks-nextcloud, base URLhttp://localhost:8090/index.php/apps/music/subsonic/). The test music is cached in.integration/music(gitignored). - lms and Nextcloud Music authenticate the Subsonic API with per-user API keys instead of passwords. The harness seeds a key and most tests use
useTokenAuth = false;SubsonicAuthIntegrationTestdrivesuseTokenAuth = trueand asserts the fallback to plaintext (lms answers code 42, Nextcloud 41). nix developdoes not source your shell rc files (so tools like atuin are not active); use direnv if you want your normal interactive shell.- The store screenshots in
metadata/en-US/images/phoneScreenshots/must be 24-bit no-alpha PNGs (Play rejects palette and alpha PNGs for screenshots).:app:demoScreenshotswrites its captures in that form, so copy them across as-is and don't run a palette optimizer over them.