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 +}