diff --git a/changelog.d/changed-lower-minimum-android-version-to-9-82ba.md b/changelog.d/changed-lower-minimum-android-version-to-9-82ba.md new file mode 100644 index 0000000..9151559 --- /dev/null +++ b/changelog.d/changed-lower-minimum-android-version-to-9-82ba.md @@ -0,0 +1,9 @@ +_2026-04-20_ + +## English + +Lower minimum Android version to 9 (API 28). Experimental — tested builds work on Android 10+, Android 9 support is unverified and needs community reports. + +## Русский + +Понижена минимальная версия Android до 9 (API 28). Экспериментально — сборки протестированы на Android 10+, работа на Android 9 не проверена и требует отчётов от пользователей. diff --git a/lsposed/app/build.gradle.kts b/lsposed/app/build.gradle.kts index 18dfa5c..36b8045 100644 --- a/lsposed/app/build.gradle.kts +++ b/lsposed/app/build.gradle.kts @@ -28,7 +28,7 @@ android { defaultConfig { applicationId = "dev.okhsunrog.vpnhide" - minSdk = 29 + minSdk = 28 targetSdk = 35 versionCode = 700 versionName = buildVersion diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DashboardData.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DashboardData.kt index 60464ad..405d42a 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DashboardData.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DashboardData.kt @@ -1094,10 +1094,16 @@ private fun runJavaProtectionCheck(cm: ConnectivityManager): JavaResult { if (!notVpn) failed++ VpnHideLog.d(TAG, "java: hasCapability(NOT_VPN)=$notVpn") - val info = caps.transportInfo - val isVpnTi = info?.javaClass?.name?.contains("VpnTransportInfo") == true - if (isVpnTi) failed++ - VpnHideLog.d(TAG, "java: transportInfo=${info?.javaClass?.name} isVpn=$isVpnTi") + // NetworkCapabilities.getTransportInfo() was introduced in API 29; + // on API 28 (Android 9) the leak path does not exist, so skip the check. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val info = caps.transportInfo + val isVpnTi = info?.javaClass?.name?.contains("VpnTransportInfo") == true + if (isVpnTi) failed++ + VpnHideLog.d(TAG, "java: transportInfo=${info?.javaClass?.name} isVpn=$isVpnTi") + } else { + VpnHideLog.d(TAG, "java: transportInfo check skipped (API < 29)") + } val vpnNets = cm.allNetworks.count { diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DiagnosticsScreen.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DiagnosticsScreen.kt index cb42dc6..ec93b41 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DiagnosticsScreen.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/DiagnosticsScreen.kt @@ -732,6 +732,11 @@ private fun checkTransportInfo( cm: ConnectivityManager, name: String, ): CheckResult { + // NetworkCapabilities.getTransportInfo() is API 29+; the leak path does + // not exist on Android 9, so the check trivially passes. + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + return CheckResult(name, true, "PASS: transportInfo unavailable (API < 29)") + } val net = cm.activeNetwork ?: return CheckResult(name, true, "PASS: no active network") val caps = cm.getNetworkCapabilities(net) ?: return CheckResult(name, true, "PASS: no capabilities") val info = caps.transportInfo diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt index 95a8e98..5ced50d 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt @@ -239,9 +239,12 @@ class HookEntry : IXposedHookLoadPackage { private fun watchTargetUidsFile() { val dir = "/data/system" val filename = "vpnhide_uids.txt" + + // FileObserver(File, Int) is API 29+; use the String-path form for API 28 compatibility. + @Suppress("DEPRECATION") val observer = object : android.os.FileObserver( - File(dir), + dir, CREATE or CLOSE_WRITE or MOVED_TO or MODIFY, ) { override fun onEvent( diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookLog.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookLog.kt index aa15968..df70767 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookLog.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookLog.kt @@ -4,6 +4,9 @@ import android.os.FileObserver import de.robv.android.xposed.XposedBridge import java.io.File +// FileObserver(File, Int) requires API 29; we stay compatible with API 28 +// (Android 9) by using the String-path constructor throughout. + /** * [XposedBridge.log] wrapper gated by a filesystem flag set from the app. * Used by LSPosed hooks running inside `system_server`, where we don't @@ -26,9 +29,10 @@ internal object HookLog { fun install() { reload() if (watcher != null) return + @Suppress("DEPRECATION") val observer = object : FileObserver( - File("/data/system"), + "/data/system", CREATE or CLOSE_WRITE or MOVED_TO or MODIFY, ) { override fun onEvent( diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/PackageVisibilityHooks.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/PackageVisibilityHooks.kt index 6cd4e7d..b023c4e 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/PackageVisibilityHooks.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/PackageVisibilityHooks.kt @@ -164,8 +164,10 @@ internal object PackageVisibilityHooks { } private fun watchConfigFiles() { + // FileObserver(File, Int) is API 29+; use the String-path form for API 28 compatibility. + @Suppress("DEPRECATION") val observer = - object : FileObserver(File("/data/system"), CREATE or CLOSE_WRITE or MOVED_TO or MODIFY) { + object : FileObserver("/data/system", CREATE or CLOSE_WRITE or MOVED_TO or MODIFY) { override fun onEvent( event: Int, path: String?,