diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a85ec14..a220d21 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -25,5 +25,10 @@
+
+
diff --git a/app/src/main/java/space/bdeshi/veil/Features.kt b/app/src/main/java/space/bdeshi/veil/Features.kt
index 0c805bd..0d5391c 100644
--- a/app/src/main/java/space/bdeshi/veil/Features.kt
+++ b/app/src/main/java/space/bdeshi/veil/Features.kt
@@ -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"
diff --git a/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt
index 8e2236e..762feca 100644
--- a/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt
+++ b/app/src/main/java/space/bdeshi/veil/SettingsActivity.kt
@@ -178,37 +178,53 @@ class SettingsActivity : Activity() {
private fun hiddenPackages(): Set =
prefs.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES) ?: Features.DEFAULT_HIDDEN_PACKAGES
+ private fun configuredPackages(): Set =
+ prefs.all.keys.filter { it.startsWith(Features.KEY_PREFIX) }.map { it.removePrefix(Features.KEY_PREFIX) }.toSet()
+
+ private fun discoveredPackages(): Set =
+ 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."
diff --git a/app/src/main/java/space/bdeshi/veil/VeilHook.kt b/app/src/main/java/space/bdeshi/veil/VeilHook.kt
index 43dc505..ca8d2d5 100644
--- a/app/src/main/java/space/bdeshi/veil/VeilHook.kt
+++ b/app/src/main/java/space/bdeshi/veil/VeilHook.kt
@@ -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() }
diff --git a/app/src/main/java/space/bdeshi/veil/VeilProvider.kt b/app/src/main/java/space/bdeshi/veil/VeilProvider.kt
new file mode 100644
index 0000000..a9b8421
--- /dev/null
+++ b/app/src/main/java/space/bdeshi/veil/VeilProvider.kt
@@ -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?,
+ selection: String?,
+ selectionArgs: Array?,
+ 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?): Int = 0
+ override fun update(uri: Uri, values: ContentValues?, selection: String?, selectionArgs: Array?): Int = 0
+}