# Development ## Project Setup To run the project, open it in the latest Version of Android Studio and build the project as usual. The default local development variant is `freeDebug`, which does not require Google services credentials. Play variants include Firebase, Crashlytics, Remote Config, and Play review dependencies. Play builds require `app/src/play/google-services.json`; CI writes this file from the release secret. By default, there is not enough memory configured for gradle. You can fix this by running. ```sh scripts/gradle_bootstrap.sh ``` This will configure your global `~/.gradle/gradle.properties` to use more memory, depending on your machine. Example of the generated properties, check the `gradle_bootstrap.sh` script for exact details. ```properties # Begin: Gradle JVM bootstrap-generated properties org.gradle.jvmargs=-Dfile.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Xms4g -Xmx16g kotlin.daemon.jvm.options=-Dfile.encoding=UTF-8 -XX:+ExitOnOutOfMemoryError -Xms4g -Xmx16g # End: Gradle JVM bootstrap-generated properties ``` ## Tests To run the unit tests, run the following command: ```sh ./gradlew voiceUnitTest ``` ## Developer Menu The developer menu is hidden by default. To unlock it: 1. Open `Settings`. 2. Tap the app version entry 13 times. 3. Open the newly visible developer menu from `Settings`. ## Opening a Screen Directly Debug builds can start on any back stack of `Destination.Compose`s, passed as JSON in the `debugBackStack` extra. The emulator skill's script builds it for you: ```sh ANDROID_SERIAL= .agents/skills/emulator/scripts/ui.py open BookOverview Settings DeveloperSettings ``` Agents use this through the emulator skill in `.agents/skills/emulator`, which also gives every agent its own emulator. ## Formatting Voice formats Kotlin with **ktfmt** in its Google style, through Spotless. - Check for formatting issues: ```sh ./gradlew spotlessCheck ``` - Auto-fix formatting: ```sh ./gradlew spotlessApply ``` - To format from Android Studio, install the [ktfmt plugin](https://plugins.jetbrains.com/plugin/14912-ktfmt) and choose "Enable for This Project" when it asks. It picks up the project's settings from `.idea/ktfmt.xml`. - To check the formatting on every commit, turn on the pre-commit hook in `.githooks`. When the check fails, it formats the code and stops the commit, so you can stage the changes and commit again. Claude Code sessions turn it on for you. Otherwise, run this once per clone, it covers all worktrees: ```sh git config core.hooksPath .githooks ``` Compose rules come from Slack's [compose-lints](https://slackhq.github.io/compose-lints/) and run as part of Android Lint. ## Releasing To release a new version, trigger the [Release Workflow](https://github.com/PaulWoitaschek/Voice/actions/workflows/release.yml) This creates a draft GitHub release with the APK and keeps the Play AAB as a workflow artifact. With `submit_to_stores` enabled, it also uploads the AAB to the Play Store’s internal track and promotes it to a 0.1% production rollout. The release workflow uses the protected `release` GitHub environment. CI signs through Gradle's injected signing properties, keeps keystores as base64 file secrets, and passes store passwords, key aliases, and key passwords as environment variables; no checked-in or generated signing properties files are required. The `release` environment requires these secrets: | Secret | Purpose | |---------------------------------------------|-----------------------------------------------------------------| | `PLAY_SERVICE_ACCOUNT` | Google Play service account JSON, base64 encoded. | | `GOOGLE_SERVICES` | Play flavor `google-services.json`, base64 encoded. | | `PLAY_SIGNING_KEYSTORE` | Play release keystore, base64 encoded. | | `PLAY_SIGNING_STORE_PASSWORD` | Play release keystore password. | | `PLAY_SIGNING_KEY_ALIAS` | Play release key alias. | | `PLAY_SIGNING_KEY_PASSWORD` | Play release key password. | | `GH_SIGNING_KEYSTORE` | GitHub/free release keystore, base64 encoded. | | `GH_SIGNING_STORE_PASSWORD` | GitHub/free release keystore password. | | `GH_SIGNING_KEY_ALIAS` | GitHub/free release key alias. | | `GH_SIGNING_KEY_PASSWORD` | GitHub/free release key password. | | `FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT` | Firebase App Distribution service account JSON, base64 encoded. | ### Releasing to F-Droid, IzzyDroid and GitHub The draft gets its git tag only when it's published. F-Droid builds every tag that matches `^[\d.]+-\d+$`, usually a few days later, and IzzyDroid picks up the APK from the published release. Neither has a staged rollout, so publish the draft only once the Play rollout has reached 100%. A version that turns out broken on Play is never published. Keep its draft until the next release is published: the next version is counted from the tags and the drafts, so deleting it earlier hands its version code out again, which Play rejects. #### Checking on F-Droid F-Droid takes a release in three steps: 1. **Picked up:** the checkupdates bot adds the new tag to [Voice's build recipe](https://gitlab.com/fdroid/fdroiddata/-/commits/master/metadata/de.ph1b.audiobook.yml) with the commit `bot: Update Voice to `. It only takes the newest tag, so a release that already has a successor when the bot runs is never built. 2. **Built:** the [last build cycle](https://monitor.f-droid.org/builds/build) shows when it ran and lists the successful and failed builds. The build log of a version, failed or not, is at `https://monitor.f-droid.org/builds/log/de.ph1b.audiobook/`. 3. **Published:** [Voice on F-Droid](https://f-droid.org/packages/de.ph1b.audiobook/) lists each version with the day it was added. ### Tester Builds To send a build to testers, trigger the [Firebase App Distribution Workflow](https://github.com/PaulWoitaschek/Voice/actions/workflows/firebase_app_distribution.yml) on the branch to test. Testers install the build through the Firebase App Tester app. The workflow builds a `playRelease` APK with the version code of the upcoming release and a version name suffixed with the commit, then uploads it with the Firebase CLI. It notifies the `beta-testers` group by default; clear the `groups` input to only upload. The APK is signed with the GitHub key, which is the app signing key Google Play re-signs with, so tester builds install over the Play Store version. App Distribution is not part of the Gradle build, so the free flavor and F-Droid builds don't depend on it. It runs in the `release` environment and needs `FIREBASE_APP_DISTRIBUTION_SERVICE_ACCOUNT`: a service account with the Firebase App Distribution Admin role in the Firebase project of the Play `google-services.json`. ## Play Store Artwork Play Store artwork is generated from the `artwork/` Node project and written to `fastlane/metadata/android/*/images`. Use: ```sh ./scripts/generate_artworks.sh ``` The generator uses: - `artwork/public/logo.svg` for the Play Store icon. - `artwork/public/raw/phone/*.png` for raw app screenshots, captured at 1080×2424 on a Pixel 9 emulator with the system UI demo mode on. - `artwork/public/raw/feature/1.png` for the phone in the feature graphic. - `artwork/src/templates/` and `artwork/src/styles/` for the rendered Play Store images. `PHONE_TONES` in `template.ts` gives each screenshot a background that matches the colors of its screen. - `fastlane/metadata/android/*/marketing.yml` for localized feature graphic text and screenshot captions. Only locales with `marketing.yml` get localized generated images. The script removes old generated screenshot folders before writing the current output, so deleted tablet screenshots do not remain in the repository. To take the raw screenshots again, run `./scripts/take_store_screenshots.py`. It needs `ffmpeg` and starts an agent emulator of its own (see the emulator skill), so your devices and library stay untouched. It adds a generated library with the books of the kiosk mode, and uses kiosk mode for the library, bookmarks, history, and feature graphic. The `artwork/package-lock.json` file is intentionally not checked in. CI and local generation use `npm install` from `artwork/package.json` to keep this small tool out of regular lockfile update churn. ## Versioning Voice uses [calendar versioning (CalVer)](https://calver.org/). - Version name format: `YY.M.RELEASE` - `YY`: release year, last 2 digits (2025 → 25) - `M`: release month (1–12) - `RELEASE`: counter for releases within the same month (starting at 1) - Tag format (used by CI): `YY.M.RELEASE-CODE` - `CODE` is the numeric Android versionCode derived from the version name using the formula below. Examples: - First release in September 2025 → version name `25.9.1` - Tag includes version code → `25.9.1-5309001` ### Version code The Android `versionCode` is calculated from the version name with: `CODE = (YY + 28)(MM as 2 digits)(RELEASE as 3 digits)` Example: `25.9.1` → `(25+28)=53`, `MM=09`, `RELEASE=001` → `5309001`. This code is embedded in the git tag by `determine_release_tag.main.kts` and used by CI and Fastlane. Builds without `-Pvoice.versionCode` take the code of the newest release tag. So a local build installs over that release from Play, and Play replaces it with the next release. ## Website The [website](https://voice.woitaschek.de/) is an Astro project in `website/`, for listeners only. Contributor docs like this one stay in `docs/` and are read on GitHub. ```sh cd website npm install npm run dev ``` - The landing page is `website/src/pages/index.astro`. Its phones show the raw captures from `artwork/public/raw/`, so new store screenshots show up on the website too. Each section picks its capture by number. - The FAQ and the Audiobookshelf guide are Markdown in `website/src/pages/`. The privacy policy page renders `PRIVACY.md`. - Old URLs redirect in `website/astro.config.mjs`. The [Pages workflow](https://github.com/PaulWoitaschek/Voice/actions/workflows/deploy_pages.yml) builds the website on pull requests that touch it and deploys it when such a change lands on `main`.