The Android app of OpenDocument.core. More at https://opendocument.app/.
| edition | where | ads | edits |
|---|---|---|---|
| Lite | Play, free | yes | inside one paragraph, sheet cells, plain text, highlighter |
| Pro | Play, paid | no | all of Lite, plus new and joined paragraphs, formatting, PDF marks |
| Foss | F-Droid, GitHub release | no | the same as Pro |
Every edition opens every format. Foss is at.tomtasche.reader.foss, a different app id, so it
installs beside a Play install and does not update one. Each GitHub release carries one apk,
app-foss-release.apk, which Obtainium tracks with no settings changed.
You need a JDK and the Android SDK. Nothing else: no NDK, no python, no conan. The core arrives
as the maven central artifact app.opendocument:odr-core-android, java classes and
libodr_jni.so per ABI.
./gradlew assembleProDebug # also assembleLiteDebug, assembleFossDebug
./gradlew testProDebugUnitTest # jvm tests
./gradlew connectedAndroidTest # device tests
./gradlew spotlessApply lintProDebugRelease variants build unsigned unless these are set, as gradle properties in
~/.gradle/gradle.properties or as environment variables:
| gradle property | environment variable | meaning |
|---|---|---|
odr.keystore |
ODR_KEYSTORE |
path to the keystore |
odr.keystorePassword |
ODR_KEYSTORE_PASSWORD |
store password |
odr.keyPasswordPro |
ODR_KEY_PASSWORD_PRO |
key password, defaults to store |
odr.keyPasswordLite |
ODR_KEY_PASSWORD_LITE |
key password, defaults to store |
No version number is checked in. The release run passes its version input to gradle as
-Podr.version, and app/build.gradle derives versionName and versionCode from it:
v4.8.0 becomes 4.8.0 and 40800. All three parts are required, and each must be below
100. Builds with no version are 0.0.0. To build a real one locally:
./gradlew assembleProRelease -Podr.version=v4.8.0English plus 19 translations of the app, and 15 locales of the Play listing, all in this repository.
| source | translated by | |
|---|---|---|
| the app | app/src/main/res/values/strings.xml |
scripts/translate-app.py |
| the listing | fastlane/metadata/android/en-US/ |
by hand |
| the release notes | CHANGELOG.md |
scripts/store-copy.py |
Both scripts run claude -p once per language, then a second pass reads the draft back and
rejects an answer that lost a key, a paragraph or an address. Neither script uploads.
scripts/translate-app.py # fill in the missing strings
scripts/store-copy.py v4.15.0 # write the release notes of a version
scripts/store-listing.py --version v4.15.0 # check every locale has themResource directories carry no region: values-de serves every German region. A new app
language is added to LANGUAGES in scripts/translate-app.py. A new store locale is added
to LOCALES in scripts/store-listing.py, and the release checks the directories against
that list. See fastlane/metadata/README.md for the listing layout.
Corrections in a pull request are welcome.
The release workflow is dispatched by hand with the version to build. No tag triggers it.
gh workflow run release.yml -f version=v4.14.0Before the build it refuses a version that already shipped, a version with no CHANGELOG.md
section, and a version with release notes missing in any locale. Then it runs these jobs:
| job | what it does |
|---|---|
build |
one gradle run, all three signed flavors |
screenshots |
one emulator per device, six screens in fifteen locales |
screenshot-set |
joins the two devices' pictures and checks the set |
upload |
one job per Play flavor, bundle to the internal track |
listing |
after the upload: title, descriptions, release notes, screenshots, per flavor |
record |
tags build/<version> and drafts the GitHub release |
Lite and Pro always ship together. Foss is built in the same run and attached to the GitHub
release. The listing job overwrites the Play Console listing; the console is not the source.
A failed screenshot run costs the release its pictures and nothing else: the listing still
uploads with the text alone. dry_run builds and signs without a version and uploads nothing.
If one flavor's upload fails, press "Re-run failed jobs". Play refuses a version code it has already accepted, so re-running all jobs fails on the flavor that made it. After GitHub stops offering re-runs, ship a new patch version.
Secrets the workflow needs:
| secret | contents |
|---|---|
ODR_KEYSTORE_BASE64 |
base64 -i google_play.keystore |
ODR_KEYSTORE_PASSWORD |
store password |
ODR_KEY_PASSWORD_PRO |
key password for the reader-pro alias |
ODR_KEY_PASSWORD_LITE |
key password for the reader alias |
GOOGLE_PLAY_SERVICE_ACCOUNT |
the Play Console service account json, whole; base64 is accepted too |
The workflow uploads to the internal track only. Wider tracks are promotions of the same bundle, because a version code can only go up once:
fastlane android openTestingPro version:v4.17.0 # or openTestingLiteClosed testing and production are promoted in the Play Console, and Play reviews them.
No tag is written before a run, and none triggers one. record writes build/<version>
once both flavors are up; a half-uploaded release gets no tag.
Publishing the drafted GitHub release creates the v<version> tag, which F-Droid builds
from. Publish it only after Play released the version to production:
gh release edit v4.14.0 --draft=falseThe draft carries the Foss apk, the version's CHANGELOG.md section, and version.json,
which is where F-Droid reads the version code from.
fastlane android listingPro and listingLite upload the listing without a bundle. With
nothing under fastlane/framed they send the text alone, so a typo needs no new version and
no emulator.
fastlane android deployPro version:v4.8.0 (or deployLite) builds and uploads the bundle
and its listing, and takes an optional track:. It reads the service account key from
fastlane_google_play.json in the repository root. The version can also come from
ODR_VERSION, but it cannot be left out. A lane writes no tag.
The release run takes the store screenshots from the build it ships. Nothing is committed. Six screens on a phone and a tablet in fifteen locales, plus a feature graphic per locale drawn from the first screenshot. The tablet set fills both tablet slots.
To take them by hand you need one emulator on adb running Android 15 or newer and Pillow:
python3 -m pip install Pillow
bundle exec fastlane android screenshots # every locale, phone
ODR_SCREENSHOT_DEVICE=tablet bundle exec fastlane android screenshots
ODR_SCREENSHOT_LANGUAGES=en-US,de-DE bundle exec fastlane android screenshotsANDROID_SERIAL picks the device when several are attached. Raw captures land in
fastlane/screenshots/, the framed set and feature graphics in fastlane/framed/. Running
scripts/frame-screenshots.py alone re-frames the captures, so a changed headline in
fastlane/frames/frames.json costs seconds. Below API 35 the status bar has a white clock on
a white bar, so ScreenshotTests refuses to run there.
hi-IN, ja-JP and zh-CN are set in a system font. On Debian install fonts-noto-core
and fonts-noto-cjk, or the framing stops and says so.
Mozilla Public License 2.0, in LICENSE. MPL is copyleft per file. Two parts came from
elsewhere under Apache-2.0 and keep their own headers: com/commonsware/android/print, and
FindActionModeCallback with the two webview_find resources.


