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:name="xposeddescription"
|
||||||
android:value="@string/module_description" />
|
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>
|
</application>
|
||||||
</manifest>
|
</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.IXposedHookLoadPackage
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
import de.robv.android.xposed.XC_MethodHook
|
||||||
import de.robv.android.xposed.XC_MethodReplacement
|
import de.robv.android.xposed.XC_MethodReplacement
|
||||||
|
import de.robv.android.xposed.XSharedPreferences
|
||||||
import de.robv.android.xposed.XposedBridge
|
import de.robv.android.xposed.XposedBridge
|
||||||
import de.robv.android.xposed.XposedHelpers
|
import de.robv.android.xposed.XposedHelpers
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
|
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
|
||||||
|
|
||||||
private const val SELF_PACKAGE = "space.bdeshi.veil"
|
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_BY_FEATURE = mapOf(
|
||||||
private val NULL_STRING_KEYS = setOf(
|
Features.ACCESSIBILITY to setOf("enabled_accessibility_services"),
|
||||||
"enabled_accessibility_services",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Settings keys whose reads (string "0" / int 0) should look disabled to the hooked app.
|
private val ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
|
||||||
private val ZERO_VALUE_KEYS = setOf(
|
Features.ACCESSIBILITY to setOf("accessibility_enabled", "touch_exploration_enabled"),
|
||||||
"accessibility_enabled",
|
Features.ADB to setOf("adb_enabled", "adb_wifi_enabled"),
|
||||||
"touch_exploration_enabled",
|
Features.DEV_OPTIONS to setOf("development_settings_enabled"),
|
||||||
"adb_enabled",
|
Features.MOCK_LOCATION to setOf("mock_location", "allow_mock_location"),
|
||||||
"adb_wifi_enabled",
|
|
||||||
)
|
)
|
||||||
|
|
||||||
class HideA11yHook : IXposedHookLoadPackage {
|
class HideA11yHook : IXposedHookLoadPackage {
|
||||||
@@ -27,9 +26,30 @@ class HideA11yHook : IXposedHookLoadPackage {
|
|||||||
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
||||||
if (lpparam.packageName == SELF_PACKAGE) return
|
if (lpparam.packageName == SELF_PACKAGE) return
|
||||||
|
|
||||||
hookAccessibilityManager(lpparam)
|
val enabled = readEnabledFeatures(lpparam.packageName)
|
||||||
hookSettingsClass("android.provider.Settings\$Secure", lpparam)
|
if (enabled.isEmpty()) return
|
||||||
hookSettingsClass("android.provider.Settings\$Global", lpparam)
|
|
||||||
|
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) {
|
private fun hookAccessibilityManager(lpparam: LoadPackageParam) {
|
||||||
@@ -60,7 +80,12 @@ class HideA11yHook : IXposedHookLoadPackage {
|
|||||||
hook("isTouchExplorationEnabled", result = false)
|
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
|
val clazz = XposedHelpers.findClassIfExists(className, lpparam.classLoader) ?: return
|
||||||
|
|
||||||
fun callOriginal(param: XC_MethodHook.MethodHookParam): Any? =
|
fun callOriginal(param: XC_MethodHook.MethodHookParam): Any? =
|
||||||
@@ -70,8 +95,8 @@ class HideA11yHook : IXposedHookLoadPackage {
|
|||||||
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
|
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
|
||||||
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param)
|
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param)
|
||||||
return when (name) {
|
return when (name) {
|
||||||
in NULL_STRING_KEYS -> null
|
in nullStringKeys -> null
|
||||||
in ZERO_VALUE_KEYS -> "0"
|
in zeroValueKeys -> "0"
|
||||||
else -> callOriginal(param)
|
else -> callOriginal(param)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +105,7 @@ class HideA11yHook : IXposedHookLoadPackage {
|
|||||||
val intReplacement = object : XC_MethodReplacement() {
|
val intReplacement = object : XC_MethodReplacement() {
|
||||||
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
|
override fun replaceHookedMethod(param: XC_MethodHook.MethodHookParam): Any? {
|
||||||
val name = param.args.getOrNull(1) as? String ?: return callOriginal(param)
|
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