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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user