Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,8 +294,9 @@ jobs:
if-no-files-found: warn

# The two halves put back together, and only now checked: a set is both devices
# in every locale, and neither runner above can see the other's. Republished
# under the name the listing job reads.
# in every locale, plus the feature graphic the phone's half draws, and neither
# runner above can see the other's. Republished under the name the listing job
# reads.
screenshot-set:
needs: screenshots
runs-on: ubuntu-24.04
Expand Down
18 changes: 14 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@ lists rather than forking it. Reach for it before `adb shell input tap`.

## Store screenshots

The store *copy* is written down here; the screenshots are not. A picture of the app is
The store *copy* is written down here; the pictures are not. A picture of the app is
worth what the build it came off is worth, so the release run takes its own - six screens
on a phone and a tablet in all fifteen locales - frames them and hands them to supply.
Nothing is committed, and `.gitignore` says so. `OpenDocument.ios` does the same thing
against App Store Connect, and the python is deliberately close enough to lift out later.
on a phone and a tablet in all fifteen locales - frames them, draws the feature graphic
off the first of them, and hands the lot to supply. Nothing is committed, and
`.gitignore` says so. `OpenDocument.ios` does the same thing against App Store Connect,
and the python is deliberately close enough to lift out later.

**`ScreenshotTests` is the whole of it.** An instrumented test runs in the app's own
process, so laying the samples out, filling the recent list and switching the app's
Expand All @@ -76,6 +77,15 @@ the test can already reach.
published dimensions - onto a canvas of its own, because play refuses a picture more
than twice as long as it is wide and a Pixel 9 Pro XL is 2.23:1 before anything is drawn
around it. `store_screenshots.py` says what a full set is and stages it.
- **The feature graphic is one of the pictures**, not a file in the tree: the same parts
laid out across a 1024x500 canvas, drawn from the first screenshot's capture and
carrying its headline, so there is no second copy to write or translate. The one that
*was* committed showed the pre-4.14 app in every storefront for three releases, which
is what a listing asset nothing regenerates comes to. Only the launcher icon is left.
- **The tablet's pictures go into both tablet slots.** Play falls back to the phone set
only where a slot is *empty*, and the 7" one was not - so it, too, showed the old app.
One capture serves both: 1600x2560 is inside the 7" slot's limits as well as the 10"
one's.
- Which locale reads which language's documents is one table, in
`store_screenshots.py`. The generator checks its own languages against it and writes it
into the assets, and `ScreenshotTests` reads it from there - do not write a second copy
Expand Down
67 changes: 32 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ F-Droid and Play are the two stores. Sideloaders take `app-foss-release.apk` fro
which is what Obtainium tracks - point it at this repository and it needs no filter, one
apk is all a release carries.

That apk is `at.tomtasche.reader.foss`, and up to 4.13.0 it was `at.tomtasche.reader.pro`.
A different application id is a different app, so the old one neither updates nor complains:
install the new one, uninstall the old one. Nothing carries over and nothing is worth
carrying - a recent documents list whose uri permissions die with the old package anyway.
That apk is `at.tomtasche.reader.foss`. A different application id is a different app, so
a sideload carrying an older one neither updates nor complains: install the new one and
uninstall the old one. Nothing carries over - a recent documents list whose uri permissions
die with the old package anyway.

## Translations

Expand Down Expand Up @@ -87,8 +87,7 @@ Without them `bundleProRelease` and friends still build, just unsigned.
## Releasing

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:
internal track. It is dispatched by hand, with the version it should build:

