Add an app picker for targets that can't report themselves

Auto-discovery only works for apps that can see Veil: on Android 11+ an
app without QUERY_ALL_PACKAGES (or a matching <queries>) can't resolve
Veil's provider, so its first-launch check-in silently fails. A real app hit this: scoped and hooked, but never listed. That made 615f000's premise,
that auto-detection covers every target, wrong.

- "Add app" opens a searchable list of launchable apps; picking one saves
  its default feature set, which is what gives an app a card. A
  launcher-intent <queries> is enough for this and narrower than
  QUERY_ALL_PACKAGES; it widens what Veil sees, not who sees Veil.
- App cards show the app's label above the package name.
- reportScoped checks resolveContentProvider first and skips the call
  when Veil isn't visible. That also drops the framework's
  "Failed to find provider info for space.bdeshi.veil.provider" line,
  which named Veil in the target's own, app-readable logcat.
- New help tip explains why a scoped app may not show up.

Verified on device (Android 16, Vector): a scoped app no longer logs the
provider failure, and adding it through the picker saves its feature set.

Co-Authored-By: agent <agent@bdeshi.space>
This commit is contained in:
bdeshiandagent committed 2026-11-03 18:54:13 +06:00
1 parent 6dfb0d5779
commit af97a85c54
3 files changed
+132 -6

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"> <manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Lets the "Add app" picker list launchable apps, including ones that can't see Veil
and so never reach its discovery provider. This only widens what Veil can see; it
doesn't make Veil any more visible to the apps it hides things from. -->
<queries>
<intent>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent>
</queries>
<application <application
android:label="@string/app_name" android:label="@string/app_name"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
@@ -1,16 +1,23 @@
package space.bdeshi.veil package space.bdeshi.veil
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.SharedPreferences import android.content.SharedPreferences
import android.graphics.Typeface import android.graphics.Typeface
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.InputType import android.text.InputType
import android.text.TextWatcher
import android.view.Gravity import android.view.Gravity
import android.view.View import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.ListView
import android.widget.ScrollView import android.widget.ScrollView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.button.MaterialButton import com.google.android.material.button.MaterialButton
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.checkbox.MaterialCheckBox import com.google.android.material.checkbox.MaterialCheckBox
@@ -20,6 +27,8 @@ import com.google.android.material.textfield.TextInputEditText
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import java.io.File import java.io.File
private data class LaunchableApp(val label: String, val pkg: String)
private val HELP_TIPS: List<Pair<String, String>> = listOf( private val HELP_TIPS: List<Pair<String, String>> = listOf(
"Target app still won't launch?" to "Target app still won't launch?" to
"Check KernelSU Manager → the target app's profile → make sure \"Umount modules\" " + "Check KernelSU Manager → the target app's profile → make sure \"Umount modules\" " +
@@ -30,6 +39,10 @@ private val HELP_TIPS: List<Pair<String, String>> = listOf(
"Standalone Zygisk modules (e.g. Zygisk Next / ReZygisk) can carry their own independent " + "Standalone Zygisk modules (e.g. Zygisk Next / ReZygisk) can carry their own independent " +
"DenyList on top of KernelSU's. If an app is still being skipped after fixing the above, " + "DenyList on top of KernelSU's. If an app is still being skipped after fixing the above, " +
"check there too.", "check there too.",
"Scoped app doesn't show up here?" to
"Android 11+ only lets an app reach packages it can see, and most apps can't see " +
"Veil, so their first-launch check-in never arrives. Veil still hides everything from " +
"them by default; use \"Add app\" to give one its own settings.",
"Changed a setting but nothing happened?" to "Changed a setting but nothing happened?" to
"Force-stop and reopen the target app. Veil reads its config once when that app's " + "Force-stop and reopen the target app. Veil reads its config once when that app's " +
"process starts, so changes here (or to LSPosed scope) only take effect on the next " + "process starts, so changes here (or to LSPosed scope) only take effect on the next " +
@@ -106,13 +119,18 @@ class SettingsActivity : AppCompatActivity() {
root.addView(sectionTitle("Apps")) root.addView(sectionTitle("Apps"))
root.addView(TextView(this).apply { root.addView(TextView(this).apply {
text = "Scope this module to a target app in LSPosed Manager, then open that app " + text = "Scope this module to a target app in LSPosed Manager, then open that app " +
"once — it shows up here automatically. Apps not listed here get everything " + "once — it shows up here automatically if it can see Veil. If it doesn't, add " +
"hidden by default." "it below. Apps not listed here get everything hidden by default."
textSize = 13f textSize = 13f
alpha = 0.8f alpha = 0.8f
setPadding(0, dp(4), 0, dp(8)) setPadding(0, dp(4), 0, dp(8))
}) })
root.addView(MaterialButton(this).apply {
text = "Add app"
setOnClickListener { showAppPicker() }
})
appListContainer = LinearLayout(this).apply { appListContainer = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL orientation = LinearLayout.VERTICAL
setPadding(0, dp(8), 0, 0) setPadding(0, dp(8), 0, 0)
@@ -233,7 +251,7 @@ class SettingsActivity : AppCompatActivity() {
if (packages.isEmpty()) { if (packages.isEmpty()) {
appListContainer.addView(TextView(this).apply { appListContainer.addView(TextView(this).apply {
text = "No apps yet — scope Veil to a target app in LSPosed Manager and open " + text = "No apps yet — scope Veil to a target app in LSPosed Manager and open " +
"it once." "it once, or add one with \"Add app\"."
textSize = 13f textSize = 13f
alpha = 0.8f alpha = 0.8f
}) })
@@ -253,12 +271,24 @@ class SettingsActivity : AppCompatActivity() {
orientation = LinearLayout.HORIZONTAL orientation = LinearLayout.HORIZONTAL
gravity = Gravity.CENTER_VERTICAL gravity = Gravity.CENTER_VERTICAL
} }
titleRow.addView(TextView(this).apply { val label = appLabel(pkg)
text = pkg val titles = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}
titles.addView(TextView(this).apply {
text = label ?: pkg
textSize = 16f textSize = 16f
setTypeface(typeface, Typeface.BOLD) setTypeface(typeface, Typeface.BOLD)
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
}) })
if (label != null) {
titles.addView(TextView(this).apply {
text = pkg
textSize = 12f
alpha = 0.7f
})
}
titleRow.addView(titles)
if (isDiscoveredOnly) { if (isDiscoveredOnly) {
titleRow.addView(Chip(this).apply { titleRow.addView(Chip(this).apply {
text = "Auto-detected" text = "Auto-detected"
@@ -307,6 +337,84 @@ class SettingsActivity : AppCompatActivity() {
return cardView return cardView
} }
// Null when Veil can't see the package: only launchable apps are in its <queries>.
private fun appLabel(pkg: String): String? = try {
packageManager.getApplicationInfo(pkg, 0).loadLabel(packageManager).toString()
} catch (e: Exception) {
null
}
private fun launchableApps(): List<LaunchableApp> {
val intent = Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER)
return packageManager.queryIntentActivities(intent, 0)
.distinctBy { it.activityInfo.packageName }
.map { LaunchableApp(it.loadLabel(packageManager).toString(), it.activityInfo.packageName) }
.sortedBy { it.label.lowercase() }
}
// Loading labels for every launchable app takes a moment, so it runs off the UI thread.
private fun showAppPicker() {
Thread {
val listed = configuredPackages() + discoveredPackages()
val apps = launchableApps().filter { it.pkg !in listed }
runOnUiThread {
if (!isFinishing) showAppPickerDialog(apps)
}
}.start()
}
private fun showAppPickerDialog(apps: List<LaunchableApp>) {
val adapter = object : ArrayAdapter<LaunchableApp>(
this, android.R.layout.simple_list_item_2, android.R.id.text1, apps.toMutableList(),
) {
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
val view = super.getView(position, convertView, parent)
val app = getItem(position) ?: return view
view.findViewById<TextView>(android.R.id.text1).text = app.label
view.findViewById<TextView>(android.R.id.text2).text = app.pkg
return view
}
}
val search = textInput("Search apps")
search.addTextChangedListener(object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable?) {
val query = s.toString().trim()
adapter.clear()
adapter.addAll(apps.filter {
it.label.contains(query, ignoreCase = true) || it.pkg.contains(query, ignoreCase = true)
})
}
})
val list = ListView(this).apply { this.adapter = adapter }
val body = LinearLayout(this).apply {
orientation = LinearLayout.VERTICAL
setPadding(dp(24), dp(8), dp(24), 0)
addView(TextInputLayout(this@SettingsActivity).apply { addView(search) })
addView(list, LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, dp(360)))
}
val dialog = MaterialAlertDialogBuilder(this)
.setTitle("Add app")
.setView(body)
.setNegativeButton("Cancel", null)
.create()
// Saving the default feature set is what makes an app "configured", so it gets a card
// whether or not it ever checks in.
list.setOnItemClickListener { _, _, position, _ ->
val app = adapter.getItem(position) ?: return@setOnItemClickListener
prefs.edit().putStringSet(Features.keyFor(app.pkg), Features.ALL).apply()
fixPermissions()
refreshAppList()
dialog.dismiss()
}
dialog.show()
}
private fun refreshHiddenPackages() { private fun refreshHiddenPackages() {
hiddenPackagesChips.removeAllViews() hiddenPackagesChips.removeAllViews()
for (pkg in hiddenPackages().sorted()) { for (pkg in hiddenPackages().sorted()) {
@@ -93,6 +93,14 @@ class VeilHook : IXposedHookLoadPackage {
override fun afterHookedMethod(param: MethodHookParam) { override fun afterHookedMethod(param: MethodHookParam) {
try { try {
val context = param.args[0] as Context val context = param.args[0] as Context
// Most apps can't see Veil (Android 11+ package visibility), and
// then can't reach its provider either. Checking first skips the
// doomed call, and with it the "Failed to find provider info"
// line naming Veil that the framework would write to the app's
// own, app-readable logcat. Such apps are added via the picker.
if (context.packageManager.resolveContentProvider(Features.PROVIDER_AUTHORITY, 0) == null) {
return
}
context.contentResolver.call( context.contentResolver.call(
Uri.parse("content://${Features.PROVIDER_AUTHORITY}"), Uri.parse("content://${Features.PROVIDER_AUTHORITY}"),
"register", "register",