Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -51,6 +59,7 @@ fun GuestBrowserScreen(
var canGoBack by remember { mutableStateOf(false) }
var canGoForward by remember { mutableStateOf(false) }
var webView by remember { mutableStateOf<WebView?>(null) }
var webViewUnavailable by remember { mutableStateOf(false) }

BackHandler {
val view = webView
Expand All @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T : Any> viewOrNull(
create: () -> T,
onFailure: (Throwable) -> Unit = {},
): T? =
try {
create()
} catch (error: Exception) {
onFailure(error)
null
} catch (error: LinkageError) {
onFailure(error)
null
}
1 change: 1 addition & 0 deletions app/src/main/res/values-ar/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1219,6 +1219,7 @@
<string name="guest_browser_back">رجوع</string>
<string name="guest_browser_forward">تقدم</string>
<string name="guest_browser_reload">إعادة تحميل</string>
<string name="guest_browser_webview_unavailable">يتعذّر تشغيل المتصفح الضيف لأن WebView لم يُحمَّل على هذا الجهاز. قد يحل تحديث Android System WebView أو إعادة تفعيله المشكلة.</string>
<string name="setup_step_development_tools">أدوات التطوير</string>
<string name="setup_development_tools_description">يتم تثبيت مكونات التشغيل الضرورية فقط افتراضيًا، ويمكنك إضافة مجموعة أدوات التطوير الكاملة.</string>
<string name="setup_required_tools_description">سيتم دائمًا تثبيت Linux والوكلاء المحددين وGit وأدوات الدعم التي يستخدمها AndCode.</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@
<string name="guest_browser_back">Atrás</string>
<string name="guest_browser_forward">Avanzar</string>
<string name="guest_browser_reload">Recargar</string>
<string name="guest_browser_webview_unavailable">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.</string>
<string name="setup_step_development_tools">Herramientas de desarrollo</string>
<string name="setup_development_tools_description">De forma predeterminada solo se instala lo necesario para ejecutar el entorno, o puedes incluir toda la cadena de desarrollo.</string>
<string name="setup_required_tools_description">Linux, los agentes seleccionados, Git y las herramientas de soporte que usa AndCode. Siempre se instalan.</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-fr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@
<string name="guest_browser_back">Retour</string>
<string name="guest_browser_forward">Avancer</string>
<string name="guest_browser_reload">Recharger</string>
<string name="guest_browser_webview_unavailable">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.</string>
<string name="setup_step_development_tools">Outils de développement</string>
<string name="setup_development_tools_description">Installe uniquement le nécessaire par défaut, ou incluez la chaîne complète d’outils.</string>
<string name="setup_required_tools_description">Linux, les agents sélectionnés, Git et les outils requis par AndCode. Ils sont toujours installés.</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-ja/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1224,6 +1224,7 @@
<string name="guest_browser_back">戻る</string>
<string name="guest_browser_forward">進む</string>
<string name="guest_browser_reload">再読み込み</string>
<string name="guest_browser_webview_unavailable">この端末の WebView を読み込めないため、ゲストブラウザを開けません。Android System WebView を更新するか、有効にし直すと解決する場合があります。</string>
<string name="setup_step_development_tools">開発ツール</string>
<string name="setup_development_tools_description">既定では実行に必要なものだけをインストールします。完全な開発ツールチェーンも選択できます。</string>
<string name="setup_required_tools_description">Linux、選択したエージェント、Git、および AndCode が使用するサポートツールです。これらは常にインストールされます。</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-pt-rBR/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1210,6 +1210,7 @@
<string name="guest_browser_back">Voltar</string>
<string name="guest_browser_forward">Avançar</string>
<string name="guest_browser_reload">Recarregar</string>
<string name="guest_browser_webview_unavailable">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.</string>
<string name="setup_step_development_tools">Ferramentas de desenvolvimento</string>
<string name="setup_development_tools_description">Por padrão, instale apenas o necessário para executar, ou inclua a cadeia completa de desenvolvimento.</string>
<string name="setup_required_tools_description">Linux, os agentes selecionados, Git e as ferramentas de suporte usadas pelo AndCode. São sempre instalados.</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-ru/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1213,6 +1213,7 @@
<string name="guest_browser_back">Назад</string>
<string name="guest_browser_forward">Вперёд</string>
<string name="guest_browser_reload">Обновить</string>
<string name="guest_browser_webview_unavailable">Гостевой браузер не запускается: на этом устройстве не удалось загрузить WebView. Обновление или повторное включение Android System WebView может помочь.</string>
<string name="setup_step_development_tools">Инструменты разработки</string>
<string name="setup_development_tools_description">По умолчанию устанавливаются только необходимые компоненты. При желании можно добавить полный набор инструментов.</string>
<string name="setup_required_tools_description">Linux, выбранные агенты, Git и вспомогательные инструменты AndCode. Они устанавливаются всегда.</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1204,6 +1204,7 @@
<string name="guest_browser_back">后退</string>
<string name="guest_browser_forward">前进</string>
<string name="guest_browser_reload">重新加载</string>
<string name="guest_browser_webview_unavailable">无法启动访客浏览器,因为此设备上的 WebView 加载失败。更新或重新启用 Android System WebView 可能会解决此问题。</string>
<string name="setup_step_development_tools">开发工具</string>
<string name="setup_development_tools_description">默认只安装运行所需组件,也可以一并安装完整开发工具链。</string>
<string name="setup_required_tools_description">Linux、所选智能体、Git 以及 AndCode 功能依赖的支持工具,将始终安装。</string>
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@
<string name="guest_browser_back">Back</string>
<string name="guest_browser_forward">Forward</string>
<string name="guest_browser_reload">Reload</string>
<string name="guest_browser_webview_unavailable">The Guest Browser cannot start because the WebView on this device failed to load. Updating or re-enabling Android System WebView may fix this.</string>
<string name="setup_step_development_tools">Development tools</string>
<string name="setup_development_tools_description">Install only the runtime by default, or include the complete development toolchain.</string>
<string name="setup_required_tools_description">Linux, the selected agents, Git, and the support tools used by AndCode. These are always installed.</string>
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Throwable>()

val view = viewOrNull<Any>(create = { throw refusal }, onFailure = { reported += it })

assertNull(view)
assertEquals(listOf<Throwable>(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<Throwable>()

val view =
viewOrNull<Any>(
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<Any>(create = { throw OutOfMemoryError() })
}
}
}
Loading