mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
Merge pull request #3410 from vitorpamplona/claude/nsites-napplets-preload-iwhh1j
Add loading progress bar and error logging to napplet/browser
This commit is contained in:
+58
-1
@@ -24,6 +24,7 @@ import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
@@ -40,6 +41,7 @@ import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
import android.webkit.WebView
|
||||
import android.webkit.WebViewClient
|
||||
@@ -94,6 +96,10 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
private var controlSheet: NappletControlSheet? = null
|
||||
private var consolePanel: NappletConsolePanel? = null
|
||||
|
||||
// A thin determinate progress bar pinned to the top edge (browser-style), driven by the chrome
|
||||
// client's onProgressChanged; hidden at 100%.
|
||||
private val topProgressBar by lazy { buildTopProgressBar() }
|
||||
|
||||
// Visit-history gating: only a clean main-frame load (no error) is recorded, so a misspelled/
|
||||
// unresolved address never enters history. Reset on each main-frame page start.
|
||||
private var pendingMainFrameUrl: String? = null
|
||||
@@ -200,6 +206,8 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
Gravity.BOTTOM,
|
||||
),
|
||||
)
|
||||
// Added last so the thin loading bar paints above the content (and over the grabber's top edge).
|
||||
addView(topProgressBar)
|
||||
}
|
||||
setContentView(root)
|
||||
// Pad by the system bars + cutout, but NOT the IME — windowSoftInputMode=adjustResize shrinks the
|
||||
@@ -315,8 +323,15 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
wv.webChromeClient = BrowserChromeClient()
|
||||
}
|
||||
|
||||
/** Captures favicon and console output; the only source of both is the WebChromeClient. */
|
||||
/** Captures favicon and console output, and drives the top loading bar; all come from the WebChromeClient. */
|
||||
private inner class BrowserChromeClient : WebChromeClient() {
|
||||
override fun onProgressChanged(
|
||||
view: WebView,
|
||||
newProgress: Int,
|
||||
) {
|
||||
updateLoadProgress(newProgress)
|
||||
}
|
||||
|
||||
override fun onReceivedIcon(
|
||||
view: WebView,
|
||||
icon: Bitmap?,
|
||||
@@ -377,6 +392,15 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
// A main-frame failure (DNS miss on a misspelled host, no connection, …) disqualifies this
|
||||
// navigation from history. Sub-resource errors are irrelevant to whether the page opened.
|
||||
if (request.isForMainFrame) mainFrameLoadFailed = true
|
||||
logConsoleError(request, getString(R.string.napplet_console_load_error, error.errorCode, error.description?.toString().orEmpty()))
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse,
|
||||
) {
|
||||
logConsoleError(request, getString(R.string.napplet_console_http_error, errorResponse.statusCode, errorResponse.reasonPhrase.orEmpty()))
|
||||
}
|
||||
|
||||
override fun onPageCommitVisible(
|
||||
@@ -658,6 +682,39 @@ class NappletBrowserActivity : ComponentActivity() {
|
||||
addView(ProgressBar(this@NappletBrowserActivity))
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin determinate progress bar pinned to the top edge, like a browser's. Driven by
|
||||
* [BrowserChromeClient.onProgressChanged]: visible while the page loads and gone at 100%.
|
||||
*/
|
||||
private fun buildTopProgressBar(): ProgressBar =
|
||||
ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
|
||||
max = 100
|
||||
isIndeterminate = false
|
||||
visibility = View.GONE
|
||||
progressTintList = ColorStateList.valueOf(resolveThemeColor(android.R.attr.colorPrimary))
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, dp(3), Gravity.TOP)
|
||||
}
|
||||
|
||||
/** Shows the thin top bar at [progress]% while loading, hiding it once the page is fully loaded. */
|
||||
private fun updateLoadProgress(progress: Int) {
|
||||
if (progress >= 100) {
|
||||
topProgressBar.visibility = View.GONE
|
||||
} else {
|
||||
topProgressBar.progress = progress
|
||||
topProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/** Appends a single ERROR line to the console panel and refreshes the chrome's unread count. */
|
||||
private fun logConsoleError(
|
||||
request: WebResourceRequest,
|
||||
message: String,
|
||||
) {
|
||||
val panel = consolePanel ?: return
|
||||
panel.appendLog(ConsoleMessage.MessageLevel.ERROR, message, request.url?.toString().orEmpty(), 0)
|
||||
controlSheet?.updateConsoleCount(panel.entryCount)
|
||||
}
|
||||
|
||||
private fun resolveThemeColor(attr: Int): Int {
|
||||
val tv = android.util.TypedValue()
|
||||
theme.resolveAttribute(attr, tv, true)
|
||||
|
||||
+126
-4
@@ -24,6 +24,7 @@ import android.app.AlertDialog
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.content.res.ColorStateList
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
@@ -37,6 +38,9 @@ import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.webkit.ConsoleMessage
|
||||
import android.webkit.WebChromeClient
|
||||
import android.webkit.WebResourceError
|
||||
import android.webkit.WebResourceRequest
|
||||
import android.webkit.WebResourceResponse
|
||||
import android.webkit.WebSettings
|
||||
@@ -147,6 +151,18 @@ class NappletHostActivity : ComponentActivity() {
|
||||
private val contentFrame by lazy { FrameLayout(this) }
|
||||
private val uiScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
||||
|
||||
// The loading splash (monogram + spinner). Kept on top of the mounted WebView and removed only on
|
||||
// first paint, so there's never a blank/dark gap between the index probe and the shell's first frame.
|
||||
private var loadingView: View? = null
|
||||
|
||||
// A thin determinate progress bar pinned to the top edge (browser-style), driven by the
|
||||
// WebChromeClient's onProgressChanged; hidden at 100%.
|
||||
private val topProgressBar by lazy { buildTopProgressBar() }
|
||||
|
||||
// Bottom pull-up developer console: the page's console.log/warn/error plus any resource load errors.
|
||||
private var consolePanel: NappletConsolePanel? = null
|
||||
private var controlSheet: NappletControlSheet? = null
|
||||
|
||||
// Set once the WebView has begun loading the shell, so a retry doesn't reload it.
|
||||
private var started = false
|
||||
|
||||
@@ -268,6 +284,18 @@ class NappletHostActivity : ComponentActivity() {
|
||||
Gravity.TOP,
|
||||
),
|
||||
)
|
||||
addView(
|
||||
buildConsolePanel(),
|
||||
FrameLayout
|
||||
.LayoutParams(
|
||||
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||
Gravity.BOTTOM,
|
||||
),
|
||||
)
|
||||
// Added last so the thin loading bar paints above the content (and over the grabber's top
|
||||
// edge); it's GONE except while loading, so it never obscures the trusted chrome.
|
||||
addView(topProgressBar)
|
||||
}
|
||||
setContentView(root)
|
||||
// Activities are edge-to-edge by default on recent Android; pad by the system bar and
|
||||
@@ -295,22 +323,26 @@ class NappletHostActivity : ComponentActivity() {
|
||||
*/
|
||||
private fun probeAndMount() {
|
||||
contentFrame.removeAllViews()
|
||||
contentFrame.addView(buildLoadingView())
|
||||
loadingView = buildLoadingView().also { contentFrame.addView(it) }
|
||||
uiScope.launch {
|
||||
val available = withContext(Dispatchers.IO) { contentServer.resolve("/") is StaticSiteResolution.Resolved }
|
||||
if (available) {
|
||||
mountWebView()
|
||||
} else {
|
||||
contentFrame.removeAllViews()
|
||||
loadingView = null
|
||||
contentFrame.addView(buildErrorView { probeAndMount() })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun mountWebView() {
|
||||
contentFrame.removeAllViews()
|
||||
(webView.parent as? ViewGroup)?.removeView(webView)
|
||||
contentFrame.addView(webView, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT))
|
||||
// Mount the WebView UNDER the loading splash (index 0) instead of replacing it: the shell + applet
|
||||
// bundle still take time to paint (seconds over Tor), and the WebView shows only its dark
|
||||
// colorBackground until then. The splash stays until the first frame paints (onPageCommitVisible),
|
||||
// so the user never sees a blank/black screen with no sign that anything is loading.
|
||||
contentFrame.addView(webView, 0, FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT))
|
||||
if (!started) {
|
||||
started = true
|
||||
webView.loadUrl(NappletWebContract.SHELL_URL)
|
||||
@@ -484,6 +516,7 @@ class NappletHostActivity : ComponentActivity() {
|
||||
webView.overScrollMode = View.OVER_SCROLL_NEVER
|
||||
WebView.setWebContentsDebuggingEnabled(false)
|
||||
webView.webViewClient = NappletWebViewClient()
|
||||
webView.webChromeClient = NappletWebChromeClient()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -529,6 +562,34 @@ class NappletHostActivity : ComponentActivity() {
|
||||
syncBackState()
|
||||
}
|
||||
|
||||
// The shell has painted its first frame — drop the loading splash so the running app shows
|
||||
// through. Null-safe so a later in-app navigation/reload (splash already gone) is a no-op.
|
||||
override fun onPageCommitVisible(
|
||||
view: WebView,
|
||||
url: String,
|
||||
) {
|
||||
loadingView?.let { contentFrame.removeView(it) }
|
||||
loadingView = null
|
||||
}
|
||||
|
||||
// Surface failed resource fetches (a missing blob, a verify miss, an off-origin request the
|
||||
// default-deny CSP blocked) in the console so an nsite/napplet developer can see what broke.
|
||||
override fun onReceivedError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
error: WebResourceError,
|
||||
) {
|
||||
logConsoleError(request, getString(R.string.napplet_console_load_error, error.errorCode, error.description?.toString().orEmpty()))
|
||||
}
|
||||
|
||||
override fun onReceivedHttpError(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
errorResponse: WebResourceResponse,
|
||||
) {
|
||||
logConsoleError(request, getString(R.string.napplet_console_http_error, errorResponse.statusCode, errorResponse.reasonPhrase.orEmpty()))
|
||||
}
|
||||
|
||||
override fun shouldOverrideUrlLoading(
|
||||
view: WebView,
|
||||
request: WebResourceRequest,
|
||||
@@ -550,6 +611,43 @@ class NappletHostActivity : ComponentActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/** Drives the top loading bar and forwards the applet/site's `console.*` output to the console panel. */
|
||||
private inner class NappletWebChromeClient : WebChromeClient() {
|
||||
override fun onProgressChanged(
|
||||
view: WebView,
|
||||
newProgress: Int,
|
||||
) {
|
||||
updateLoadProgress(newProgress)
|
||||
}
|
||||
|
||||
override fun onConsoleMessage(consoleMessage: ConsoleMessage): Boolean {
|
||||
val panel = consolePanel ?: return false
|
||||
panel.appendLog(consoleMessage.messageLevel(), consoleMessage.message(), consoleMessage.sourceId(), consoleMessage.lineNumber())
|
||||
controlSheet?.updateConsoleCount(panel.entryCount)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the thin top bar at [progress]% while loading, hiding it once the page is fully loaded. */
|
||||
private fun updateLoadProgress(progress: Int) {
|
||||
if (progress >= 100) {
|
||||
topProgressBar.visibility = View.GONE
|
||||
} else {
|
||||
topProgressBar.progress = progress
|
||||
topProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
|
||||
/** Appends a single ERROR line to the console panel and refreshes the chrome's unread count. */
|
||||
private fun logConsoleError(
|
||||
request: WebResourceRequest,
|
||||
message: String,
|
||||
) {
|
||||
val panel = consolePanel ?: return
|
||||
panel.appendLog(ConsoleMessage.MessageLevel.ERROR, message, request.url?.toString().orEmpty(), 0)
|
||||
controlSheet?.updateConsoleCount(panel.entryCount)
|
||||
}
|
||||
|
||||
// ---- bridge: shell <-> native ----
|
||||
|
||||
private fun onShellMessage(
|
||||
@@ -663,6 +761,9 @@ class NappletHostActivity : ComponentActivity() {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
gravity = Gravity.CENTER
|
||||
setPadding(dp(32), dp(32), dp(32), dp(32))
|
||||
// Opaque so the splash/error screen fully covers the WebView it now overlays (mounted beneath
|
||||
// it until first paint) instead of letting the dark, not-yet-painted page show through.
|
||||
setBackgroundColor(resolveThemeColor(android.R.attr.colorBackground))
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
|
||||
}
|
||||
|
||||
@@ -741,7 +842,28 @@ class NappletHostActivity : ComponentActivity() {
|
||||
torInitiallyOn = if (profile.exposesNetwork && proxyPort > 0) useTor else null,
|
||||
onNetworkTap = if (profile.exposesNetwork && proxyPort > 0) ({ setNetworkMode(!useTor) }) else null,
|
||||
onInfo = { showAccessDialog() },
|
||||
)
|
||||
onConsole = { consolePanel?.toggle() },
|
||||
).also { controlSheet = it }
|
||||
|
||||
private fun buildConsolePanel(): View =
|
||||
NappletConsolePanel(this).also {
|
||||
it.onClearCallback = { controlSheet?.updateConsoleCount(0) }
|
||||
consolePanel = it
|
||||
}
|
||||
|
||||
/**
|
||||
* A thin determinate progress bar pinned to the top edge, like a browser's. Driven by
|
||||
* [NappletWebChromeClient.onProgressChanged]: visible while the shell + verified blobs load and gone
|
||||
* at 100%, so a slow load (e.g. a large bundle over Tor) shows progress instead of a blank dark WebView.
|
||||
*/
|
||||
private fun buildTopProgressBar(): ProgressBar =
|
||||
ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
|
||||
max = 100
|
||||
isIndeterminate = false
|
||||
visibility = View.GONE
|
||||
progressTintList = ColorStateList.valueOf(resolveThemeColor(android.R.attr.colorPrimary))
|
||||
layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, dp(3), Gravity.TOP)
|
||||
}
|
||||
|
||||
/** Persists the new routing choice in the main process, then relaunches this screen to apply it. */
|
||||
private fun setNetworkMode(newUseTor: Boolean) {
|
||||
|
||||
@@ -31,4 +31,8 @@
|
||||
<string name="napplet_unavailable_title">Couldn\'t load “%1$s”</string>
|
||||
<string name="napplet_unavailable_subtitle">The publisher\'s servers may be offline, or you\'re not connected. You can try again.</string>
|
||||
<string name="napplet_unavailable_retry">Try again</string>
|
||||
|
||||
<!-- Developer console: page-load failures surfaced as console errors -->
|
||||
<string name="napplet_console_load_error">Failed to load (%1$d): %2$s</string>
|
||||
<string name="napplet_console_http_error">HTTP %1$d %2$s</string>
|
||||
</resources>
|
||||
|
||||
Reference in New Issue
Block a user