diff --git a/app/src/main/assets/xposed_init b/app/src/main/assets/xposed_init index 177f5b5..ab982a3 100644 --- a/app/src/main/assets/xposed_init +++ b/app/src/main/assets/xposed_init @@ -1 +1 @@ -space.bdeshi.veil.HideA11yHook +space.bdeshi.veil.VeilHook diff --git a/app/src/main/java/space/bdeshi/veil/Features.kt b/app/src/main/java/space/bdeshi/veil/Features.kt index 37ca591..e041f2a 100644 --- a/app/src/main/java/space/bdeshi/veil/Features.kt +++ b/app/src/main/java/space/bdeshi/veil/Features.kt @@ -1,21 +1,38 @@ package space.bdeshi.veil object Features { - const val PREFS_NAME = "hidea11y_prefs" + const val PREFS_NAME = "veil_prefs" const val KEY_PREFIX = "features_" + const val HIDDEN_PACKAGES_KEY = "hidden_packages" const val ACCESSIBILITY = "accessibility" const val ADB = "adb" const val DEV_OPTIONS = "dev_options" const val MOCK_LOCATION = "mock_location" + const val ROOT_APPS = "root_apps" + const val BUILD_TAGS = "build_tags" - val ALL: Set = setOf(ACCESSIBILITY, ADB, DEV_OPTIONS, MOCK_LOCATION) + val ALL: Set = setOf( + ACCESSIBILITY, + ADB, + DEV_OPTIONS, + MOCK_LOCATION, + ROOT_APPS, + BUILD_TAGS, + ) val LABELS: LinkedHashMap = linkedMapOf( ACCESSIBILITY to "Accessibility services", ADB to "USB / wireless debugging", DEV_OPTIONS to "Developer options enabled", MOCK_LOCATION to "Mock location (legacy flag only)", + ROOT_APPS to "Root/manager apps (PackageManager)", + BUILD_TAGS to "Build tags/type (test-keys, userdebug)", + ) + + val DEFAULT_HIDDEN_PACKAGES: Set = setOf( + "me.weishu.kernelsu", + "com.topjohnwu.magisk", ) fun keyFor(packageName: String): String = KEY_PREFIX + packageName diff --git a/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt index 310787c..29ed753 100644 --- a/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt +++ b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt @@ -19,8 +19,10 @@ import java.io.File class SettingsActivity : Activity() { private lateinit var prefs: SharedPreferences - private lateinit var listContainer: LinearLayout + private lateinit var appListContainer: LinearLayout + private lateinit var hiddenPackagesContainer: LinearLayout private lateinit var packageInput: EditText + private lateinit var hiddenPackageInput: EditText override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -33,7 +35,7 @@ class SettingsActivity : Activity() { } root.addView(TextView(this).apply { - text = "Hide A11y" + text = "Veil" textSize = 20f setTypeface(typeface, Typeface.BOLD) }) @@ -44,57 +46,99 @@ class SettingsActivity : Activity() { setPadding(0, dp(8), 0, dp(16)) }) - val addRow = LinearLayout(this).apply { - orientation = LinearLayout.HORIZONTAL - gravity = Gravity.CENTER_VERTICAL - } packageInput = EditText(this).apply { hint = "e.g. com.example.targetapp" inputType = InputType.TYPE_CLASS_TEXT layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) } - val addButton = Button(this).apply { - text = "Add" - setOnClickListener { - val pkg = packageInput.text.toString().trim() - if (pkg.isNotEmpty() && pkg.contains(".")) { - prefs.edit().putStringSet(Features.keyFor(pkg), Features.ALL.toMutableSet()).apply() - fixPermissions() - packageInput.setText("") - refreshList() - } + root.addView(addRow(packageInput) { + val pkg = packageInput.text.toString().trim() + if (pkg.isNotEmpty() && pkg.contains(".")) { + prefs.edit().putStringSet(Features.keyFor(pkg), Features.ALL.toMutableSet()).apply() + fixPermissions() + packageInput.setText("") + refreshAppList() } - } - addRow.addView(packageInput) - addRow.addView(addButton) - root.addView(addRow) + }) - listContainer = LinearLayout(this).apply { + appListContainer = LinearLayout(this).apply { orientation = LinearLayout.VERTICAL setPadding(0, dp(16), 0, 0) } - root.addView(listContainer) + root.addView(appListContainer) + + root.addView(TextView(this).apply { + text = "Packages hidden from PackageManager" + textSize = 18f + setTypeface(typeface, Typeface.BOLD) + setPadding(0, dp(24), 0, dp(4)) + }) + root.addView(TextView(this).apply { + text = "Used by the \"Root/manager apps\" toggle above — apps in this list become " + + "invisible to getPackageInfo/getInstalledPackages for any target app with that " + + "toggle on." + setPadding(0, 0, 0, dp(8)) + }) + + hiddenPackageInput = EditText(this).apply { + hint = "e.g. me.weishu.kernelsu" + inputType = InputType.TYPE_CLASS_TEXT + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + } + root.addView(addRow(hiddenPackageInput) { + val pkg = hiddenPackageInput.text.toString().trim() + if (pkg.isNotEmpty() && pkg.contains(".")) { + val current = hiddenPackages().toMutableSet() + current.add(pkg) + prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply() + fixPermissions() + hiddenPackageInput.setText("") + refreshHiddenPackages() + } + }) + + hiddenPackagesContainer = LinearLayout(this).apply { + orientation = LinearLayout.VERTICAL + setPadding(0, dp(8), 0, 0) + } + root.addView(hiddenPackagesContainer) setContentView(ScrollView(this).apply { addView(root) }) - refreshList() + refreshAppList() + refreshHiddenPackages() } - private fun refreshList() { - listContainer.removeAllViews() + private fun addRow(input: EditText, onAdd: () -> Unit): View { + return LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + addView(input) + addView(Button(this@SettingsActivity).apply { + text = "Add" + setOnClickListener { onAdd() } + }) + } + } + + private fun hiddenPackages(): Set = + prefs.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) ?: Features.DEFAULT_HIDDEN_PACKAGES + + private fun refreshAppList() { + appListContainer.removeAllViews() val packages = prefs.all.keys .filter { it.startsWith(Features.KEY_PREFIX) } .map { it.removePrefix(Features.KEY_PREFIX) } .sorted() if (packages.isEmpty()) { - listContainer.addView(TextView(this).apply { + appListContainer.addView(TextView(this).apply { text = "No apps configured yet." }) return } for (pkg in packages) { - listContainer.addView(buildPackageCard(pkg)) + appListContainer.addView(buildPackageCard(pkg)) } } @@ -129,13 +173,47 @@ class SettingsActivity : Activity() { setOnClickListener { prefs.edit().remove(Features.keyFor(pkg)).apply() fixPermissions() - refreshList() + refreshAppList() } }) return card } + private fun refreshHiddenPackages() { + hiddenPackagesContainer.removeAllViews() + val packages = hiddenPackages().sorted() + + if (packages.isEmpty()) { + hiddenPackagesContainer.addView(TextView(this).apply { + text = "None configured." + }) + return + } + + for (pkg in packages) { + val row = LinearLayout(this).apply { + orientation = LinearLayout.HORIZONTAL + gravity = Gravity.CENTER_VERTICAL + } + row.addView(TextView(this).apply { + text = pkg + layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f) + }) + row.addView(Button(this).apply { + text = "Remove" + setOnClickListener { + val current = hiddenPackages().toMutableSet() + current.remove(pkg) + prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply() + fixPermissions() + refreshHiddenPackages() + } + }) + hiddenPackagesContainer.addView(row) + } + } + // LSPosed's own SELinux policy is what actually lets hooked processes read this file; // this just ensures the DAC bits don't block it too. private fun fixPermissions() { diff --git a/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt b/app/src/main/java/space/bdeshi/veil/VeilHook.kt similarity index 54% rename from app/src/main/java/space/bdeshi/veil/HideA11yHook.kt rename to app/src/main/java/space/bdeshi/veil/VeilHook.kt index 74e6821..919a669 100644 --- a/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt +++ b/app/src/main/java/space/bdeshi/veil/VeilHook.kt @@ -1,5 +1,9 @@ package space.bdeshi.veil +import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.os.Build import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.XC_MethodHook import de.robv.android.xposed.XC_MethodReplacement @@ -21,12 +25,13 @@ private val ZERO_VALUE_KEYS_BY_FEATURE = mapOf( Features.MOCK_LOCATION to setOf("mock_location", "allow_mock_location"), ) -class HideA11yHook : IXposedHookLoadPackage { +class VeilHook : IXposedHookLoadPackage { override fun handleLoadPackage(lpparam: LoadPackageParam) { if (lpparam.packageName == SELF_PACKAGE) return - val enabled = readEnabledFeatures(lpparam.packageName) + val prefs = openPrefs() + val enabled = readEnabledFeatures(prefs, lpparam.packageName) if (enabled.isEmpty()) return if (Features.ACCESSIBILITY in enabled) { @@ -39,19 +44,37 @@ class HideA11yHook : IXposedHookLoadPackage { hookSettingsClass("android.provider.Settings\$Secure", lpparam, nullStringKeys, zeroValueKeys) hookSettingsClass("android.provider.Settings\$Global", lpparam, nullStringKeys, zeroValueKeys) } + + if (Features.ROOT_APPS in enabled) { + val hiddenPackages = readHiddenPackages(prefs) + if (hiddenPackages.isNotEmpty()) { + hookPackageManager(lpparam, hiddenPackages) + } + } + + if (Features.BUILD_TAGS in enabled) { + spoofBuildTags() + } } - private fun readEnabledFeatures(packageName: String): Set { + private fun openPrefs(): XSharedPreferences? { return try { - val prefs = XSharedPreferences(SELF_PACKAGE, Features.PREFS_NAME) - prefs.reload() - prefs.getStringSet(Features.keyFor(packageName), Features.ALL) ?: Features.ALL + XSharedPreferences(SELF_PACKAGE, Features.PREFS_NAME).apply { reload() } } catch (t: Throwable) { - // Config unreadable (e.g. never configured, or permissions not yet fixed) — fail open to "hide everything". - Features.ALL + null } } + private fun readEnabledFeatures(prefs: XSharedPreferences?, packageName: String): Set { + // Config unreadable (never configured, or permissions not yet fixed) — fail open to "hide everything". + return prefs?.getStringSet(Features.keyFor(packageName), Features.ALL) ?: Features.ALL + } + + private fun readHiddenPackages(prefs: XSharedPreferences?): Set { + return prefs?.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) + ?: Features.DEFAULT_HIDDEN_PACKAGES + } + private fun hookAccessibilityManager(lpparam: LoadPackageParam) { val clazz = XposedHelpers.findClassIfExists( "android.view.accessibility.AccessibilityManager", @@ -138,4 +161,76 @@ class HideA11yHook : IXposedHookLoadPackage { } } } + + private fun hookPackageManager(lpparam: LoadPackageParam, hiddenPackages: Set) { + val clazz = XposedHelpers.findClassIfExists( + "android.app.ApplicationPackageManager", + lpparam.classLoader, + ) ?: return + + val throwIfHidden = object : XC_MethodReplacement() { + override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? { + val name = param.args.getOrNull(0) as? String + if (name != null && name in hiddenPackages) { + throw PackageManager.NameNotFoundException(name) + } + return XposedBridge.invokeOriginalMethod(param.method, param.thisObject, param.args) + } + } + + val filterList = object : XC_MethodHook() { + override fun afterHookedMethod(param: MethodHookParam) { + @Suppress("UNCHECKED_CAST") + val list = param.result as? MutableList ?: return + list.removeAll { entry -> + val name = (entry as? PackageInfo)?.packageName ?: (entry as? ApplicationInfo)?.packageName + name != null && name in hiddenPackages + } + } + } + + val flagsClasses = listOfNotNull( + Int::class.javaPrimitiveType, + XposedHelpers.findClassIfExists("android.content.pm.PackageManager\$PackageInfoFlags", lpparam.classLoader), + ) + val appFlagsClasses = listOfNotNull( + Int::class.javaPrimitiveType, + XposedHelpers.findClassIfExists("android.content.pm.PackageManager\$ApplicationInfoFlags", lpparam.classLoader), + ) + val stringType = String::class.java + + for (flagsType in flagsClasses) { + try { + XposedHelpers.findAndHookMethod(clazz, "getPackageInfo", stringType, flagsType, throwIfHidden) + } catch (t: Throwable) { + // Not present with this signature on this API level. + } + try { + XposedHelpers.findAndHookMethod(clazz, "getInstalledPackages", flagsType, filterList) + } catch (t: Throwable) { + // Not present with this signature on this API level. + } + } + for (flagsType in appFlagsClasses) { + try { + XposedHelpers.findAndHookMethod(clazz, "getApplicationInfo", stringType, flagsType, throwIfHidden) + } catch (t: Throwable) { + // Not present with this signature on this API level. + } + try { + XposedHelpers.findAndHookMethod(clazz, "getInstalledApplications", flagsType, filterList) + } catch (t: Throwable) { + // Not present with this signature on this API level. + } + } + } + + private fun spoofBuildTags() { + try { + XposedHelpers.setStaticObjectField(Build::class.java, "TAGS", "release-keys") + XposedHelpers.setStaticObjectField(Build::class.java, "TYPE", "user") + } catch (t: Throwable) { + // Field not present/settable on this API level — safe to skip. + } + } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b13ae9..0ea752e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,4 @@ - Hide A11y - Hides accessibility service state from apps selected in LSPosed scope + Veil + Hides accessibility, debugging, and root/hook state from apps selected per-package in the settings screen diff --git a/settings.gradle.kts b/settings.gradle.kts index 1925939..4fd0d1e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -15,5 +15,5 @@ dependencyResolutionManagement { } } -rootProject.name = "hidea11y" +rootProject.name = "veil" include(":app")