Skip to content

Commit 2618ae6

Browse files
committed
fix: improve WebView stability and environment setup
- Fix WebView dropdown/dialog rendering by using Activity context. - Improve graphics environment initialization for better compatibility. - Clean up potential stray cache directories to prevent WebView startup errors.
1 parent ed48781 commit 2618ae6

3 files changed

Lines changed: 148 additions & 135 deletions

File tree

app/src/main/java/com/example/BrowserApplication.kt

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -7,30 +7,36 @@ import java.io.File
77
class BrowserApplication : Application() {
88

99
init {
10-
// In containerized and virtualized emulator environments without hardware DRI rendernodes (/dev/dri/renderD128),
11-
// instruct the Mesa graphics loader to use software rendering directly, avoiding "Failed to open rendernode" errors.
12-
try {
13-
val hasDri = File("/dev/dri").exists()
14-
if (!hasDri) {
15-
Os.setenv("LIBGL_ALWAYS_SOFTWARE", "1", true)
16-
}
17-
} catch (e: Throwable) { }
10+
configureGraphicsEnvironment()
11+
}
12+
13+
override fun attachBaseContext(base: android.content.Context?) {
14+
super.attachBaseContext(base)
15+
configureGraphicsEnvironment()
1816
}
1917

2018
override fun onCreate() {
2119
super.onCreate()
20+
configureGraphicsEnvironment()
2221

23-
// Ensure Chromium cache directories exist so simple_file_enumerator won't fail with ENOENT
22+
// Clean up any stray directories accidentally placed inside HTTP Cache from prior sessions
23+
// so Chromium's SimpleCache index reconstruction will succeed without errors.
2424
try {
25-
val cache = cacheDir
26-
val jsCodeCache = File(cache, "WebView/Default/HTTP Cache/Code Cache/js")
27-
val wasmCodeCache = File(cache, "WebView/Default/HTTP Cache/Code Cache/wasm")
28-
if (!jsCodeCache.exists()) {
29-
jsCodeCache.mkdirs()
30-
}
31-
if (!wasmCodeCache.exists()) {
32-
wasmCodeCache.mkdirs()
25+
val strayCodeCache = File(cacheDir, "WebView/Default/HTTP Cache/Code Cache")
26+
if (strayCodeCache.exists()) {
27+
strayCodeCache.deleteRecursively()
3328
}
3429
} catch (e: Throwable) { }
3530
}
31+
32+
private fun configureGraphicsEnvironment() {
33+
// In containerized and virtualized emulator environments without hardware DRI rendernodes (/dev/dri/renderD128),
34+
// silence Mesa debug logging and instruct the loader to use software rasterization.
35+
try {
36+
Os.setenv("MESA_DEBUG", "silent", true)
37+
Os.setenv("MESA_LOG_FILE", "/dev/null", true)
38+
Os.setenv("LIBGL_ALWAYS_SOFTWARE", "1", true)
39+
Os.setenv("GALLIUM_DRIVER", "llvmpipe", true)
40+
} catch (e: Throwable) { }
41+
}
3642
}

app/src/main/java/com/example/ui/components/TabsManagerSheet.kt

Lines changed: 113 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -402,26 +402,6 @@ fun DownsideTabCard(
402402
.fillMaxHeight()
403403
.offset { IntOffset(0, animatedOffsetY.roundToInt()) }
404404
.graphicsLayer { alpha = cardAlpha }
405-
.pointerInput(tab.id) {
406-
detectVerticalDragGestures(
407-
onDragEnd = {
408-
if (abs(offsetY) > 160f) {
409-
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
410-
onClose()
411-
} else {
412-
offsetY = 0f
413-
}
414-
},
415-
onDragCancel = {
416-
offsetY = 0f
417-
},
418-
onVerticalDrag = { change, dragAmount ->
419-
change.consume()
420-
offsetY += dragAmount
421-
}
422-
)
423-
}
424-
.clickable(onClick = onClick)
425405
.testTag("tab_card_${tab.id}")
426406
) {
427407
Column(
@@ -451,90 +431,113 @@ fun DownsideTabCard(
451431
.fillMaxWidth()
452432
.padding(horizontal = 4.dp, vertical = 2.dp)
453433
) {
454-
// Site Icon Badge
455-
Surface(
456-
shape = CircleShape,
457-
color = when {
458-
isYouTube -> Color(0xFFFF0000).copy(alpha = 0.15f)
459-
isHome -> profileColor.copy(alpha = 0.15f)
460-
else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
461-
},
462-
modifier = Modifier.size(24.dp)
434+
// Tapping badge or title selects this tab
435+
Row(
436+
verticalAlignment = Alignment.CenterVertically,
437+
modifier = Modifier
438+
.weight(1f)
439+
.clip(RoundedCornerShape(8.dp))
440+
.clickable(onClick = onClick)
441+
.padding(vertical = 4.dp, horizontal = 2.dp)
463442
) {
464-
Box(contentAlignment = Alignment.Center) {
465-
when {
466-
isYouTube -> {
467-
Icon(
468-
imageVector = Icons.Filled.PlayArrow,
469-
contentDescription = null,
470-
tint = Color(0xFFFF0000),
471-
modifier = Modifier.size(15.dp)
472-
)
473-
}
474-
isHome -> {
475-
Icon(
476-
imageVector = Icons.Default.Home,
477-
contentDescription = null,
478-
tint = profileColor,
479-
modifier = Modifier.size(14.dp)
480-
)
481-
}
482-
tab.isPrivate -> {
483-
Icon(
484-
imageVector = Icons.Default.VpnKey,
485-
contentDescription = null,
486-
tint = Color(0xFF9333EA),
487-
modifier = Modifier.size(13.dp)
488-
)
489-
}
490-
else -> {
491-
Text(
492-
text = domain.firstOrNull()?.uppercase() ?: "W",
493-
fontSize = 12.sp,
494-
fontWeight = FontWeight.Bold,
495-
color = MaterialTheme.colorScheme.primary
496-
)
443+
// Site Icon Badge
444+
Surface(
445+
shape = CircleShape,
446+
color = when {
447+
isYouTube -> Color(0xFFFF0000).copy(alpha = 0.15f)
448+
isHome -> profileColor.copy(alpha = 0.15f)
449+
else -> MaterialTheme.colorScheme.primary.copy(alpha = 0.15f)
450+
},
451+
modifier = Modifier.size(24.dp)
452+
) {
453+
Box(contentAlignment = Alignment.Center) {
454+
when {
455+
isYouTube -> {
456+
Icon(
457+
imageVector = Icons.Filled.PlayArrow,
458+
contentDescription = null,
459+
tint = Color(0xFFFF0000),
460+
modifier = Modifier.size(15.dp)
461+
)
462+
}
463+
isHome -> {
464+
Icon(
465+
imageVector = Icons.Default.Home,
466+
contentDescription = null,
467+
tint = profileColor,
468+
modifier = Modifier.size(14.dp)
469+
)
470+
}
471+
tab.isPrivate -> {
472+
Icon(
473+
imageVector = Icons.Default.VpnKey,
474+
contentDescription = null,
475+
tint = Color(0xFF9333EA),
476+
modifier = Modifier.size(13.dp)
477+
)
478+
}
479+
else -> {
480+
Text(
481+
text = domain.firstOrNull()?.uppercase() ?: "W",
482+
fontSize = 12.sp,
483+
fontWeight = FontWeight.Bold,
484+
color = MaterialTheme.colorScheme.primary
485+
)
486+
}
497487
}
498488
}
499489
}
490+
491+
Spacer(modifier = Modifier.width(8.dp))
492+
493+
// Title
494+
Text(
495+
text = when {
496+
isHome -> "Home"
497+
tab.title.isNotBlank() -> tab.title
498+
else -> domain
499+
},
500+
fontSize = 13.sp,
501+
fontWeight = if (isActive) FontWeight.Bold else FontWeight.SemiBold,
502+
color = MaterialTheme.colorScheme.onSurface,
503+
maxLines = 1,
504+
overflow = TextOverflow.Ellipsis,
505+
modifier = Modifier.weight(1f)
506+
)
500507
}
501508

502-
Spacer(modifier = Modifier.width(8.dp))
503-
504-
// Title
505-
Text(
506-
text = when {
507-
isHome -> "Home"
508-
tab.title.isNotBlank() -> tab.title
509-
else -> domain
510-
},
511-
fontSize = 13.sp,
512-
fontWeight = if (isActive) FontWeight.Bold else FontWeight.SemiBold,
513-
color = MaterialTheme.colorScheme.onSurface,
514-
maxLines = 1,
515-
overflow = TextOverflow.Ellipsis,
516-
modifier = Modifier.weight(1f)
517-
)
509+
Spacer(modifier = Modifier.width(4.dp))
518510

519-
// Close tab button
520-
IconButton(
521-
onClick = onClose,
511+
// Close tab button with dedicated 36dp touch target and clear hit boundary
512+
Surface(
513+
shape = CircleShape,
514+
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f),
522515
modifier = Modifier
523-
.size(24.dp)
516+
.size(36.dp)
517+
.clip(CircleShape)
518+
.clickable(
519+
role = androidx.compose.ui.semantics.Role.Button,
520+
onClick = {
521+
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
522+
onClose()
523+
}
524+
)
524525
.testTag("close_tab_${tab.id}")
525526
) {
526-
Icon(
527-
imageVector = Icons.Default.Close,
528-
contentDescription = "Close Tab",
529-
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.8f),
530-
modifier = Modifier.size(16.dp)
531-
)
527+
Box(contentAlignment = Alignment.Center) {
528+
Icon(
529+
imageVector = Icons.Default.Close,
530+
contentDescription = "Close Tab",
531+
tint = MaterialTheme.colorScheme.onSurfaceVariant,
532+
modifier = Modifier.size(18.dp)
533+
)
534+
}
532535
}
533536
}
534537

535538
Spacer(modifier = Modifier.height(8.dp))
536539

537-
// Body: High quality preview surface
540+
// Body: High quality preview surface (tapping opens tab, swiping vertically dismisses tab)
538541
Surface(
539542
shape = RoundedCornerShape(16.dp),
540543
color = MaterialTheme.colorScheme.surface,
@@ -545,6 +548,27 @@ fun DownsideTabCard(
545548
modifier = Modifier
546549
.fillMaxWidth()
547550
.weight(1f)
551+
.clip(RoundedCornerShape(16.dp))
552+
.pointerInput(tab.id) {
553+
detectVerticalDragGestures(
554+
onDragEnd = {
555+
if (abs(offsetY) > 160f) {
556+
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
557+
onClose()
558+
} else {
559+
offsetY = 0f
560+
}
561+
},
562+
onDragCancel = {
563+
offsetY = 0f
564+
},
565+
onVerticalDrag = { change, dragAmount ->
566+
change.consume()
567+
offsetY += dragAmount
568+
}
569+
)
570+
}
571+
.clickable(onClick = onClick)
548572
) {
549573
if (isHome) {
550574
// Mini Home Page representation

app/src/main/java/com/example/ui/components/WebViewContainer.kt

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
package com.example.ui.components
22

3+
import android.app.Activity
4+
import android.content.ContextWrapper
35
import android.view.ContextThemeWrapper
46
import android.annotation.SuppressLint
57
import android.content.res.Configuration
@@ -349,14 +351,10 @@ fun WebViewContainer(
349351
setProgressBackgroundColorSchemeColor(progressBgColor)
350352
}
351353

352-
val overrideConfig = Configuration(ctx.resources.configuration)
353-
overrideConfig.uiMode = if (effectiveDark) {
354-
(overrideConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or Configuration.UI_MODE_NIGHT_YES
355-
} else {
356-
(overrideConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK.inv()) or Configuration.UI_MODE_NIGHT_NO
357-
}
354+
// Wrap the Activity context directly so WebView retains a valid WindowManager token for HTML <select> dropdowns and dialogs
355+
val activity = ctx.findActivity() ?: ctx
358356
val themedContext = ContextThemeWrapper(
359-
ctx.createConfigurationContext(overrideConfig),
357+
activity,
360358
if (effectiveDark) android.R.style.Theme_DeviceDefault else android.R.style.Theme_DeviceDefault_Light
361359
)
362360

@@ -378,13 +376,6 @@ fun WebViewContainer(
378376
val initialBgColor = if (effectiveDark) android.graphics.Color.parseColor("#121212") else android.graphics.Color.WHITE
379377
setBackgroundColor(initialBgColor)
380378

381-
// Use software layer only as a recovery fallback if a render process crash occurred
382-
if (renderCrashCount > 0) {
383-
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
384-
} else {
385-
setLayerType(View.LAYER_TYPE_NONE, null)
386-
}
387-
388379
// High refresh rate (90Hz/120Hz) nested scrolling optimization
389380
isNestedScrollingEnabled = true
390381
overScrollMode = View.OVER_SCROLL_IF_CONTENT_SCROLLS
@@ -885,16 +876,6 @@ fun WebViewContainer(
885876
webView.allowBackgroundPlayback = enableBackgroundPlay
886877
}
887878

888-
if (renderCrashCount > 0) {
889-
if (webView.layerType != View.LAYER_TYPE_SOFTWARE) {
890-
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null)
891-
}
892-
} else {
893-
if (webView.layerType != View.LAYER_TYPE_NONE) {
894-
webView.setLayerType(View.LAYER_TYPE_NONE, null)
895-
}
896-
}
897-
898879
// Pull-to-refresh enabled unless custom video view is active, and only for active tab
899880
swipeRefresh.isEnabled = (customVideoView == null) && isActive
900881

@@ -935,11 +916,7 @@ fun WebViewContainer(
935916
if (customVideoView != null) {
936917
AndroidView(
937918
factory = {
938-
customVideoView!!.apply {
939-
if (renderCrashCount > 0) {
940-
setLayerType(View.LAYER_TYPE_SOFTWARE, null)
941-
}
942-
}
919+
customVideoView!!
943920
},
944921
modifier = Modifier
945922
.fillMaxSize()
@@ -949,3 +926,9 @@ fun WebViewContainer(
949926
}
950927
}
951928
}
929+
930+
private tailrec fun Context.findActivity(): Activity? = when (this) {
931+
is Activity -> this
932+
is ContextWrapper -> baseContext.findActivity()
933+
else -> null
934+
}

0 commit comments

Comments
 (0)