This is an Android frontend for our C++ OpenDocument.core library. Feel free to use it in your own project too, but please don't forget to tell us about it!
More information at https://opendocument.app/ and in the app itself.
Please help to translate on the https://crowdin.com/project/opendocument
A JDK and the android SDK, and that is the whole list - ./gradlew assembleProDebug
works on a fresh checkout. There is no NDK to install, no python, and no conan: the
app compiles no native code of its own, and odrcore arrives as an ordinary maven
dependency (app.opendocument:odr-core-android) carrying both halves of its JNI
bindings, the java classes and a libodr_jni.so per ABI.
It resolves from maven central rather than github packages on purpose - github packages demands authentication even for a public artifact, which no clean source builder such as f-droid can supply. No credentials are involved anywhere in the build.
Debug builds need no setup. Release variants are signed only if the credentials are
supplied from outside the repository, 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 |
Without them bundleProRelease and friends still build, just unsigned.
The release workflow builds both signed bundles and uploads them to the Play Store
internal track - the same thing the fastlane lanes did from a laptop. It is dispatched
by hand, with the version it should build:
gh workflow run release.yml -f version=v4.14.0Nothing triggers it on a tag. It runs as three jobs:
| job | what it does |
|---|---|
build |
one gradle run producing all three signed flavors, archived on the run |
upload |
one job per play flavor, handing its bundle to fastlane |
record |
once both landed: tag the commit, draft the GitHub release |
Lite and Pro always go out together, and nothing chooses one: they are the same app with
ads and tracking switched off. Foss is built in the same run but uploaded nowhere - it is
the APK on the GitHub release. That is what keeps a version on a single commit - the one
the v* tag names and F-Droid builds.
Internal is the only track it uploads to. Anything wider - closed, open, production - is a promotion in the Play Console, which moves the same bundle and version code that was tested onto the wider track instead of uploading a second one, and is where the release notes get written. It is also where the review that a production release waits on actually happens, so the workflow finishing is not the same as the release being out.
If one flavor's upload fails, press "Re-run failed jobs". Only that upload runs again,
against the bundle already built and signed, and record runs behind it. Re-running all
jobs is the wrong button: Play refuses a version code it has already accepted, so the half
that made it cannot go up twice. Past the roughly 30 days GitHub offers re-runs for, the
way out is a new patch version for both flavors.
dry_run builds and signs both flavors without uploading either. It is the only run
allowed to go without a version, and the only one leaving neither tag nor draft.
In its first seconds the run also refuses a version that has already gone out and one
with no CHANGELOG.md section, which is what the release body is made of.
.github/scripts/resolve-version.py and changelog-section.py decide both; run either
by hand to see what a dispatch would do.
It needs these repository secrets:
| 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 |
play console service account json key |
The service account key goes in as the json file the Play Console hands out, whole and
unedited - base64 of it is accepted too, but nothing else is: the workflow checks it is
a service_account key before the build starts rather than letting fastlane trip over
it once the build is done.
Releasing from a laptop still works: fastlane android deployPro version:v4.8.0 builds
and uploads, and takes an optional track: (... track:beta). The version can come from
ODR_VERSION instead, but it cannot be left out - see below. That reads the key from
fastlane_google_play.json in the repository root, as the Appfile says.
No tag triggers a build, and none is pushed before one. A tag written up front is a
promise the run can fail to keep: v4.9.0's tag push run failed and the upload came
from a dispatched run - the same commit that time, which was luck.
Tags are written afterwards instead, in two kinds:
| tag | who writes it | what it means |
|---|---|---|
build/<version> |
the release workflow, once both flavors are up | this commit went to the internal track |
v<version> |
publishing the drafted release | this is what shipped |
One build tag, not one per flavor: a single run builds both from a single checkout. A half uploaded release gets no tag at all, which is the honest answer - nothing yet could be published from it. A lane run from a laptop leaves none either.
The v* tag is written neither by hand nor by the workflow. record drafts a GitHub
release named v<version> at the built commit, carrying the Foss APK - the sideloadable
copy every release up to v4.6 has had, now the flavor that links nothing proprietary -
a version.json naming the version and its code, and the version's CHANGELOG.md
section above GitHub's generated list of pull requests. A draft creates no tag; publishing it does, at
exactly that commit:
gh release edit v4.14.0 --draft=falseThat is the whole manual step, and it waits because internal is not released: promotion to
production, and the review it needs, happen in the Play Console days later. F-Droid tracks
this repository through that release, so publishing any earlier would push a version to
F-Droid users that Google may never release. version.json is what it reads: the version
is nowhere in the tree, so releases/latest/download/version.json is the only place
F-Droid can learn a version code from.
The version is the release run's version input, and no version number is checked in
anywhere. The workflow hands it to gradle as -Podr.version, and app/build.gradle
derives both halves of it: v4.8.0 becomes version name 4.8.0 and version code
40800, two digits per part. Every part therefore has to stay below 100, which the
build refuses rather than folding 4.100.0 onto the same code as 5.0.0. Nobody bumps
it anywhere: a commit on main is not a release, and no number on main can describe
one that already went out.
All three parts have to be spelled out. A two-part v4.7 used to be padded to 4.7.0,
which meant one build could be tagged under two names, and the tags older than v4.8.0
are in both formats because of it. They are left as they are - a release asset is served
from a URL carrying its tag name, and F-Droid rebuilds old versions from those names -
so the rule only holds for what is tagged from here on.
Builds handed no version - local ones, PR builds, assembleProDebug - are 0.0.0.
Nothing reads it: no code in the app looks at its own version, and only what the release
workflow builds ever leaves the machine. Any build can be given a real one anyway, with
./gradlew assembleProRelease -Podr.version=v4.8.0.
Version codes up to 204 were counted by hand in AndroidManifest.xml, which is why the
first derived one is a five digit jump. That is one way: the Play Store only ever accepts
a code above the last one it saw.

