Android's WebView scales text by the system font-size setting; Chrome
ignores it and uses its own accessibility preference. With the system
font one step above default, the wrapper rendered every string ~20%
larger than the website at an identical layout width -- the brand
truncated to "mc-web...", message bodies wrapped early, and the footer
broke apart. Pin textZoom to 100 so the wrapper matches the browser.
The footer was fragile independently of that. Status, region badge and
the "Updated:" time sat in a justify-content-between row with no wrap,
so once they exceeded the width each item broke internally instead of
the row reflowing -- a dot with "Polaczono" stranded below it, a time
split from its own label. Longer translated labels ("Aktualizacja:" vs
"Updated:") hit this well before English did. Adding flex-wrap plus
text-nowrap on the items makes them reflow as whole units; .badge
already carries white-space: nowrap, so it needs nothing.
Verified in Chrome against the local container, pl locale, region badge
forced visible, at 412/360/320 CSS px and with a 1.2x text-only override
that reproduces textZoom: before/after screenshots show the reported
breakage and a clean two-line reflow respectively, with no horizontal
overflow. At full width the change is a no-op.
Rides along in the already-pending, never-built wrapper 1.2.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
| Published build | mc-webui-wrapper.apk |
| Package | it.wojtaszek.mc.wrapper |
| Min / target SDK | 21 (Android 5.0) / 34 |
| 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
└── res/ layout, strings, icons
MainActivity.kt is the entire application. Worth knowing about it:
- The server address is stored in
SharedPreferencesand 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-
httpschemes 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.jsdefines a stand-inwindow.Notificationand forwards every call to a@JavascriptInterfacebridge inMainActivity.kt, which posts through Android's ownNotificationManager. 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 ofDOMContentLoaded, 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
grantedoutright; after a refusal,shouldShowRequestPermissionRationaleis 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
- Limitation: notifications only arrive while the app's process is alive — open, or recently backgrounded. Android eventually suspends it and they stop. This is the same limit the PWA has; real background delivery would need a foreground service or push from the server
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
releasevariant, 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
- Bump
versionCode(and usuallyversionName) insrc/app/build.gradle.kts - Build the signed release APK
- Copy it here as
mc-webui-wrapper.apk - Update the version, size and SHA-256 in
docs/android-app.md—sha256sumon Linux,Get-FileHashin PowerShell - Mention the change in
docs/whatsnew.md