Rename hook class to VeilHook; add root/manager-app hiding and build-tag spoofing

Renamed since the module hides more than accessibility state now — hook class
and app display name become VeilHook/Veil (the module already lived under the
space.bdeshi.veil namespace).

Two new features, same per-app toggle model as everything else:

- Root/manager apps: hides packages in a configurable shared list (default
  KernelSU + Magisk) from PackageManager's getPackageInfo/getApplicationInfo/
  getInstalledPackages/getInstalledApplications.
- Build tags/type: overwrites Build.TAGS/Build.TYPE to release-keys/user for
  the hooked process, hiding the test-keys/userdebug markers apps sometimes
  check for.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Claude Sonnet 5
2026-08-06 20:57:29 +06:00
parent cf7ee62f46
commit 2f6065cc8b
6 changed files with 232 additions and 42 deletions
+1 -1
View File
@@ -1 +1 @@
space.bdeshi.veil.HideA11yHook
space.bdeshi.veil.VeilHook
@@ -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<String> = setOf(ACCESSIBILITY, ADB, DEV_OPTIONS, MOCK_LOCATION)
val ALL: Set<String> = setOf(
ACCESSIBILITY,
ADB,
DEV_OPTIONS,
MOCK_LOCATION,
ROOT_APPS,
BUILD_TAGS,
)
val LABELS: LinkedHashMap<String, String> = 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<String> = setOf(
"me.weishu.kernelsu",
"com.topjohnwu.magisk",
)
fun keyFor(packageName: String): String = KEY_PREFIX + packageName
@@ -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<String> =
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() {
@@ -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<String> {
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<String> {
// 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<String> {
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<String>) {
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<Any?> ?: 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.
}
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
<resources>
<string name="app_name">Hide A11y</string>
<string name="module_description">Hides accessibility service state from apps selected in LSPosed scope</string>
<string name="app_name">Veil</string>
<string name="module_description">Hides accessibility, debugging, and root/hook state from apps selected per-package in the settings screen</string>
</resources>
+1 -1
View File
@@ -15,5 +15,5 @@ dependencyResolutionManagement {
}
}
rootProject.name = "hidea11y"
rootProject.name = "veil"
include(":app")