Add per-app configurable settings screen
Replaces "hide everything for every scoped app" with per-package, per-feature toggles, backed by SharedPreferences the hook reads via XSharedPreferences. A package with no explicit entry still gets every feature hidden, keeping prior behavior as the default. Adds a launcher activity so the settings screen is directly reachable. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -15,5 +15,15 @@
|
||||
android:name="xposeddescription"
|
||||
android:value="@string/module_description" />
|
||||
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
|
||||
@@ -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<String> = setOf(ACCESSIBILITY, ADB, DEV_OPTIONS, MOCK_LOCATION)
|
||||
|
||||
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)",
|
||||
)
|
||||
|
||||
fun keyFor(packageName: String): String = KEY_PREFIX + packageName
|
||||
}
|
||||
@@ -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<String> {
|
||||
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<String>,
|
||||
zeroValueKeys: Set<String>,
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user