diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-27 11:47:35 +0100 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-27 11:47:35 +0100 |
| commit | 101e8fbf1b277da74a3510435e28fd4bcaa6edae (patch) | |
| tree | 460c1a062d53badf8301d9429507db5470dc28f5 /packages | |
| parent | 89358ec1e8323d2ce11b4e10e84d0389e806b8dc (diff) | |
| download | meshbay-101e8fbf1b277da74a3510435e28fd4bcaa6edae.tar.gz | |
fix(android): keep the screen on while a video plays
The WebView has no Screen Wake Lock API, so the player's request did
nothing. The bridge now supplies navigator.wakeLock over a native
screen:keep-on channel (the WebView's keepScreenOn).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages')
5 files changed, 47 insertions, 2 deletions
diff --git a/packages/meshbay-android/README.md b/packages/meshbay-android/README.md index d44b58c3..4070bbbb 100644 --- a/packages/meshbay-android/README.md +++ b/packages/meshbay-android/README.md @@ -27,6 +27,9 @@ freezes the page 60 s after the screen goes off. Where play services are absent, the page is offered no cast at all. Music playing on the phone itself holds the same service and the same visibility, for as long as it plays (`playback:keep-alive`), so the next track still loads with the screen off. +The WebView has no Screen Wake Lock API; the bridge supplies +`navigator.wakeLock` over `screen:keep-on` (the WebView's `keepScreenOn`), so a +film playing does not let the screen time out. ```bash # needs JDK 17+ and an Android SDK (ANDROID_HOME, or sdk.dir in local.properties) diff --git a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js index e8b40b2d..e78a2337 100644 --- a/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js +++ b/packages/meshbay-android/app/src/main/assets/bridge/meshbay-bridge.js @@ -46,6 +46,45 @@ port.postMessage(JSON.stringify({ id, ch: channel, args })); }); + // The Screen Wake Lock API, which Chrome has and the WebView does not: the + // video player (and music, when asked) holds it so the screen does not time + // out mid-film. Held natively as the WebView's keepScreenOn, and released + // when the page hides, as the API does. Cleared first, for a reload that + // left one held. + if (!('wakeLock' in navigator)) { + const held = new Set(); + const sync = () => call('screen:keep-on', held.size > 0).catch(() => {}); + class WakeLockSentinel extends EventTarget { + constructor() { super(); this.type = 'screen'; this.released = false; this.onrelease = null; } + release() { + if (!this.released) { + this.released = true; + held.delete(this); + sync(); + const event = new Event('release'); + if (typeof this.onrelease === 'function') this.onrelease(event); + this.dispatchEvent(event); + } + return Promise.resolve(); + } + } + const request = (type) => { + if (type !== 'screen') return Promise.reject(new TypeError('Unsupported wake lock type')); + if (document.visibilityState !== 'visible') { + return Promise.reject(new DOMException('The page is not visible', 'NotAllowedError')); + } + const sentinel = new WakeLockSentinel(); + held.add(sentinel); + sync(); + return Promise.resolve(sentinel); + }; + document.addEventListener('visibilitychange', () => { + if (document.visibilityState !== 'visible') [...held].forEach((s) => s.release()); + }); + Object.defineProperty(navigator, 'wakeLock', { value: Object.freeze({ request }) }); + sync(); + } + // A write is a binary message: "MBB1" | id | channel | 0 | handle | bytes, // big-endian — one copy, no JSON, no base64 (spike S-3: 136 MB/s awaited // per 48 KB chunk). Without ArrayBuffer messages, base64 over JSON. diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt index 75ac0156..e60535b9 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/MainActivity.kt @@ -134,6 +134,7 @@ class MainActivity : Activity() { channels = Channels(hub, onHubChanged = { runOnUiThread { reloadForHub() } }, hasCatalogue = { code -> hasAsset("ui/locales/$code.js") }, keys = keys, saves = saves, cast = cast, onPlayback = { on -> runOnUiThread { playing = on; keepAlive() } }, + onKeepScreenOn = { on -> runOnUiThread { web.keepScreenOn = on } }, push = PushChannels(this, PushState(getSharedPreferences(PushState.PREFS, Context.MODE_PRIVATE)), channelNames = { mapOf( Notifier.CHANNEL_CHAT to text.get("push.channel_chat", channels.locale), diff --git a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt index 19b0f3c2..1563810b 100644 --- a/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt +++ b/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/bridge/Channels.kt @@ -32,6 +32,7 @@ class Channels( private val saves: SaveSinks? = null, private val cast: CastChannels? = null, private val onPlayback: (Boolean) -> Unit = {}, + private val onKeepScreenOn: (Boolean) -> Unit = {}, private val push: PushChannels? = null, private val photos: PhotoChannels? = null, private val destinations: DestinationChannels? = null, @@ -48,6 +49,7 @@ class Channels( "ice:resolve-stun" -> resolveStun(args.optJSONArray(0) ?: JSONArray()) "ui:locale" -> setLocale(args.optString(0, "")) "playback:keep-alive" -> { onPlayback(args.optBoolean(0, false)); true } + "screen:keep-on" -> { onKeepScreenOn(args.optBoolean(0, false)); true } // Where downloads go, chosen once; a display name, never a URI. "folder:choose" -> saves().chooseFolder() diff --git a/packages/meshbay-hub/tests/test_android_shell.py b/packages/meshbay-hub/tests/test_android_shell.py index eedc1988..95b9d796 100644 --- a/packages/meshbay-hub/tests/test_android_shell.py +++ b/packages/meshbay-hub/tests/test_android_shell.py @@ -153,10 +153,10 @@ def test_the_shim_offers_desktop_channels_and_native_answers_each(): # A phone has a push distributor to talk to and a camera roll and an # address book to back up, and a desktop has none of them; those families are the ones the desktop lacks # rather than the ones it shares. Nor does a desktop ask before reaching the - # local network (Android 17). + # local network (Android 17), nor stand in for a Wake Lock API it has. phone_only = {c for c in shim if c.startswith(("push:", "photosync:", "phonesync:", "contactsync:", "calendarsync:", "drivesync:", - "wasync:", "lan:"))} + "wasync:", "lan:", "screen:"))} assert any(c.startswith("push:") for c in phone_only), "the push channels are gone" assert any(c.startswith("photosync:") for c in phone_only), "the backup channels are gone" assert shim - phone_only <= preload, f"channels the desktop does not have: {shim - preload}" |