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>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<provider
|
||||||
|
android:name=".VeilProvider"
|
||||||
|
android:authorities="space.bdeshi.veil.provider"
|
||||||
|
android:exported="true" />
|
||||||
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ object Features {
|
|||||||
const val PREFS_NAME = "veil_prefs"
|
const val PREFS_NAME = "veil_prefs"
|
||||||
const val KEY_PREFIX = "features_"
|
const val KEY_PREFIX = "features_"
|
||||||
const val HIDDEN_PACKAGES_KEY = "hidden_packages"
|
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 ACCESSIBILITY = "accessibility"
|
||||||
const val ADB = "adb"
|
const val ADB = "adb"
|
||||||
|
|||||||
@@ -178,37 +178,53 @@ class SettingsActivity : Activity() {
|
|||||||
private fun hiddenPackages(): Set<String> =
|
private fun hiddenPackages(): Set<String> =
|
||||||
prefs.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) ?: Features.DEFAULT_HIDDEN_PACKAGES
|
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() {
|
private fun refreshAppList() {
|
||||||
appListContainer.removeAllViews()
|
appListContainer.removeAllViews()
|
||||||
val packages = prefs.all.keys
|
val configured = configuredPackages()
|
||||||
.filter { it.startsWith(Features.KEY_PREFIX) }
|
val packages = (configured + discoveredPackages()).sorted()
|
||||||
.map { it.removePrefix(Features.KEY_PREFIX) }
|
|
||||||
.sorted()
|
|
||||||
|
|
||||||
if (packages.isEmpty()) {
|
if (packages.isEmpty()) {
|
||||||
appListContainer.addView(TextView(this).apply {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (pkg in packages) {
|
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 enabled = prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL
|
||||||
|
|
||||||
val card = LinearLayout(this).apply {
|
val card = LinearLayout(this).apply {
|
||||||
orientation = LinearLayout.VERTICAL
|
orientation = LinearLayout.VERTICAL
|
||||||
setPadding(0, dp(12), 0, dp(12))
|
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
|
text = pkg
|
||||||
textSize = 16f
|
textSize = 16f
|
||||||
setTypeface(typeface, Typeface.BOLD)
|
setTypeface(typeface, Typeface.BOLD)
|
||||||
})
|
})
|
||||||
|
if (isDiscoveredOnly) {
|
||||||
|
titleRow.addView(TextView(this).apply {
|
||||||
|
text = " (auto-detected)"
|
||||||
|
textSize = 13f
|
||||||
|
})
|
||||||
|
}
|
||||||
|
card.addView(titleRow)
|
||||||
card.addView(TextView(this).apply {
|
card.addView(TextView(this).apply {
|
||||||
text = "Checking a box below hides that thing from this app specifically — it stays " +
|
text = "Checking a box below hides that thing from this app specifically — it stays " +
|
||||||
"on for every other app."
|
"on for every other app."
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
package space.bdeshi.veil
|
package space.bdeshi.veil
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import android.content.pm.ApplicationInfo
|
import android.content.pm.ApplicationInfo
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
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
|
||||||
@@ -47,6 +49,8 @@ class VeilHook : IXposedHookLoadPackage {
|
|||||||
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
||||||
if (lpparam.packageName == SELF_PACKAGE) return
|
if (lpparam.packageName == SELF_PACKAGE) return
|
||||||
|
|
||||||
|
reportScoped(lpparam)
|
||||||
|
|
||||||
val prefs = openPrefs()
|
val prefs = openPrefs()
|
||||||
val enabled = readEnabledFeatures(prefs, lpparam.packageName)
|
val enabled = readEnabledFeatures(prefs, lpparam.packageName)
|
||||||
if (enabled.isEmpty()) return
|
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? {
|
private fun openPrefs(): XSharedPreferences? {
|
||||||
return try {
|
return try {
|
||||||
XSharedPreferences(SELF_PACKAGE, Features.PREFS_NAME).apply { reload() }
|
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