Skip to content

Commit 5cbd618

Browse files
committed
fix: implement conditional hardware rendering
Disable hardware acceleration by default in the manifest and apply it dynamically only to supported devices via DeviceUtils to prevent rendering crashes.
1 parent fc31073 commit 5cbd618

8 files changed

Lines changed: 292 additions & 45 deletions

File tree

app/src/main/AndroidManifest.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@
99
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
1010

1111
<application
12+
android:name=".BrowserApplication"
1213
android:allowBackup="true"
13-
android:hardwareAccelerated="true"
14+
android:hardwareAccelerated="false"
1415
android:usesCleartextTraffic="true"
1516
android:dataExtractionRules="@xml/data_extraction_rules"
1617
android:fullBackupContent="@xml/backup_rules"
@@ -22,7 +23,7 @@
2223
<activity
2324
android:name=".MainActivity"
2425
android:exported="true"
25-
android:hardwareAccelerated="true"
26+
android:hardwareAccelerated="false"
2627
android:label="@string/app_name"
2728
android:theme="@style/Theme.MyApplication"
2829
android:windowSoftInputMode="adjustResize"
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.example
2+
3+
import android.app.Application
4+
import android.system.Os
5+
import java.io.File
6+
7+
class BrowserApplication : Application() {
8+
companion object {
9+
init {
10+
try {
11+
Os.setenv("MESA_LOG_LEVEL", "none", true)
12+
Os.setenv("MESA_LOG_FILE", "/dev/null", true)
13+
Os.setenv("LIBGL_DRI3_DISABLE", "1", true)
14+
} catch (e: Throwable) { }
15+
}
16+
}
17+
18+
override fun onCreate() {
19+
super.onCreate()
20+
try {
21+
Os.setenv("MESA_LOG_LEVEL", "none", true)
22+
Os.setenv("MESA_LOG_FILE", "/dev/null", true)
23+
Os.setenv("LIBGL_DRI3_DISABLE", "1", true)
24+
} catch (e: Throwable) { }
25+
26+
// Clean up any corrupted Chromium HTTP Cache directory from previous runs
27+
try {
28+
val cache = cacheDir
29+
val httpCache = File(cache, "WebView/Default/HTTP Cache")
30+
if (httpCache.exists()) {
31+
val corruptCodeCache = File(httpCache, "Code Cache")
32+
if (corruptCodeCache.exists()) {
33+
httpCache.deleteRecursively()
34+
}
35+
}
36+
} catch (e: Throwable) { }
37+
}
38+
}

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

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ class MainActivity : ComponentActivity() {
3434

3535
override fun onCreate(savedInstanceState: Bundle?) {
3636
super.onCreate(savedInstanceState)
37-
initializeWebViewDirectories()
37+
if (!com.example.browser.DeviceUtils.needsSoftwareRendering) {
38+
window.setFlags(
39+
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED,
40+
android.view.WindowManager.LayoutParams.FLAG_HARDWARE_ACCELERATED
41+
)
42+
}
3843
enableEdgeToEdge()
3944
enableHighRefreshRate()
4045

@@ -67,21 +72,6 @@ class MainActivity : ComponentActivity() {
6772
}
6873
}
6974

70-
private fun initializeWebViewDirectories() {
71-
try {
72-
val cacheDir = applicationContext.cacheDir
73-
val webViewCache = java.io.File(cacheDir, "WebView/Default/HTTP Cache")
74-
val codeCacheJs = java.io.File(webViewCache, "Code Cache/js")
75-
val codeCacheWasm = java.io.File(webViewCache, "Code Cache/wasm")
76-
val indexDir = java.io.File(webViewCache, "index-dir")
77-
codeCacheJs.mkdirs()
78-
codeCacheWasm.mkdirs()
79-
indexDir.mkdirs()
80-
} catch (e: Throwable) {
81-
// Safe fallback
82-
}
83-
}
84-
8575
private fun enableHighRefreshRate() {
8676
if (com.example.browser.DeviceUtils.isEmulator) return
8777
try {

app/src/main/java/com/example/browser/BrowserModels.kt

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,15 +159,73 @@ object DeviceUtils {
159159
val brand = android.os.Build.BRAND.lowercase()
160160
val device = android.os.Build.DEVICE.lowercase()
161161
val product = android.os.Build.PRODUCT.lowercase()
162+
val board = android.os.Build.BOARD.lowercase()
163+
val manufacturer = android.os.Build.MANUFACTURER.lowercase()
164+
val abis = android.os.Build.SUPPORTED_ABIS.map { it.lowercase() }
165+
val isX86 = abis.any { it.contains("x86") }
162166

163167
fingerprint.startsWith("generic")
164168
|| fingerprint.startsWith("unknown")
169+
|| (fingerprint.contains("test-keys") && (isX86 || brand.contains("google")))
165170
|| model.contains("google_sdk")
166171
|| model.contains("emulator")
167172
|| model.contains("android sdk built for")
173+
|| model.contains("cuttlefish")
174+
|| model.contains("sdk_gphone")
175+
|| model.contains("subsystem for android")
168176
|| hardware.contains("goldfish")
169177
|| hardware.contains("ranchu")
178+
|| hardware.contains("cutf")
179+
|| hardware.contains("cuttlefish")
180+
|| hardware.contains("vbox")
181+
|| hardware.contains("qemu")
182+
|| hardware.contains("virtio")
183+
|| (hardware.contains("intel") && isX86)
170184
|| (brand.startsWith("generic") && device.startsWith("generic"))
171185
|| product.contains("sdk")
186+
|| product.contains("cf_")
187+
|| product.contains("cuttlefish")
188+
|| product.contains("vbox")
189+
|| product.contains("emulator")
190+
|| device.contains("vsoc")
191+
|| device.contains("cuttlefish")
192+
|| board.contains("cutf")
193+
|| board.contains("vsoc")
194+
|| board.contains("goldfish")
195+
|| manufacturer.contains("genymotion")
196+
|| isX86
197+
}
198+
199+
val hasDrmRenderNode: Boolean by lazy {
200+
try {
201+
val renderNode = java.io.File("/dev/dri/renderD128")
202+
if (renderNode.exists() && renderNode.canRead()) return@lazy true
203+
val dri = java.io.File("/dev/dri")
204+
if (dri.exists() && dri.isDirectory) {
205+
val list = dri.listFiles()
206+
return@lazy !list.isNullOrEmpty() && list.any { it.name.startsWith("renderD") && it.canRead() }
207+
}
208+
false
209+
} catch (e: Throwable) {
210+
false
211+
}
212+
}
213+
214+
val isDrmSystemWithoutRenderNode: Boolean by lazy {
215+
try {
216+
val dri = java.io.File("/dev/dri")
217+
dri.exists() && !hasDrmRenderNode
218+
} catch (e: Throwable) {
219+
false
220+
}
221+
}
222+
223+
/**
224+
* Determines whether WebView should use software rendering (LAYER_TYPE_SOFTWARE)
225+
* to prevent Mesa driver crashes and "Failed to open rendernode" errors in emulators
226+
* and virtualized environments where host DRM graphics devices are absent or restricted.
227+
*/
228+
val needsSoftwareRendering: Boolean by lazy {
229+
isEmulator || isDrmSystemWithoutRenderNode
172230
}
173231
}

app/src/main/java/com/example/media/MediaSessionManager.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,20 @@ object MediaSessionManager {
9292
return
9393
}
9494

95+
if (playing) {
96+
val prevTabId = _activeMediaTabId.value
97+
if (prevTabId != null && prevTabId != tabId) {
98+
// Another tab has started playing media! Pause the previous tab so sound sources don't overlap
99+
val prevWebView = webViewRegistry[prevTabId]?.get()
100+
prevWebView?.post {
101+
prevWebView.evaluateJavascript(
102+
"if (typeof window.__feather_media_pause === 'function') { window.__feather_media_pause(); }",
103+
null
104+
)
105+
}
106+
}
107+
}
108+
95109
_isPlaying.value = playing
96110
_activeMediaTabId.value = tabId
97111

app/src/main/java/com/example/privacy/FingerprintScriptGenerator.kt

Lines changed: 106 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -98,17 +98,18 @@ object FingerprintScriptGenerator {
9898
Document.prototype.hasFocus = function() { return true; };
9999
} catch(e) {}
100100
101-
// 2. Prevent YouTube from auto-pausing on tab change: intercept visibilitychange and pagehide
101+
// 2. Prevent YouTube from auto-pausing on tab change / backgrounding
102+
const blockedEvents = ['visibilitychange', 'webkitvisibilitychange', 'pagehide', 'blur', 'focusout', 'freeze'];
102103
try {
103104
const origAddEventListener = EventTarget.prototype.addEventListener;
104105
EventTarget.prototype.addEventListener = function(type, listener, options) {
105-
if (type === 'visibilitychange' || type === 'webkitvisibilitychange') {
106+
if (typeof type === 'string' && blockedEvents.indexOf(type.toLowerCase()) !== -1) {
106107
return;
107108
}
108109
return origAddEventListener.apply(this, arguments);
109110
};
110111
111-
['visibilitychange', 'webkitvisibilitychange', 'pagehide'].forEach(function(evt) {
112+
blockedEvents.forEach(function(evt) {
112113
window.addEventListener(evt, function(e) {
113114
if (e && e.stopImmediatePropagation) e.stopImmediatePropagation();
114115
if (e && e.stopPropagation) e.stopPropagation();
@@ -118,8 +119,51 @@ object FingerprintScriptGenerator {
118119
if (e && e.stopPropagation) e.stopPropagation();
119120
}, true);
120121
});
122+
123+
try {
124+
Object.defineProperty(window, 'onblur', { get: function() { return null; }, set: function(v) {}, configurable: true });
125+
Object.defineProperty(window, 'onpagehide', { get: function() { return null; }, set: function(v) {}, configurable: true });
126+
Object.defineProperty(document, 'onvisibilitychange', { get: function() { return null; }, set: function(v) {}, configurable: true });
127+
} catch(e) {}
121128
} catch(e) {}
122129
130+
// Track genuine user interaction vs automatic tab-switch pause
131+
let isUserAction = false;
132+
let userInteractionTimer = null;
133+
let programmaticPauseAllowed = false;
134+
135+
function markUserAction() {
136+
isUserAction = true;
137+
if (userInteractionTimer) clearTimeout(userInteractionTimer);
138+
userInteractionTimer = setTimeout(function() {
139+
isUserAction = false;
140+
}, 1000);
141+
}
142+
143+
['click', 'pointerdown', 'touchend', 'keydown'].forEach(function(evt) {
144+
window.addEventListener(evt, markUserAction, true);
145+
document.addEventListener(evt, markUserAction, true);
146+
});
147+
148+
// Wrap HTMLMediaElement pause to prevent unwanted tab switch pauses
149+
const origPlay = HTMLMediaElement.prototype.play;
150+
const origPause = HTMLMediaElement.prototype.pause;
151+
HTMLMediaElement.prototype.pause = function() {
152+
if (programmaticPauseAllowed || isUserAction) {
153+
return origPause.apply(this, arguments);
154+
}
155+
// Auto-pause detected from background/tab switch; immediately resume playback
156+
const media = this;
157+
origPause.apply(this, arguments);
158+
setTimeout(function() {
159+
if (!programmaticPauseAllowed && !isUserAction && media.paused && !media.ended) {
160+
try {
161+
origPlay.call(media).catch(function() {});
162+
} catch(e) {}
163+
}
164+
}, 40);
165+
};
166+
123167
// 3. Spoof IntersectionObserver for video / player elements so YouTube mobile doesn't pause when offscreen
124168
try {
125169
if ('IntersectionObserver' in window) {
@@ -303,32 +347,33 @@ object FingerprintScriptGenerator {
303347
};
304348
305349
window.__feather_media_pause = function() {
350+
programmaticPauseAllowed = true;
306351
try {
307352
if (window.__feather_actions && typeof window.__feather_actions['pause'] === 'function') {
308353
window.__feather_actions['pause']();
309-
return;
310354
}
311355
} catch(e) {}
312356
try {
313357
const moviePlayer = document.getElementById('movie_player');
314358
if (moviePlayer && typeof moviePlayer.pauseVideo === 'function') {
315359
moviePlayer.pauseVideo();
316-
return;
317360
}
318361
} catch(e) {}
319362
try {
320363
const pauseBtn = document.querySelector('.ytp-play-button, .player-control-play-pause-icon, ytm-custom-control-button, [aria-label*="Pause"]');
321364
if (pauseBtn) {
322365
pauseBtn.click();
323-
return;
324366
}
325367
} catch(e) {}
326368
try {
327369
const mediaEls = document.querySelectorAll('video, audio');
328370
mediaEls.forEach(function(m) {
329-
if (!m.paused) m.pause();
371+
if (!m.paused) origPause.call(m);
330372
});
331373
} catch(e) {}
374+
setTimeout(function() {
375+
programmaticPauseAllowed = false;
376+
}, 400);
332377
};
333378
334379
window.__feather_media_toggle = function() {
@@ -448,4 +493,58 @@ object FingerprintScriptGenerator {
448493
})();
449494
""".trimIndent()
450495
}
496+
497+
/**
498+
* Injects CSS and JS overrides to ensure websites adhere to the browser's theme setting
499+
* (e.g. Light mode when selected, preventing unwanted dark mode detection).
500+
*/
501+
fun generateThemeScript(isDark: Boolean): String {
502+
val targetScheme = if (isDark) "dark" else "light"
503+
return """
504+
(function() {
505+
try {
506+
const targetScheme = '$targetScheme';
507+
const isDark = $isDark;
508+
509+
// 1. Set documentElement color-scheme
510+
if (document.documentElement) {
511+
document.documentElement.style.colorScheme = targetScheme;
512+
}
513+
514+
// 2. Add or update meta color-scheme tag
515+
let meta = document.querySelector('meta[name="color-scheme"]');
516+
if (!meta) {
517+
meta = document.createElement('meta');
518+
meta.name = 'color-scheme';
519+
if (document.head) document.head.appendChild(meta);
520+
}
521+
if (meta) {
522+
meta.content = targetScheme;
523+
}
524+
525+
// 3. Spoof window.matchMedia for prefers-color-scheme media queries
526+
const origMatchMedia = window.matchMedia;
527+
window.matchMedia = function(query) {
528+
if (!query) return origMatchMedia ? origMatchMedia.call(window, query) : null;
529+
const q = String(query).toLowerCase();
530+
if (q.indexOf('prefers-color-scheme') !== -1) {
531+
const matches = isDark ? (q.indexOf('dark') !== -1) : (q.indexOf('light') !== -1);
532+
const mql = origMatchMedia ? origMatchMedia.call(window, query) : {};
533+
return {
534+
matches: matches,
535+
media: query,
536+
onchange: null,
537+
addListener: function(fn) { if (mql && mql.addListener) mql.addListener(fn); },
538+
removeListener: function(fn) { if (mql && mql.removeListener) mql.removeListener(fn); },
539+
addEventListener: function(type, fn, opt) { if (mql && mql.addEventListener) mql.addEventListener(type, fn, opt); },
540+
removeEventListener: function(type, fn, opt) { if (mql && mql.removeEventListener) mql.removeEventListener(type, fn, opt); },
541+
dispatchEvent: function(e) { return mql && mql.dispatchEvent ? mql.dispatchEvent(e) : true; }
542+
};
543+
}
544+
return origMatchMedia ? origMatchMedia.call(window, query) : null;
545+
};
546+
} catch(e) {}
547+
})();
548+
""".trimIndent()
549+
}
451550
}

app/src/main/java/com/example/privacy/PrivacyManager.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ class PrivacyManager(private val context: Context, private val database: AppData
4141
withContext(Dispatchers.Main) {
4242
try {
4343
// Clear WebView cache on main thread safely
44-
val dummyWebView = WebView(context)
44+
val dummyWebView = WebView(context).apply {
45+
if (com.example.browser.DeviceUtils.needsSoftwareRendering) {
46+
setLayerType(android.view.View.LAYER_TYPE_SOFTWARE, null)
47+
}
48+
}
4549
dummyWebView.clearCache(true)
4650
dummyWebView.destroy()
4751
} catch (e: Throwable) {

0 commit comments

Comments
 (0)