```sh
gh workflow run release.yml -f version=v4.14.0
Expand Down Expand Up @@ -117,10 +116,10 @@ review that a production release waits on actually happens, so the workflow fini
not the same as the release being out.

The listing goes up in its own job, behind the bundle: the title, both descriptions, the
release notes of that version and the screenshots, in all fifteen locales, for each app.
**This overwrites what the Play Console says**, which is the point - the copy is written
here now, not there. The release notes are no longer typed into the promotion box. The
icon and the feature graphic are still not uploaded; see `fastlane/metadata/README.md`.
release notes of that version, the screenshots and the feature graphic, in all fifteen
locales, for each app. **This overwrites what the Play Console says**, which is the point
- the copy is written here, not there. The launcher icon is the one listing asset left where
it is; see `fastlane/metadata/README.md`.

Separate from the bundle upload on purpose: a bundle cannot go up twice, while the listing
stays editable for as long as the release sits on the internal track - and the listing is
Expand All @@ -141,13 +140,19 @@ way out is a new patch version for both flavors.

### Screenshots

The store copy is written down here; the screenshots are not. A picture of the app is worth
The store copy is written down here; the pictures of the app are not. A picture is worth
what the build it came off is worth, so they are taken during the release run, from the
build going out, framed there, and handed to supply from there. Nothing is committed.

Six screens - the recently opened list, a text document with a search running, a
spreadsheet, an edit under way, a PDF and a Word file - on a phone and on a tablet, in the
fifteen locales the listing is written in. That is 180 pictures a release.
fifteen locales the listing is written in. That is 180 pictures a release. The tablet's go
into both of Play's tablet slots: it falls back to the phone set only where a slot is
*empty*.

The feature graphic, the one picture above the listing, is drawn beside them - the same
frame laid out across a 1024x500 canvas, off the first screenshot's capture and carrying
its headline in each of the fifteen languages. One per locale.

Taking them by hand needs one emulator on adb running **Android 15 or newer**, and Pillow:

Expand All @@ -159,8 +164,8 @@ ODR_SCREENSHOT_LANGUAGES=en-US,de-DE bundle exec fastlane android screenshots
```

With more than one device attached, `ANDROID_SERIAL` picks which. The raw captures land in
`fastlane/screenshots/`, the framed set in `fastlane/framed/`, and only the second is what
the store is given. Re-running `scripts/frame-screenshots.py` alone re-frames what is
`fastlane/screenshots/`, the framed set and the feature graphics in `fastlane/framed/`, and
only the second is what the store is given. Re-running `scripts/frame-screenshots.py` alone re-frames what is
already captured, so changing a headline in `fastlane/frames/frames.json` costs a second of
Pillow rather than a quarter hour of emulators.

Expand Down Expand Up @@ -208,11 +213,8 @@ the lane's - and takes an optional `track:` (`... track:beta`). The version can

### Tags

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:
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. Tags are written afterwards instead, in two kinds:

| tag | who writes it | what it means |
|---|---|---|
Expand All @@ -224,11 +226,10 @@ half uploaded release gets no tag at all, which is the honest answer - nothing y
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:
release named `v<version>` at the built commit, carrying the Foss APK - 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:

