Open links in system browser

This commit is contained in:
theipw
2026-06-10 13:40:07 +01:00
parent 62ba37c5bb
commit e44f85a797
3 changed files with 103 additions and 22 deletions
+1
View File
@@ -4,6 +4,7 @@
<selectionStates> <selectionStates>
<SelectionState runConfigName="app"> <SelectionState runConfigName="app">
<option name="selectionMode" value="DROPDOWN" /> <option name="selectionMode" value="DROPDOWN" />
<DialogSelection />
</SelectionState> </SelectionState>
</selectionStates> </selectionStates>
</component> </component>
+3 -3
View File
@@ -14,9 +14,9 @@ android {
defaultConfig { defaultConfig {
applicationId = "com.example.privatefacebook" applicationId = "com.example.privatefacebook"
minSdk = 24 minSdk = 24
targetSdk = 36 targetSdk = 35
versionCode = 1 versionCode = 6
versionName = "1.0" versionName = "1.7"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
} }
@@ -1,6 +1,7 @@
package com.example.privatefacebook package com.example.privatefacebook
import android.content.Context import android.content.Intent
import android.net.Uri
import android.net.http.SslError import android.net.http.SslError
import android.os.Bundle import android.os.Bundle
import android.view.View import android.view.View
@@ -42,6 +43,9 @@ class MainActivity : ComponentActivity() {
webView = findViewById(R.id.webview) webView = findViewById(R.id.webview)
// Use hardware layer for faster video rendering
webView.setLayerType(View.LAYER_TYPE_HARDWARE, null)
val webSettings: WebSettings = webView.settings val webSettings: WebSettings = webView.settings
webSettings.javaScriptEnabled = true webSettings.javaScriptEnabled = true
webSettings.domStorageEnabled = true webSettings.domStorageEnabled = true
@@ -51,9 +55,13 @@ class MainActivity : ComponentActivity() {
webSettings.allowContentAccess = false webSettings.allowContentAccess = false
webSettings.setGeolocationEnabled(false) webSettings.setGeolocationEnabled(false)
// Allow autoplay of media without a user gesture (required for seamless reels autoplay)
webSettings.mediaPlaybackRequiresUserGesture = false
webSettings.userAgentString = webSettings.userAgentString =
"Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Mobile Safari/537.36" "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)) { if (WebViewFeature.isFeatureSupported(WebViewFeature.START_SAFE_BROWSING)) {
WebViewCompat.startSafeBrowsing(this) { /* no-op */ } WebViewCompat.startSafeBrowsing(this) { /* no-op */ }
} }
@@ -75,14 +83,64 @@ class MainActivity : ComponentActivity() {
loadFilterList() loadFilterList()
// Pre-connect to common Facebook/CDN endpoints to reduce initial video handshake latency
Thread {
val warmHosts = listOf(
"https://m.facebook.com",
"https://www.facebook.com",
"https://fbcdn.net",
"https://*.fbcdn.net",
"https://*.facebook.com",
"https://fbsbx.com",
"https://fb.com"
)
for (host in warmHosts) {
try {
val u = URL(host.replace("*.", ""))
val c = (u.openConnection() as? HttpURLConnection) ?: continue
c.connectTimeout = 3000
c.readTimeout = 3000
c.requestMethod = "HEAD"
c.instanceFollowRedirects = true
c.connect()
c.disconnect()
} catch (_: Exception) {
// ignore
}
}
}.start()
webView.webViewClient = object : WebViewClient() { webView.webViewClient = object : WebViewClient() {
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)
view?.context?.startActivity(intent)
return true // Intercept the navigation
} catch (e: Exception) {
return false // Fallback to WebView if browser fails
}
}
return false // Stay in app for Facebook links
}
override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? { override fun shouldInterceptRequest(view: WebView?, request: WebResourceRequest?): WebResourceResponse? {
val req = request ?: return null val req = request ?: return null
val urlStr = req.url.toString() val urlStr = req.url.toString()
val lower = urlStr.lowercase() val lower = urlStr.lowercase()
// Deny cleartext (extra safety)
if (lower.startsWith("http://")) { if (lower.startsWith("http://")) {
// Deny cleartext
return WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) return WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray()))
} }
@@ -93,14 +151,19 @@ class MainActivity : ComponentActivity() {
} }
// Only intercept main-frame GET navigations to strip X-Requested-With and inject CSP // Only intercept main-frame GET navigations to strip X-Requested-With and inject CSP
if (req.isForMainFrame && req.method == "GET" && lower.contains("facebook.com")) { try {
return performControlledFetch(req) if (req.isForMainFrame && req.method == "GET" && lower.contains("facebook.com")) {
return performControlledFetch(req)
}
} catch (_: Exception) {
// Fall through to default behaviour
} }
return null return null
} }
override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) {
// Cancel on SSL errors — safer default
handler?.cancel() handler?.cancel()
} }
@@ -111,6 +174,7 @@ class MainActivity : ComponentActivity() {
} }
} }
// Load mobile Facebook
webView.loadUrl("https://m.facebook.com") webView.loadUrl("https://m.facebook.com")
} }
@@ -124,7 +188,9 @@ class MainActivity : ComponentActivity() {
blockPatterns.add(Regex(escaped, RegexOption.IGNORE_CASE)) blockPatterns.add(Regex(escaped, RegexOption.IGNORE_CASE))
} }
} }
} catch (_: Exception) {} } catch (_: Exception) {
// ignore — empty list is safe
}
} }
private fun performControlledFetch(request: WebResourceRequest): WebResourceResponse? { private fun performControlledFetch(request: WebResourceRequest): WebResourceResponse? {
@@ -138,14 +204,20 @@ class MainActivity : ComponentActivity() {
conn.instanceFollowRedirects = true conn.instanceFollowRedirects = true
conn.requestMethod = "GET" conn.requestMethod = "GET"
request.requestHeaders.forEach { (k, v) -> // Copy headers except X-Requested-With
if (k.lowercase() != "x-requested-with") { try {
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) val cookie = CookieManager.getInstance().getCookie(urlString)
if (!cookie.isNullOrEmpty()) conn.setRequestProperty("Cookie", cookie) if (!cookie.isNullOrEmpty()) {
try { conn.setRequestProperty("Cookie", cookie) } catch (_: Exception) {}
}
conn.connect() conn.connect()
} catch (_: Exception) { } catch (_: Exception) {
@@ -165,16 +237,20 @@ class MainActivity : ComponentActivity() {
} }
val responseHeaders = mutableMapOf<String, String>() val responseHeaders = mutableMapOf<String, String>()
conn.headerFields.forEach { (key, values) -> try {
if (key != null && values != null) responseHeaders[key] = values.joinToString("; ") conn.headerFields.forEach { (key, values) ->
} if (key != null && values != null) responseHeaders[key] = values.joinToString("; ")
}
} catch (_: Exception) {}
val csp = "default-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fbsbx.com; " + // 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; " + "script-src 'self' 'unsafe-inline' 'unsafe-eval' https://*.facebook.com https://*.fbcdn.net; " +
"style-src 'self' 'unsafe-inline' https://*.facebook.com https://*.fbcdn.net; " + "style-src 'self' 'unsafe-inline' https://*.facebook.com https://*.fbcdn.net; " +
"img-src * data: blob:; " + "img-src * data: blob: https://*.fb.com; " +
"connect-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fbsbx.com; " + "media-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fb.com blob: data:; " +
"frame-src 'self' https://*.facebook.com;" "connect-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fbsbx.com https://*.fb.com; " +
"frame-src 'self' https://*.facebook.com https://*.fbsbx.com;")
responseHeaders["Content-Security-Policy"] = csp responseHeaders["Content-Security-Policy"] = csp
responseHeaders["Referrer-Policy"] = "no-referrer" responseHeaders["Referrer-Policy"] = "no-referrer"
@@ -184,7 +260,9 @@ class MainActivity : ComponentActivity() {
val res = WebResourceResponse(mimeType, "utf-8", inputStream ?: ByteArrayInputStream("".toByteArray())) val res = WebResourceResponse(mimeType, "utf-8", inputStream ?: ByteArrayInputStream("".toByteArray()))
res.responseHeaders = responseHeaders res.responseHeaders = responseHeaders
res res
} catch (_: Exception) { null } } catch (_: Exception) {
null
}
} }
private fun injectPrivacyScript() { private fun injectPrivacyScript() {
@@ -222,7 +300,9 @@ class MainActivity : ComponentActivity() {
} catch(_){} } catch(_){}
})(); })();
""".trimIndent() """.trimIndent()
webView.evaluateJavascript(js, null) try {
webView.evaluateJavascript(js, null)
} catch (_: Exception) {}
} }
private fun clearAllWebViewData() { private fun clearAllWebViewData() {