diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml index b268ef3..ca16a99 100644 --- a/.idea/deploymentTargetSelector.xml +++ b/.idea/deploymentTargetSelector.xml @@ -4,6 +4,7 @@ diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2d5b881..33c0d08 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -14,9 +14,9 @@ android { defaultConfig { applicationId = "com.example.privatefacebook" minSdk = 24 - targetSdk = 36 - versionCode = 1 - versionName = "1.0" + targetSdk = 35 + versionCode = 6 + versionName = "1.7" 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 c51cdce..d977427 100644 --- a/app/src/main/java/com/example/privatefacebook/MainActivity.kt +++ b/app/src/main/java/com/example/privatefacebook/MainActivity.kt @@ -1,6 +1,7 @@ package com.example.privatefacebook -import android.content.Context +import android.content.Intent +import android.net.Uri import android.net.http.SslError import android.os.Bundle import android.view.View @@ -42,6 +43,9 @@ 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 webSettings.javaScriptEnabled = true webSettings.domStorageEnabled = true @@ -51,9 +55,13 @@ class MainActivity : ComponentActivity() { 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 */ } } @@ -75,14 +83,64 @@ 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", + "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() { + 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? { val req = request ?: return null val urlStr = req.url.toString() val lower = urlStr.lowercase() + // Deny cleartext (extra safety) if (lower.startsWith("http://")) { - // Deny cleartext 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 - if (req.isForMainFrame && req.method == "GET" && lower.contains("facebook.com")) { - return performControlledFetch(req) + try { + if (req.isForMainFrame && req.method == "GET" && lower.contains("facebook.com")) { + return performControlledFetch(req) + } + } catch (_: Exception) { + // Fall through to default behaviour } return null } override fun onReceivedSslError(view: WebView?, handler: SslErrorHandler?, error: SslError?) { + // Cancel on SSL errors — safer default handler?.cancel() } @@ -111,6 +174,7 @@ class MainActivity : ComponentActivity() { } } + // Load mobile Facebook webView.loadUrl("https://m.facebook.com") } @@ -124,7 +188,9 @@ class MainActivity : ComponentActivity() { blockPatterns.add(Regex(escaped, RegexOption.IGNORE_CASE)) } } - } catch (_: Exception) {} + } catch (_: Exception) { + // ignore — empty list is safe + } } private fun performControlledFetch(request: WebResourceRequest): WebResourceResponse? { @@ -138,14 +204,20 @@ class MainActivity : ComponentActivity() { conn.instanceFollowRedirects = true conn.requestMethod = "GET" - request.requestHeaders.forEach { (k, v) -> - if (k.lowercase() != "x-requested-with") { - try { conn.setRequestProperty(k, v) } catch (_: Exception) {} + // 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) {} + } } - } + } catch (_: Exception) {} + // Forward cookies from WebView cookie store if present 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() } catch (_: Exception) { @@ -165,16 +237,20 @@ class MainActivity : ComponentActivity() { } val responseHeaders = mutableMapOf() - conn.headerFields.forEach { (key, values) -> - if (key != null && values != null) responseHeaders[key] = values.joinToString("; ") - } + try { + 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; " + "style-src 'self' 'unsafe-inline' https://*.facebook.com https://*.fbcdn.net; " + - "img-src * data: blob:; " + - "connect-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fbsbx.com; " + - "frame-src 'self' https://*.facebook.com;" + "img-src * data: blob: https://*.fb.com; " + + "media-src 'self' https://*.facebook.com https://*.fbcdn.net https://*.fb.com blob: data:; " + + "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["Referrer-Policy"] = "no-referrer" @@ -184,7 +260,9 @@ class MainActivity : ComponentActivity() { val res = WebResourceResponse(mimeType, "utf-8", inputStream ?: ByteArrayInputStream("".toByteArray())) res.responseHeaders = responseHeaders res - } catch (_: Exception) { null } + } catch (_: Exception) { + null + } } private fun injectPrivacyScript() { @@ -222,7 +300,9 @@ class MainActivity : ComponentActivity() { } catch(_){} })(); """.trimIndent() - webView.evaluateJavascript(js, null) + try { + webView.evaluateJavascript(js, null) + } catch (_: Exception) {} } private fun clearAllWebViewData() {