```sh
gh release edit v4.14.0 --draft=false
Expand All @@ -251,26 +252,22 @@ build refuses rather than folding `4.100.0` onto the same code as `5.0.0`. Nobod
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.
All three parts have to be spelled out: a two-part `v4.7` is refused rather than padded,
so one build cannot be tagged under two names.

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.
Version codes only ever go up: the Play Store accepts a code only above the last one it
saw, so a version number cannot be reused or walked backwards.

## License

Mozilla Public License 2.0, in `LICENSE`, replacing the GPL-3.0-or-later this carried
before. MPL is copyleft per *file*: a changed file goes back under MPL, and a larger work
that merely links this can stay under whatever license it likes.
Mozilla Public License 2.0, in `LICENSE`. MPL is copyleft per *file*: a changed file goes
back under MPL, and a larger work that merely links this can stay under whatever license
it likes.

The notice sits in `LICENSE` rather than atop every source file, which Exhibit A of the
license itself allows. Two things keep their own headers because they came from elsewhere
Expand Down
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file removed app/src/main/icon-web.png
Binary file not shown.
22 changes: 13 additions & 9 deletions fastlane/Fastfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ ROOT = File.expand_path("..", __dir__).freeze
# same thing, and the few places they differ are read over the shared text.
STAGED_LISTING = File.join(ROOT, "fastlane", ".listing").freeze

# The screenshots, which unlike the copy are not written down here: they are taken
# The pictures, which unlike the copy are not written down here: they are taken
# during the release run, from the build going out, framed, and handed to supply
# from there. A picture of the app is worth what the build it came off is worth.
# from there. A picture of the app is worth what the build it came off is worth -
# as true of the feature graphic drawn beside them as of a screenshot.
SCREENSHOT_DIR = File.join(ROOT, "fastlane", "screenshots").freeze
FRAMED_DIR = File.join(ROOT, "fastlane", "framed").freeze

Expand Down Expand Up @@ -110,9 +111,9 @@ def stage_listing(flavor, version)
staged
end

# Puts the framed screenshots into the same tree, under the `images/` directory
# supply reads a locale's pictures from - so one directory is handed over and one
# edit goes to play.
# Puts the framed pictures - the screenshots and the feature graphic - into the
# same tree, under the `images/` directory supply reads a locale's pictures from,
# so one directory is handed over and one edit goes to play.
#
# A run with nothing captured writes the text alone, so fixing a word in a
# description does not cost a quarter hour of emulators. A run with something
Expand Down Expand Up @@ -224,7 +225,8 @@ platform :android do
end

# What the store says about the app: the listing text, this version's release
# notes, and the screenshots a capture run left under fastlane/framed.
# notes, and the pictures a capture run left under fastlane/framed - the
# screenshots and the feature graphic drawn beside them.
private_lane :uploadListing do |options|
flavor = options[:flavor]
version = require_version(options[:version])
Expand All @@ -241,9 +243,11 @@ platform :android do
# without a bundle, supply has to be told which release the notes belong to
skip_upload_aab: true,
version_code: version_code(version),
# the icon and the feature graphic are still the pre-4.14 ones and are their
# own job - see fastlane/metadata/README.md. only the screenshots go up.
skip_upload_images: true,
# Both come off the same capture run, and without one both are left alone.
# `images` is the feature graphic here: the icon is not staged, and what
# supply does not find in the tree it does not touch - see
# fastlane/metadata/README.md.
skip_upload_images: !captured,
skip_upload_screenshots: !captured
)
end
Expand Down
36 changes: 24 additions & 12 deletions fastlane/metadata/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,24 +73,36 @@ there is no translation of it that fits at all.
`CHANGELOG.md` at the root is the other record of the same release, written for
this repository rather than for the store.

## Screenshots
## Screenshots and the feature graphic

Not here, and not committed anywhere: they are taken during the release run, from
the build going out, and staged into this tree beside the text - one directory to
supply, one edit to Play. `scripts/store_screenshots.py` puts them under
`<locale>/images/phoneScreenshots/` and `.../tenInchScreenshots/`, which is where
supply reads a locale's pictures from.

The copy is written; a screenshot is taken. A picture of the app is worth what the
supply, one edit to Play. `scripts/store_screenshots.py` puts the screenshots under
`<locale>/images/phoneScreenshots/`, `.../tenInchScreenshots/` and
`.../sevenInchScreenshots/`, and the feature graphic at
`<locale>/images/featureGraphic.png`, which is where supply reads a locale's
pictures from.

The tablet's pictures go into both tablet slots. Play falls back to the phone set
only where a slot is *empty*, and the 7" one was not - it held five pictures of the
pre-4.14 app, and went on showing them through every release that rewrote the rest.

The feature graphic is the one Play shows above the listing. It is drawn from the
first screenshot's capture, by `scripts/frame-screenshots.py`, and says what that
screenshot says in the same fifteen languages - so it cannot be a picture of an app
that no longer looks like that, which is exactly what the committed one had become.

The copy is written; a picture is taken. A picture of the app is worth what the
build it came off is worth, so it is not a file that sits in git going quietly out
of date. See the README's "Screenshots" section for how to take them by hand.

## What is not uploaded

`images/` holds an icon and a feature graphic that predate the 4.14 redesign.
`skip_upload_images` stays on so they are left where they are; only the screenshots
staged above go up. Those two graphics are their own job.
`images/` holds an icon that predates the 4.14 redesign. Nothing stages it, and
what supply does not find in the staged tree it leaves alone, so it stays where it
is - the app's own launcher icon is a job of its own and a release is a poor moment
for it.

The four phone screenshots that used to sit beside them are gone: the release now
uploads its own, taken from the build it is shipping, so a stale copy in the tree
could only ever disagree with the store.
The feature graphic and the four phone screenshots that used to sit beside it are
gone: the release now draws and uploads its own, from the build it is shipping, so
a stale copy in the tree could only ever disagree with the store.
Binary file not shown.
Loading
Loading