From 1502c0b039ae438b6bc980236589f3d442f9815d Mon Sep 17 00:00:00 2001 From: theipw Date: Wed, 10 Jun 2026 13:25:48 +0100 Subject: [PATCH] Open links in system browser, tested on Pixel 8 Pro --- app/build.gradle.kts | 2 +- .../example/privatefacebook/MainActivity.kt | 76 ++++++++----------- 2 files changed, 32 insertions(+), 46 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 33c0d08..36589db 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -16,7 +16,7 @@ android { minSdk = 24 targetSdk = 35 versionCode = 6 - versionName = "1.7" + versionName = "1.8" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/com/example/privatefacebook/MainActivity.kt b/app/src/main/java/com/example/privatefacebook/MainActivity.kt index d977427..fe5f28f 100644 --- a/app/src/main/java/com/example/privatefacebook/MainActivity.kt +++ b/app/src/main/java/com/example/privatefacebook/MainActivity.kt @@ -42,8 +42,6 @@ class MainActivity : ComponentActivity() { } webView = findViewById(R.id.webview) - - // Use hardware layer for faster video rendering webView.setLayerType(View.LAYER_TYPE_HARDWARE, null) val webSettings: WebSettings = webView.settings @@ -54,19 +52,14 @@ class MainActivity : ComponentActivity() { webSettings.allowFileAccess = false webSettings.allowContentAccess = false webSettings.setGeolocationEnabled(false) - - // Allow autoplay of media without a user gesture (required for seamless reels autoplay) webSettings.mediaPlaybackRequiresUserGesture = false - webSettings.userAgentString = "Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" - // Safe browsing (guarded by feature check) if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) { WebViewCompat.startSafeBrowsing(this) { /* no-op */ } } - // Optional: force dark in-webview to follow system (if supported) if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) { WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_ON) } @@ -83,7 +76,6 @@ class MainActivity : ComponentActivity() { loadFilterList() - // Pre-connect to common Facebook/CDN endpoints to reduce initial video handshake latency Thread { val warmHosts = listOf( "https://m.facebook.com", @@ -104,42 +96,58 @@ class MainActivity : ComponentActivity() { c.instanceFollowRedirects = true c.connect() c.disconnect() - } catch (_: Exception) { - // ignore - } + } catch (_: Exception) {} } }.start() webView.webViewClient = object : WebViewClient() { + // First Gate: Standard navigation override override fun shouldOverrideUrlLoading(view: WebView?, request: WebResourceRequest?): Boolean { val url = request?.url ?: return false val urlStr = url.toString().lowercase() - // Define what stays inside the app (Facebook domains) val isFacebook = urlStr.contains("facebook.com") || urlStr.contains("fbcdn.net") || urlStr.contains("fbsbx.com") || urlStr.contains("fb.com") if (!isFacebook) { - // External link detected: hand off to default system browser try { val intent = Intent(Intent.ACTION_VIEW, url) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) view?.context?.startActivity(intent) - return true // Intercept the navigation + return true } catch (e: Exception) { - return false // Fallback to WebView if browser fails + return false } } - return false // Stay in app for Facebook links + return false } + // Second Gate: Intercepting requests to catch deep-links and redirects override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { val req = request ?: return null val urlStr = req.url.toString() val lower = urlStr.lowercase() - // Deny cleartext (extra safety) + // HARDWARE FIX: If a main frame is loading a non-Facebook URL, force it to browser + if (req.isForMainFrame) { + val isFacebook = lower.contains("facebook.com") || + lower.contains("fb.com") || + lower.contains("fbcdn.net") || + lower.contains("fbsbx.com") + + if (!isFacebook) { + try { + val intent = Intent(Intent.ACTION_VIEW, req.url) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + view?.context?.startActivity(intent) + } catch (_: Exception) {} + // Return empty stream so the WebView stays on the FB page + return WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) + } + } + if (lower.startsWith("http://")) { return WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) } @@ -150,20 +158,16 @@ class MainActivity : ComponentActivity() { } } - // Only intercept main-frame GET navigations to strip X-Requested-With and inject CSP try { if (req.isForMainFrame && req.method == "GET" && lower.contains("facebook.com")) { return performControlledFetch(req) } - } catch (_: Exception) { - // Fall through to default behaviour - } + } catch (_: Exception) {} return null } override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { - // Cancel on SSL errors — safer default handler?.cancel() } @@ -174,7 +178,6 @@ class MainActivity : ComponentActivity() { } } - // Load mobile Facebook webView.loadUrl("https://m.facebook.com") } @@ -188,9 +191,7 @@ class MainActivity : ComponentActivity() { blockPatterns.add(Regex(escaped, RegexOption.IGNORE_CASE)) } } - } catch (_: Exception) { - // ignore — empty list is safe - } + } catch (_: Exception) {} } private fun performControlledFetch(request: WebResourceRequest): WebResourceResponse? { @@ -204,16 +205,12 @@ class MainActivity : ComponentActivity() { conn.instanceFollowRedirects = true conn.requestMethod = "GET" - // Copy headers except X-Requested-With - try { - request.requestHeaders.forEach { (k, v) -> - if (k.lowercase() != "x-requested-with") { - try { conn.setRequestProperty(k, v) } catch (_: Exception) {} - } + request.requestHeaders.forEach { (k, v) -> + if (k.lowercase() != "x-requested-with") { + try { conn.setRequestProperty(k, v) } catch (_: Exception) {} } - } catch (_: Exception) {} + } - // Forward cookies from WebView cookie store if present val cookie = CookieManager.getInstance().getCookie(urlString) if (!cookie.isNullOrEmpty()) { try { conn.setRequestProperty("Cookie", cookie) } catch (_: Exception) {} @@ -243,7 +240,6 @@ class MainActivity : ComponentActivity() { } } catch (_: Exception) {} - // CSP updated to allow facebook video streaming (blob:, data:) and fb.com previews val csp = ("default-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fbsbx.com https://*.fb.com; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.facebook.com https://*.fbcdn.net; " + "style-src 'self' 'unsafe-inline' https://*.facebook.com https://*.fbcdn.net; " + @@ -305,16 +301,6 @@ class MainActivity : ComponentActivity() { } catch (_: Exception) {} } - private fun clearAllWebViewData() { - try { - webView.clearCache(true) - webView.clearHistory() - WebStorage.getInstance().deleteAllData() - CookieManager.getInstance().removeAllCookies(null) - CookieManager.getInstance().flush() - } catch (_: Exception) {} - } - override fun onPause() { super.onPause() CookieManager.getInstance().flush()