From a85cf1f0ad66612c922a69d32106000dbfcbad6f Mon Sep 17 00:00:00 2001 From: okhsunrog Date: Sun, 19 Apr 2026 19:44:54 +0300 Subject: [PATCH] feat(lsposed): experimentally lower minSdk to 28 for Android 9 Users on older devices asked for Android 9 support. The APK code had no hard dependency on API 29+ beyond a handful of specific call sites; the kmod (GKI 5.10+) still requires Android 12+, but the lsposed path can cover Android 9 if the hooks hold. This ships a build users can test. - minSdk 29 -> 28 - Switch FileObserver(File, Int) to FileObserver(String, Int) in three places (HookEntry, HookLog, PackageVisibilityHooks). The File-based ctor is API 29+ and would NoSuchMethodError on Android 9 during hook install. - Guard NetworkCapabilities.getTransportInfo() (API 29+) in the two self-test call sites in DashboardData and DiagnosticsScreen; on API 28 the leak path does not exist, so report PASS/skipped. --- ...nged-lower-minimum-android-version-to-9-82ba.md | 9 +++++++++ lsposed/app/build.gradle.kts | 2 +- .../kotlin/dev/okhsunrog/vpnhide/DashboardData.kt | 14 ++++++++++---- .../dev/okhsunrog/vpnhide/DiagnosticsScreen.kt | 5 +++++ .../main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt | 5 ++++- .../main/kotlin/dev/okhsunrog/vpnhide/HookLog.kt | 6 +++++- .../okhsunrog/vpnhide/PackageVisibilityHooks.kt | 4 +++- 7 files changed, 37 insertions(+), 8 deletions(-) create mode 100644 changelog.d/changed-lower-minimum-android-version-to-9-82ba.md 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?,