Files
mc-webui/android/README.md
T
MarekWoandClaude Opus 5 b40a28c3b4 docs(android): describe the path for releasing a new version
Closes the last open [CLAUDE] item in the Play release plan. The section only
covered the .apk in this repository; the app has been on Google Play since
2026-09-05 and the two have to go out together or they drift apart. Adds what
Play needs on top - an .aab rather than an .apk, a versionCode that has not been
used, release notes per listing language, and the declaration a foreground
service always triggers - plus the promote-from-library shortcut that made the
first production review take hours instead of days.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-06 10:50:46 +02:00

149 lines
7.4 KiB
Markdown

# Android companion app — source
The app users install is a thin WebView wrapper around their own mc-webui
instance: one screen for the server address, one full-screen WebView for the
interface itself. It contains no mesh logic and talks to nothing except the
server the user typed in.
The sources are here so that anyone installing the `.apk` can see what they are
installing, and build it themselves if they prefer. For install instructions,
see [Android App](../docs/android-app.md).
| | |
|---|---|
| **Published build** | [`mc-webui-wrapper.apk`](mc-webui-wrapper.apk) |
| **Package** | `it.wojtaszek.mc.wrapper` |
| **Min / target SDK** | 21 (Android 5.0) / 36 (Android 16) |
| **Permissions** | `INTERNET`; `CAMERA` for QR scanning; `POST_NOTIFICATIONS` for new-message alerts; `WRITE_EXTERNAL_STORAGE` (Android 9 and older) for saving downloads |
## Layout
```
src/
├── settings.gradle.kts, build.gradle.kts, gradle.properties
├── gradle/wrapper/gradle-wrapper.properties
└── app/
├── build.gradle.kts
└── src/main/
├── AndroidManifest.xml
├── assets/notification_shim.js ← window.Notification
├── java/it/wojtaszek/mc/wrapper/
│ ├── MainActivity.kt ← the whole app
│ └── MeshWatchService.kt ← background delivery
└── res/ layout, strings, icons
```
`MainActivity.kt` is the entire application. Worth knowing about it:
- The server address is stored in `SharedPreferences` and **only ever replaced
by the user**. A failed connection or a Back press shows the address form
pre-filled — it never wipes what was saved
- Back on the first mc-webui page asks: exit, change server, or cancel
- Links to other hosts (URLs in messages, the packet analyzer) and non-`http`
schemes are handed to the system, so the app stays on the user's instance
- The page's camera request (QR scanning) is mirrored to an Android permission
request; downloads go to the phone's Downloads folder via `DownloadManager`
## Notifications
Android's WebView ships **no Web Notifications API at all** — `window.Notification`
is simply undefined, so mc-webui detects that and greys its notification toggle
out as "Unavailable". Native code has to supply the missing piece:
- `assets/notification_shim.js` defines a stand-in `window.Notification` and
forwards every call to a `@JavascriptInterface` bridge in `MainActivity.kt`,
which posts through Android's own `NotificationManager`. **mc-webui itself
needs no changes** — the page uses the standard API and never knows
- The shim is injected from `onPageStarted`, which runs as the document begins
loading. That is comfortably ahead of `DOMContentLoaded`, where mc-webui reads
the permission and decides whether to enable the toggle
- The web permission states map onto Android's: below Android 13 posting needs
no permission so it is `granted` outright; after a refusal,
`shouldShowRequestPermissionRationale` is what separates "ask again"
(`default`) from "blocked for good" (`denied`)
- The notification channel is created at startup, which is also what puts the
app in Android's notification settings list at all
- **Background delivery** (from `versionCode 6`) is `MeshWatchService`, a
foreground service of type `remoteMessaging`. It does no work: it exists so
Android leaves the process out of the cached-process freezer, which is what
used to silence alerts minutes after backgrounding. The page keeps producing
every notification, so mutes, blocked senders, channel names, translations and
deep links stay in one place
- The service follows mc-webui's own notification toggle. The shim reads
`mc_notifications_enabled` from `localStorage` and reports it over the bridge —
read rather than asked of the page, because the wrapper connects to whichever
server its user typed in, very often an older mc-webui. It also wraps
`Storage.prototype.setItem`, since localStorage fires no event in the document
that wrote it
- `setRendererPriorityPolicy(RENDERER_PRIORITY_IMPORTANT, false)` goes with it.
The page renders in a process of its own, and WebView waives that process's
priority once the view stops being visible — a service that saves only the
host process would still lose the renderer under memory pressure
- Android 12 forbids starting a foreground service from the background, so the
service is only ever started while the activity is on screen: from the shim's
report and again from `onResume`
## Building
Open `src/` in Android Studio (the Gradle wrapper JAR is not checked in —
Android Studio supplies it) and let it sync. Then:
- **Debug build:** *Build → Build Bundle(s) / APK(s) → Build APK(s)* →
`app/build/outputs/apk/debug/app-debug.apk`. Fine for trying things out on
your own phone, not for publishing — it is signed with the throwaway debug
key and is marked debuggable
- **Release build:** *Build → Generate Signed App Bundle or APK → APK*, pick
the project keystore, choose the `release` variant, and let it build. The
result is what ships
### Signing
Android identifies an app by its package name **and its signing key**. An
update only installs over an existing app when both match, so a release signed
with a different key forces every user to uninstall first — losing their saved
server address. In practice this means:
- Use the **same keystore for every release**, from the first published one
- **Back it up** (and its passwords) somewhere that survives a reinstalled
laptop. There is no way to recover or reissue it
- Never commit the keystore or its passwords to this repository
### Publishing a new build
The app goes out through two channels, from one build of one commit. Do them in
the same sitting, or the `.apk` here and the Store drift apart.
**Both channels, first:**
1. Bump `versionCode` — Play rejects an upload that reuses one — and usually
`versionName`, in `src/app/build.gradle.kts`
2. Mention the change in [`docs/whatsnew.md`](../docs/whatsnew.md)
3. Check whether the change touches anything
[`docs/privacy-policy.html`](../docs/privacy-policy.html) claims: it lists
every permission and everything stored on the phone, in both languages, and
Play reviews it
**The `.apk` in this repository:**
4. Build the signed release **APK**
5. Copy it here as `mc-webui-wrapper.apk`
6. Update the version, size and **SHA-256** in
[`docs/android-app.md`](../docs/android-app.md) — `sha256sum` on Linux,
`Get-FileHash` in PowerShell
**Google Play** (`it.wojtaszek.mc.wrapper`):
7. Build a signed **App Bundle** (`.aab`) — Play does not take an APK
8. Play Console → *Production* → *Create new release* → upload the `.aab`
9. Answer whatever new declarations the build triggers. A new permission
usually asks for one; **a foreground service always does**, and its
declaration wants a link to a **video** showing the feature in use
10. Write the release notes in every language the listing has (en-US and pl-PL),
500 characters each
11. Send for review. A production review is normally hours to a couple of days
> **Where a change has already been tested by the closed track**, promote that
> exact build from *Releases overview → Add from library* instead of building
> again. Play has already reviewed the artifact, which is what made the first
> production review take hours rather than days.