Add auto-discovery of LSPosed-scoped apps via ContentProvider

Manually re-typing a package name Veil already got injected into was
redundant work. The hook now reports "I got loaded into package X" to a
small ContentProvider in Veil's own app the moment a scoped app's process
starts (found via hooking Application#attach for a reliable early Context),
which records it to SharedPreferences. Veil's settings screen lists the
union of explicitly configured apps and auto-detected ones, tagging the
latter so it's clear they haven't been customized yet.

No persistent service: the OS starts Veil's process on demand for each
call() and can kill it again right after, same footprint as everything
else in this module.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Claude Sonnet 5
2026-08-06 21:46:16 +06:00
parent fbdf5dedf6
commit 22d7ec9d12
5 changed files with 128 additions and 8 deletions
+5
View File
@@ -25,5 +25,10 @@
</intent-filter>
</activity>
<provider
android:name=".VeilProvider"
android:authorities="space.bdeshi.veil.provider"
android:exported="true" />
</application>
</manifest>
@@ -6,6 +6,8 @@ object Features {
const val PREFS_NAME = "veil_prefs"
const val KEY_PREFIX = "features_"
const val HIDDEN_PACKAGES_KEY = "hidden_packages"
const val DISCOVERED_PACKAGES_KEY = "discovered_packages"
const val PROVIDER_AUTHORITY = "space.bdeshi.veil.provider"
const val ACCESSIBILITY = "accessibility"
const val ADB = "adb"
@@ -178,37 +178,53 @@ class SettingsActivity : Activity() {
private fun hiddenPackages(): Set<String> =
prefs.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) ?: Features.DEFAULT_HIDDEN_PACKAGES
private fun configuredPackages(): Set<String> =
prefs.all.keys.filter { it.startsWith(Features.KEY_PREFIX) }.map { it.removePrefix(Features.KEY_PREFIX) }.toSet()
private fun discoveredPackages(): Set<String> =
prefs.getStringSet(Features.DISCOVERED_PACKAGES_KEY, emptySet()) ?: emptySet()
private fun refreshAppList() {
appListContainer.removeAllViews()
val packages = prefs.all.keys
.filter { it.startsWith(Features.KEY_PREFIX) }
.map { it.removePrefix(Features.KEY_PREFIX) }
.sorted()
val configured = configuredPackages()
val packages = (configured + discoveredPackages()).sorted()
if (packages.isEmpty()) {
appListContainer.addView(TextView(this).apply {
text = "No apps configured yet."
text = "No apps yet — scope Veil to a target app in LSPosed Manager and open " +
"it once, or add a package name above."
})
return
}
for (pkg in packages) {
appListContainer.addView(buildPackageCard(pkg))
appListContainer.addView(buildPackageCard(pkg, isDiscoveredOnly = pkg !in configured))
}
}
private fun buildPackageCard(pkg: String): View {
private fun buildPackageCard(pkg: String, isDiscoveredOnly: Boolean): 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 {
val titleRow = LinearLayout(this).apply {
orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL
}
titleRow.addView(TextView(this).apply {
text = pkg
textSize = 16f
setTypeface(typeface, Typeface.BOLD)
})
if (isDiscoveredOnly) {
titleRow.addView(TextView(this).apply {
text = " (auto-detected)"
textSize = 13f
})
}
card.addView(titleRow)
card.addView(TextView(this).apply {
text = "Checking a box below hides that thing from this app specifically — it stays " +
"on for every other app."
@@ -1,8 +1,10 @@
package space.bdeshi.veil
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XC_MethodHook
@@ -47,6 +49,8 @@ class VeilHook : IXposedHookLoadPackage {
override fun handleLoadPackage(lpparam: LoadPackageParam) {
if (lpparam.packageName == SELF_PACKAGE) return
reportScoped(lpparam)
val prefs = openPrefs()
val enabled = readEnabledFeatures(prefs, lpparam.packageName)
if (enabled.isEmpty()) return
@@ -78,6 +82,37 @@ class VeilHook : IXposedHookLoadPackage {
}
}
// Lets Veil's own settings screen list this app automatically, without the user re-typing
// a package name it can already see was actually injected. Application#attach is the
// earliest reliable point at which a real Context exists in the hooked process.
private fun reportScoped(lpparam: LoadPackageParam) {
try {
XposedHelpers.findAndHookMethod(
"android.app.Application",
lpparam.classLoader,
"attach",
Context::class.java,
object : XC_MethodHook() {
override fun afterHookedMethod(param: MethodHookParam) {
try {
val context = param.args[0] as Context
context.contentResolver.call(
Uri.parse("content://${Features.PROVIDER_AUTHORITY}"),
"register",
lpparam.packageName,
null,
)
} catch (t: Throwable) {
// Veil app unreachable — doesn't affect the hiding hooks below.
}
}
},
)
} catch (t: Throwable) {
// Application#attach signature not present — safe to skip discovery only.
}
}
private fun openPrefs(): XSharedPreferences? {
return try {
XSharedPreferences(SELF_PACKAGE, Features.PREFS_NAME).apply { reload() }
@@ -0,0 +1,62 @@
package space.bdeshi.veil
import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.net.Uri
import android.os.Bundle
import java.io.File
/**
* Exists only so hooked app processes can report "LSPosed loaded me into package X" back to
* Veil's own settings screen — cross-process writes to another app's SharedPreferences aren't
* possible any other way. No persistent service; the OS starts this app briefly on demand for
* each call() and it's free to be killed again right after.
*/
class VeilProvider : ContentProvider() {
override fun onCreate(): Boolean = true
override fun call(method: String, arg: String?, extras: Bundle?): Bundle? {
if (method == "register" && !arg.isNullOrBlank()) {
registerDiscovered(arg)
}
return null
}
private fun registerDiscovered(packageName: String) {
val ctx = context ?: return
val prefs = ctx.getSharedPreferences(Features.PREFS_NAME, Context.MODE_PRIVATE)
val current = prefs.getStringSet(Features.DISCOVERED_PACKAGES_KEY, emptySet()) ?: emptySet()
if (packageName !in current) {
prefs.edit().putStringSet(Features.DISCOVERED_PACKAGES_KEY, current + packageName).apply()
fixPermissions(ctx)
}
}
private fun fixPermissions(ctx: Context) {
try {
val prefsDir = File(ctx.applicationInfo.dataDir, "shared_prefs")
prefsDir.setReadable(true, false)
prefsDir.setExecutable(true, false)
prefsDir.listFiles()?.forEach { it.setReadable(true, false) }
} catch (t: Throwable) {
// Best-effort.
}
}
// No actual table behind this provider — it exists purely for call().
override fun query(
uri: Uri,
projection: Array<String>?,
selection: String?,
selectionArgs: Array<String>?,
sortOrder: String?,
): Cursor? = null
override fun getType(uri: Uri): String? = null
override fun insert(uri: Uri, values: ContentValues?): Uri? = null
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0
override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array<String>?): Int = 0
}