diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index bfa34ab..a85ec14 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,5 +15,15 @@
android:name="xposeddescription"
android:value="@string/module_description" />
+
+
+
+
+
+
+
diff --git a/app/src/main/java/space/bdeshi/veil/Features.kt b/app/src/main/java/space/bdeshi/veil/Features.kt
new file mode 100644
index 0000000..37ca591
--- /dev/null
+++ b/app/src/main/java/space/bdeshi/veil/Features.kt
@@ -0,0 +1,22 @@
+package space.bdeshi.veil
+
+object Features {
+ const val PREFS_NAME = "hidea11y_prefs"
+ const val KEY_PREFIX = "features_"
+
+ const val ACCESSIBILITY = "accessibility"
+ const val ADB = "adb"
+ const val DEV_OPTIONS = "dev_options"
+ const val MOCK_LOCATION = "mock_location"
+
+ val ALL: Set = setOf(ACCESSIBILITY, ADB, DEV_OPTIONS, MOCK_LOCATION)
+
+ 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)",
+ )
+
+ fun keyFor(packageName: String): String = KEY_PREFIX + packageName
+}
diff --git a/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt b/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt
index f6f626d..74e6821 100644
--- a/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt
+++ b/app/src/main/java/space/bdeshi/veil/HideA11yHook.kt
@@ -3,23 +3,22 @@ package space.bdeshi.veil
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
import de.robv.android.xposed.XC_MethodReplacement
+import de.robv.android.xposed.XSharedPreferences
import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.XposedHelpers
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
private const val SELF_PACKAGE = "space.bdeshi.veil"
-// Settings keys whose *string* reads should look absent to the hooked app.
-private val NULL_STRING_KEYS = setOf(
- "enabled_accessibility_services",
+private val NULL_STRING_KEYS_BY_FEATURE = mapOf(
+ Features.ACCESSIBILITY to setOf("enabled_accessibility_services"),
)
-// Settings keys whose reads (string "0" / int 0) should look disabled to the hooked app.
-private val ZERO_VALUE_KEYS = setOf(
- "accessibility_enabled",
- "touch_exploration_enabled",
- "adb_enabled",
- "adb_wifi_enabled",
+private val ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
+ Features.ACCESSIBILITY to setOf("accessibility_enabled", "touch_exploration_enabled"),
+ Features.ADB to setOf("adb_enabled", "adb_wifi_enabled"),
+ Features.DEV_OPTIONS to setOf("development_settings_enabled"),
+ Features.MOCK_LOCATION to setOf("mock_location", "allow_mock_location"),
)
class HideA11yHook : IXposedHookLoadPackage {
@@ -27,9 +26,30 @@ class HideA11yHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: LoadPackageParam) {
if (lpparam.packageName == SELF_PACKAGE) return
- hookAccessibilityManager(lpparam)
- hookSettingsClass("android.provider.Settings\$Secure", lpparam)
- hookSettingsClass("android.provider.Settings\$Global", lpparam)
+ val enabled = readEnabledFeatures(lpparam.packageName)
+ if (enabled.isEmpty()) return
+
+ if (Features.ACCESSIBILITY in enabled) {
+ hookAccessibilityManager(lpparam)
+ }
+
+ val nullStringKeys = enabled.flatMap { NULL_STRING_KEYS_BY_FEATURE[it].orEmpty() }.toSet()
+ val zeroValueKeys = enabled.flatMap { ZERO_VALUE_KEYS_BY_FEATURE[it].orEmpty() }.toSet()
+ if (nullStringKeys.isNotEmpty() || zeroValueKeys.isNotEmpty()) {
+ hookSettingsClass("android.provider.Settings\$Secure", lpparam, nullStringKeys, zeroValueKeys)
+ hookSettingsClass("android.provider.Settings\$Global", lpparam, nullStringKeys, zeroValueKeys)
+ }
+ }
+
+ private fun readEnabledFeatures(packageName: String): Set {
+ return try {
+ val prefs = XSharedPreferences(SELF_PACKAGE, Features.PREFS_NAME)
+ prefs.reload()
+ prefs.getStringSet(Features.keyFor(packageName), Features.ALL) ?: Features.ALL
+ } catch (t: Throwable) {
+ // Config unreadable (e.g. never configured, or permissions not yet fixed) — fail open to "hide everything".
+ Features.ALL
+ }
}
private fun hookAccessibilityManager(lpparam: LoadPackageParam) {
@@ -60,7 +80,12 @@ class HideA11yHook : IXposedHookLoadPackage {
hook("isTouchExplorationEnabled", result = false)
}
- private fun hookSettingsClass(className: String, lpparam: LoadPackageParam) {
+ private fun hookSettingsClass(
+ className: String,
+ lpparam: LoadPackageParam,
+ nullStringKeys: Set,
+ zeroValueKeys: Set,
+ ) {
val clazz = XposedHelpers.findClassIfExists(className, lpparam.classLoader) ?: return
fun callOriginal(param: XC_MethodHook.MethodHookParam): Any? =
@@ -70,8 +95,8 @@ class HideA11yHook : IXposedHookLoadPackage {
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param)
return when (name) {
- in NULL_STRING_KEYS -> null
- in ZERO_VALUE_KEYS -> "0"
+ in nullStringKeys -> null
+ in zeroValueKeys -> "0"
else -> callOriginal(param)
}
}
@@ -80,7 +105,7 @@ class HideA11yHook : IXposedHookLoadPackage {
val intReplacement = object : XC_MethodReplacement() {
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param)
- return if (name in ZERO_VALUE_KEYS) 0 else callOriginal(param)
+ return if (name in zeroValueKeys) 0 else callOriginal(param)
}
}
diff --git a/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt
new file mode 100644
index 0000000..310787c
--- /dev/null
+++ b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt
@@ -0,0 +1,153 @@
+package space.bdeshi.veil
+
+import android.app.Activity
+import android.content.Context
+import android.content.SharedPreferences
+import android.graphics.Typeface
+import android.os.Bundle
+import android.text.InputType
+import android.view.Gravity
+import android.view.View
+import android.widget.Button
+import android.widget.CheckBox
+import android.widget.EditText
+import android.widget.LinearLayout
+import android.widget.ScrollView
+import android.widget.TextView
+import java.io.File
+
+class SettingsActivity : Activity() {
+
+ private lateinit var prefs: SharedPreferences
+ private lateinit var listContainer: LinearLayout
+ private lateinit var packageInput: EditText
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ prefs = getSharedPreferences(Features.PREFS_NAME, Context.MODE_PRIVATE)
+
+ val padding = dp(16)
+ val root = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(padding, padding, padding, padding)
+ }
+
+ root.addView(TextView(this).apply {
+ text = "Hide A11y"
+ textSize = 20f
+ setTypeface(typeface, Typeface.BOLD)
+ })
+ root.addView(TextView(this).apply {
+ text = "Add the package name of an app you've scoped this module to in LSPosed, " +
+ "then choose which checks to hide from it. Apps you haven't added here get " +
+ "everything hidden by default (matches this module's original behavior)."
+ 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()
+ }
+ }
+ }
+ addRow.addView(packageInput)
+ addRow.addView(addButton)
+ root.addView(addRow)
+
+ listContainer = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, dp(16), 0, 0)
+ }
+ root.addView(listContainer)
+
+ setContentView(ScrollView(this).apply { addView(root) })
+ refreshList()
+ }
+
+ private fun refreshList() {
+ listContainer.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 {
+ text = "No apps configured yet."
+ })
+ return
+ }
+
+ for (pkg in packages) {
+ listContainer.addView(buildPackageCard(pkg))
+ }
+ }
+
+ private fun buildPackageCard(pkg: String): View {
+ val enabled = prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL
+
+ val card = LinearLayout(this).apply {
+ orientation = LinearLayout.VERTICAL
+ setPadding(0, dp(12), 0, dp(12))
+ }
+ card.addView(TextView(this).apply {
+ text = pkg
+ textSize = 16f
+ setTypeface(typeface, Typeface.BOLD)
+ })
+
+ for ((id, label) in Features.LABELS) {
+ card.addView(CheckBox(this).apply {
+ text = label
+ isChecked = id in enabled
+ setOnCheckedChangeListener { _, isChecked ->
+ val current = (prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL).toMutableSet()
+ if (isChecked) current.add(id) else current.remove(id)
+ prefs.edit().putStringSet(Features.keyFor(pkg), current).apply()
+ fixPermissions()
+ }
+ })
+ }
+
+ card.addView(Button(this).apply {
+ text = "Remove (revert to default: hide everything)"
+ setOnClickListener {
+ prefs.edit().remove(Features.keyFor(pkg)).apply()
+ fixPermissions()
+ refreshList()
+ }
+ })
+
+ return card
+ }
+
+ // 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() {
+ try {
+ val prefsDir = File(applicationInfo.dataDir, "shared_prefs")
+ prefsDir.setReadable(true, false)
+ prefsDir.setExecutable(true, false)
+ prefsDir.listFiles()?.forEach { it.setReadable(true, false) }
+ } catch (t: Throwable) {
+ // Best-effort.
+ }
+ }
+
+ private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
+}