mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-10-30 23:51:57 +01:00
fix(android): nudge the page, because nothing else will
Measured on a Galaxy S20 FE with the service running: the app process, the service and the WebView's own renderer process all stayed alive for an hour and a half, and the page still stopped after 56 minutes. Notifications were posted correctly from 11:29 to 12:25 - 45 of them, traced through the PendingIntent each one creates - and then nothing, while the server went on emitting messages at 12:28:46, 12:28:53 and 12:29:12. The server saw the client time out at 12:29:52 and no reconnection was attempted for the next 29 minutes, although socket.io retries forever by default. Delivery came back only when the activity was resumed. Ruled out with evidence rather than reasoning: Doze (the phone was charging and only reached full at 13:05), Wi-Fi (wpa_supplicant heartbeats every 10s, no drop), Samsung's Freecess freezer (it froze a dozen other apps by name that afternoon and never this one - the foreground service did its job), a killed renderer or app process (neither appears in any kill line), and socket.io giving up (reconnectionAttempts defaults to Infinity). What is left is that Chromium suspends the work of a hidden page. A foreground service buys the process; it does not make Chromium run the page inside it. That gap is not something the Stage B plan knew about. So the service now nudges the page every two minutes. evaluateJavascript is the lever: injected script does not queue behind the throttled timers the page's own setTimeout calls sit in. The script re-opens the socket if it has died, which is what mc-webui would have done a second later had its timers been running, and it reads mc-webui's own chatSocket rather than anything added for this, so it works against whichever server version the user typed in. The wake logs one line every two minutes under the McPageWake tag. That is deliberate: it is the only way to tell "the page answered" from "the page never ran the script" after the fact, which is exactly what the next measurement has to distinguish. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d14ebd6994
commit
8b5ed4ceb5
1 file changed
+78
-1
@@ -18,6 +18,9 @@ import android.net.http.SslError
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.webkit.CookieManager
|
||||
import android.webkit.HttpAuthHandler
|
||||
@@ -97,6 +100,16 @@ class MainActivity : AppCompatActivity() {
|
||||
/** Set by [connect]; drops the pages of whichever server we just left. */
|
||||
private var clearHistoryOnLoad = false
|
||||
|
||||
private val pageWakeHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
/** Reschedules itself for as long as the background service is wanted. */
|
||||
private val pageWakeTick = object : Runnable {
|
||||
override fun run() {
|
||||
wakePage()
|
||||
pageWakeHandler.postDelayed(this, WAKE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// Before setContentView, as androidx asks. Transparent rather than the
|
||||
@@ -175,6 +188,7 @@ class MainActivity : AppCompatActivity() {
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
pageWakeHandler.removeCallbacks(pageWakeTick)
|
||||
MeshWatchService.stop(this)
|
||||
}
|
||||
|
||||
@@ -740,6 +754,31 @@ class MainActivity : AppCompatActivity() {
|
||||
* to call from anywhere and as often as convenient: both directions are
|
||||
* no-ops once the service is already in the state being asked for.
|
||||
*/
|
||||
/**
|
||||
* Nudges the page, because nothing else will.
|
||||
*
|
||||
* Measured on a real device: with the service running, the process, the
|
||||
* service and the WebView's own renderer process all stayed alive for an
|
||||
* hour and a half - and the page still stopped after 56 minutes. Chromium
|
||||
* had suspended its work. A foreground service buys the process; it does
|
||||
* not make Chromium run a hidden page.
|
||||
*
|
||||
* `evaluateJavascript` is the lever, because injected script does not wait
|
||||
* in the throttled timer queues the page's own `setTimeout` calls sit in.
|
||||
* The script re-opens the socket if it has died, which is what mc-webui
|
||||
* would have done by itself a second later had its timers been running.
|
||||
*/
|
||||
private fun wakePage() {
|
||||
if (webView.visibility != View.VISIBLE) return
|
||||
// Global and idempotent, and free when nothing was paused
|
||||
webView.resumeTimers()
|
||||
webView.evaluateJavascript(PAGE_WAKE) { state ->
|
||||
// One line every two minutes, and the only way to tell "the page
|
||||
// answered" from "the page never ran the script" after the fact
|
||||
Log.d(WAKE_TAG, "wake -> $state")
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWatchService() {
|
||||
// Android's own switches have the last word: a revoked permission or a
|
||||
// channel turned off there leaves nothing for the service to show, and
|
||||
@@ -747,7 +786,13 @@ class MainActivity : AppCompatActivity() {
|
||||
// one thing worse than not raising them
|
||||
val allowed = notificationsWanted &&
|
||||
NotificationManagerCompat.from(this).areNotificationsEnabled()
|
||||
if (allowed) MeshWatchService.start(this) else MeshWatchService.stop(this)
|
||||
pageWakeHandler.removeCallbacks(pageWakeTick)
|
||||
if (allowed) {
|
||||
MeshWatchService.start(this)
|
||||
pageWakeHandler.postDelayed(pageWakeTick, WAKE_INTERVAL_MS)
|
||||
} else {
|
||||
MeshWatchService.stop(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -896,6 +941,38 @@ class MainActivity : AppCompatActivity() {
|
||||
// without the form turning into a page of its own
|
||||
private const val HISTORY_LIMIT = 6
|
||||
|
||||
/**
|
||||
* How often the page gets nudged while the app is away. Short enough
|
||||
* that a stalled page costs one notification rather than every one
|
||||
* after it, long enough to be invisible on the battery.
|
||||
*/
|
||||
private const val WAKE_INTERVAL_MS = 2 * 60 * 1000L
|
||||
private const val WAKE_TAG = "McPageWake"
|
||||
|
||||
/**
|
||||
* Asks the page how its connection is and puts it back if it has gone.
|
||||
* Reads mc-webui's own `chatSocket` rather than anything added for this,
|
||||
* so it works against whichever version of the server the user typed in;
|
||||
* where that name is missing the script simply reports so, and having
|
||||
* run at all is still what un-stalls the page.
|
||||
*/
|
||||
private const val PAGE_WAKE = """
|
||||
(function () {
|
||||
try {
|
||||
if (typeof chatSocket === 'undefined' || !chatSocket) return 'no-socket';
|
||||
if (chatSocket.connected) return 'connected';
|
||||
try {
|
||||
chatSocket.connect();
|
||||
return 'reopened';
|
||||
} catch (e) {
|
||||
return 'reopen-failed';
|
||||
}
|
||||
} catch (e) {
|
||||
return 'error';
|
||||
}
|
||||
})()
|
||||
"""
|
||||
|
||||
private const val BRIDGE_NAME = "__mcNotifyBridge"
|
||||
private const val CHROME_BRIDGE_NAME = "__mcChromeBridge"
|
||||
private const val SHIM_ASSET = "notification_shim.js"
|
||||
|
||||
Reference in new issue
Block a user