diff --git a/app/src/main/java/com/yugahashimoto/andcode/feature/browser/GuestBrowserScreen.kt b/app/src/main/java/com/yugahashimoto/andcode/feature/browser/GuestBrowserScreen.kt index e40c049c5..558031d70 100644 --- a/app/src/main/java/com/yugahashimoto/andcode/feature/browser/GuestBrowserScreen.kt +++ b/app/src/main/java/com/yugahashimoto/andcode/feature/browser/GuestBrowserScreen.kt @@ -1,11 +1,14 @@ package com.yugahashimoto.andcode.feature.browser import android.annotation.SuppressLint +import android.util.Log +import android.view.View import android.view.ViewGroup import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.compose.BackHandler +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize @@ -20,6 +23,7 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Scaffold import androidx.compose.material3.Text @@ -31,14 +35,18 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.yugahashimoto.andcode.BuildConfig import com.yugahashimoto.andcode.R +private const val TAG = "GuestBrowser" + /** In-app browser for pages served inside the guest runtime (e.g. http://127.0.0.1:PORT/). */ @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -51,6 +59,7 @@ fun GuestBrowserScreen( var canGoBack by remember { mutableStateOf(false) } var canGoForward by remember { mutableStateOf(false) } var webView by remember { mutableStateOf(null) } + var webViewUnavailable by remember { mutableStateOf(false) } BackHandler { val view = webView @@ -74,48 +83,82 @@ fun GuestBrowserScreen( }, ) { padding -> Column(modifier = Modifier.padding(padding).fillMaxSize()) { - GuestBrowserUrlBar( - urlInput = urlInput, - onUrlChange = { urlInput = it }, - onGo = { webView?.loadUrl(normalizeUrl(urlInput)) }, - ) + if (!webViewUnavailable) { + GuestBrowserUrlBar( + urlInput = urlInput, + onUrlChange = { urlInput = it }, + onGo = { webView?.loadUrl(normalizeUrl(urlInput)) }, + ) + } if (progress in 1..99) { LinearProgressIndicator(progress = { progress / 100f }) } - AndroidView( - // WebView can draw its background outside its measured bounds, covering the URL - // field even while that field still accepts focus and keyboard input. - modifier = Modifier.weight(1f).clipToBounds(), - factory = { context -> - // Lets the in-guest agent drive this WebView over CDP (the devtools abstract - // socket is reachable from the guest, which shares the app's UID), so a page - // can be shown to the user and operated by human and agent at the same time. - WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) - WebView(context).apply { - layoutParams = - ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT, + if (webViewUnavailable) { + WebViewUnavailableNotice(modifier = Modifier.weight(1f).fillMaxWidth()) + } else { + AndroidView( + // WebView can draw its background outside its measured bounds, covering the URL + // field even while that field still accepts focus and keyboard input. + modifier = Modifier.weight(1f).clipToBounds(), + factory = { context -> + // Only starting the system WebView provider can fail for reasons the app cannot + // fix; everything after it runs on a WebView that exists, so a failure there is + // a real bug and must not leave a half-built view behind. + val created = + viewOrNull( + create = { + // Lets the in-guest agent drive this WebView over CDP (the devtools abstract + // socket is reachable from the guest, which shares the app's UID), so a page + // can be shown to the user and operated by human and agent at the same time. + WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG) + WebView(context) + }, + onFailure = { error -> Log.w(TAG, "The system WebView could not start", error) }, ) - configure( - onProgress = { progress = it }, - onNavigationStateChange = { url, back, forward -> - urlInput = url - canGoBack = back - canGoForward = forward - }, - ) - webView = this - if (initialUrl.isNotBlank()) { - loadUrl(normalizeUrl(initialUrl)) + if (created == null) { + webViewUnavailable = true + View(context) + } else { + created.apply { + layoutParams = + ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT, + ) + configure( + onProgress = { progress = it }, + onNavigationStateChange = { url, back, forward -> + urlInput = url + canGoBack = back + canGoForward = forward + }, + ) + webView = this + if (initialUrl.isNotBlank()) { + loadUrl(normalizeUrl(initialUrl)) + } + } } - } - }, - ) + }, + ) + } } } } +/** Shown instead of the page when the system WebView cannot start, which the app cannot repair. */ +@Composable +private fun WebViewUnavailableNotice(modifier: Modifier = Modifier) { + Box(modifier = modifier.padding(24.dp), contentAlignment = Alignment.Center) { + Text( + text = stringResource(R.string.guest_browser_webview_unavailable), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + ) + } +} + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun GuestBrowserTopBar( diff --git a/app/src/main/java/com/yugahashimoto/andcode/feature/browser/PlatformView.kt b/app/src/main/java/com/yugahashimoto/andcode/feature/browser/PlatformView.kt new file mode 100644 index 000000000..3b3887dc6 --- /dev/null +++ b/app/src/main/java/com/yugahashimoto/andcode/feature/browser/PlatformView.kt @@ -0,0 +1,28 @@ +package com.yugahashimoto.andcode.feature.browser + +/** + * Builds a view the platform may refuse to build, returning null instead of throwing. + * + * WebView needs the system's WebView provider, and starting it can fail on a given device: on one + * vivo phone the provider's resource registration threw `Resources.NotFoundException`, which + * surfaced as an `AndroidRuntimeException` inside the Compose view factory and took the whole app + * down. A provider whose classes or native libraries cannot be loaded fails the same way as a + * [LinkageError]. Nothing the app does can repair either, so the caller shows an explanation instead. + * + * Only [Exception] and [LinkageError] are caught. Any other [Error], an [OutOfMemoryError] above + * all, is not a missing provider, and hiding it would leave the screen blaming WebView for a memory + * problem. + */ +internal fun viewOrNull( + create: () -> T, + onFailure: (Throwable) -> Unit = {}, +): T? = + try { + create() + } catch (error: Exception) { + onFailure(error) + null + } catch (error: LinkageError) { + onFailure(error) + null + } diff --git a/app/src/main/res/values-ar/strings.xml b/app/src/main/res/values-ar/strings.xml index 38689c6f0..790fa2ab9 100644 --- a/app/src/main/res/values-ar/strings.xml +++ b/app/src/main/res/values-ar/strings.xml @@ -1219,6 +1219,7 @@ رجوع تقدم إعادة تحميل + يتعذّر تشغيل المتصفح الضيف لأن WebView لم يُحمَّل على هذا الجهاز. قد يحل تحديث Android System WebView أو إعادة تفعيله المشكلة. أدوات التطوير يتم تثبيت مكونات التشغيل الضرورية فقط افتراضيًا، ويمكنك إضافة مجموعة أدوات التطوير الكاملة. سيتم دائمًا تثبيت Linux والوكلاء المحددين وGit وأدوات الدعم التي يستخدمها AndCode. diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 427d1f990..b7ef33fd2 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -1210,6 +1210,7 @@ Atrás Avanzar Recargar + El navegador invitado no puede iniciarse porque WebView no se pudo cargar en este dispositivo. Actualizar o volver a activar Android System WebView puede solucionarlo. Herramientas de desarrollo De forma predeterminada solo se instala lo necesario para ejecutar el entorno, o puedes incluir toda la cadena de desarrollo. Linux, los agentes seleccionados, Git y las herramientas de soporte que usa AndCode. Siempre se instalan. diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index cbd9bac45..f81ba3159 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1210,6 +1210,7 @@ Retour Avancer Recharger + Le navigateur invité ne peut pas démarrer, car WebView n\'a pas pu se charger sur cet appareil. Mettre à jour ou réactiver Android System WebView peut résoudre le problème. Outils de développement Installe uniquement le nécessaire par défaut, ou incluez la chaîne complète d’outils. Linux, les agents sélectionnés, Git et les outils requis par AndCode. Ils sont toujours installés. diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index 8931d058c..4cbc51e90 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -1224,6 +1224,7 @@ 戻る 進む 再読み込み + この端末の WebView を読み込めないため、ゲストブラウザを開けません。Android System WebView を更新するか、有効にし直すと解決する場合があります。 開発ツール 既定では実行に必要なものだけをインストールします。完全な開発ツールチェーンも選択できます。 Linux、選択したエージェント、Git、および AndCode が使用するサポートツールです。これらは常にインストールされます。 diff --git a/app/src/main/res/values-pt-rBR/strings.xml b/app/src/main/res/values-pt-rBR/strings.xml index a5b402fee..d8711a238 100644 --- a/app/src/main/res/values-pt-rBR/strings.xml +++ b/app/src/main/res/values-pt-rBR/strings.xml @@ -1210,6 +1210,7 @@ Voltar Avançar Recarregar + O navegador convidado não pode ser iniciado porque o WebView não foi carregado neste dispositivo. Atualizar ou reativar o Android System WebView pode resolver o problema. Ferramentas de desenvolvimento Por padrão, instale apenas o necessário para executar, ou inclua a cadeia completa de desenvolvimento. Linux, os agentes selecionados, Git e as ferramentas de suporte usadas pelo AndCode. São sempre instalados. diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index d0e70387d..bf6e8cfc5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -1213,6 +1213,7 @@ Назад Вперёд Обновить + Гостевой браузер не запускается: на этом устройстве не удалось загрузить WebView. Обновление или повторное включение Android System WebView может помочь. Инструменты разработки По умолчанию устанавливаются только необходимые компоненты. При желании можно добавить полный набор инструментов. Linux, выбранные агенты, Git и вспомогательные инструменты AndCode. Они устанавливаются всегда. diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 229c827bb..853b2b229 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -1204,6 +1204,7 @@ 后退 前进 重新加载 + 无法启动访客浏览器,因为此设备上的 WebView 加载失败。更新或重新启用 Android System WebView 可能会解决此问题。 开发工具 默认只安装运行所需组件,也可以一并安装完整开发工具链。 Linux、所选智能体、Git 以及 AndCode 功能依赖的支持工具,将始终安装。 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 46d822f6b..f649d394f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1229,6 +1229,7 @@ Back Forward Reload + The Guest Browser cannot start because the WebView on this device failed to load. Updating or re-enabling Android System WebView may fix this. Development tools Install only the runtime by default, or include the complete development toolchain. Linux, the selected agents, Git, and the support tools used by AndCode. These are always installed. diff --git a/app/src/test/java/com/yugahashimoto/andcode/feature/browser/PlatformViewTest.kt b/app/src/test/java/com/yugahashimoto/andcode/feature/browser/PlatformViewTest.kt new file mode 100644 index 000000000..3541a4c58 --- /dev/null +++ b/app/src/test/java/com/yugahashimoto/andcode/feature/browser/PlatformViewTest.kt @@ -0,0 +1,51 @@ +package com.yugahashimoto.andcode.feature.browser + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertSame +import org.junit.Assert.assertThrows +import org.junit.Test + +class PlatformViewTest { + @Test + fun `returns the view when the platform builds it`() { + val view = Any() + + assertSame(view, viewOrNull(create = { view })) + } + + @Test + fun `returns null and reports the cause when the platform refuses to build the view`() { + // The WebView provider's resource registration failed on a vivo device and surfaced as a + // RuntimeException wrapping a Resources.NotFoundException. + val refusal = RuntimeException("failed to redirect ResourcesImpl", IllegalStateException("not found")) + val reported = mutableListOf() + + val view = viewOrNull(create = { throw refusal }, onFailure = { reported += it }) + + assertNull(view) + assertEquals(listOf(refusal), reported) + } + + @Test + fun `a provider whose native code fails to load is also a missing view`() { + // The provider's classes and libraries can fail to load, which arrives as a LinkageError. + val reported = mutableListOf() + + val view = + viewOrNull( + create = { throw UnsatisfiedLinkError("libwebviewchromium.so") }, + onFailure = { reported += it }, + ) + + assertNull(view) + assertEquals(1, reported.size) + } + + @Test + fun `does not hide an error that is not a missing view`() { + assertThrows(OutOfMemoryError::class.java) { + viewOrNull(create = { throw OutOfMemoryError() }) + } + } +}