Apply one fixed baseline to every scoped app; drop all settings
Per-app settings never worked: a hooked app reads Veil's config through XSharedPreferences, which for a legacy module points at Veil's private data dir (/data/data/space.bdeshi.veil/shared_prefs). Hooked apps can't read it, so every app silently fell back to "hide everything" and every toggle, hidden-package edit and picker entry was ignored. Confirmed on device with Veil Probe: unticking a feature changed nothing. The framework's fix for this (xposedsharedprefs, redirecting the module's prefs dir to one hooked apps can read) needs the framework to hook Veil's own process. On Vector v2.2 here it never does, even with Veil in its own scope and after a reboot, so there's no reliable channel from Veil's app to a hooked app. So stop pretending there is one: - VeilHook applies all six features with the fixed Features.HIDDEN_PACKAGES (KernelSU, Magisk, Veil) to every app in scope, with no prefs read. - Remove discovery: the Application#attach hook, the exported provider (one less way for an app to spot Veil) and the launcher <queries>. - The settings screen becomes a read-only summary plus the help panel; help tips cover leaving an app out of scope, changing the list by rebuilding, and turning verbose logging off afterwards, since it names Veil in each hooked app's own logcat. Verified on device: all 24 testable hooked paths in Veil Probe stay hidden, Veil's provider no longer resolves, a scoped app still loads Veil. Co-Authored-By: agent <agent@bdeshi.space>
This commit is contained in:
7 files changed
+102
-576
No files matched your search
@@ -1,7 +1,7 @@
|
||||
# Veil
|
||||
|
||||
An LSPosed module that hides accessibility, debugging, and root/hook state from specific
|
||||
apps, per app, without changing the actual state of the device.
|
||||
An LSPosed module that hides accessibility, debugging, and root/hook state from the apps
|
||||
in its scope, without changing the actual state of the device.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> Built entirely by AI in one evening to fix a personal problem. YMMV — it works for me
|
||||
@@ -16,9 +16,8 @@ apps, per app, without changing the actual state of the device.
|
||||
## Setup
|
||||
|
||||
1. Build (below) or grab a release APK from the repo's Releases page.
|
||||
2. Enable the module in LSPosed Manager and scope it to the target app.
|
||||
3. Open the target app once — it appears in Veil's own settings screen automatically.
|
||||
4. Uncheck anything you don't want hidden for that app.
|
||||
2. Enable the module in LSPosed Manager and scope it to the target apps.
|
||||
3. Force-stop each target app; Veil applies from its next cold start.
|
||||
|
||||
If the target app still won't launch, check whether it's on your root manager's DenyList
|
||||
(e.g. KernelSU: app profile → "Umount modules"). An app on that list never gets Zygisk
|
||||
@@ -27,7 +26,7 @@ a few other gotchas.
|
||||
|
||||
## Features
|
||||
|
||||
Per app, per feature — anything not listed gets everything hidden by default.
|
||||
Every app in scope gets all of these. Veil's settings screen is a read-only summary.
|
||||
|
||||
| Feature | Hides |
|
||||
| ------------------------ | ------------------------------------------------------ |
|
||||
@@ -35,12 +34,20 @@ Per app, per feature — anything not listed gets everything hidden by default.
|
||||
| USB / wireless debugging | `adb_enabled` / `adb_wifi_enabled` |
|
||||
| Developer options | Developer Options enabled state |
|
||||
| Mock location | Legacy pre-Marshmallow flag only |
|
||||
| Root / manager apps | Package list (default: KernelSU, Magisk, Veil itself) |
|
||||
| Root / manager apps | Fixed list: KernelSU, Magisk, Veil itself |
|
||||
| Build tags/type | Real build swapped for `release-keys` / `user` |
|
||||
|
||||
Doesn't cover native-level root/hook detection or remote attestation (Play Integrity,
|
||||
SafetyNet) — that's your Zygisk implementation's job, not Veil's.
|
||||
|
||||
### Why there's no per-app configuration
|
||||
|
||||
A hooked app can't read anything Veil's own app saves: Veil's data dir is private to it,
|
||||
and the framework's shared-prefs redirect for legacy modules only works if the framework
|
||||
hooks Veil's own process, which Vector doesn't reliably do. Settings would be silently
|
||||
ignored, so there are none. To change what's hidden, edit `VeilHook.kt` /
|
||||
`Features.HIDDEN_PACKAGES` and rebuild. For per-app package lists, use Hide My Applist.
|
||||
|
||||
## Building
|
||||
|
||||
Requires JDK 17 and Android SDK (platform 35, build-tools 35.0.0).
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
<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
|
||||
android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
@@ -39,10 +29,5 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".VeilProvider"
|
||||
android:authorities="space.bdeshi.veil.provider"
|
||||
android:exported="true" />
|
||||
|
||||
</application>
|
||||
</manifest>
|
||||
@@ -4,64 +4,40 @@ data class FeatureInfo(val label: String, val description: String)
|
||||
|
||||
object Features {
|
||||
const val SELF_PACKAGE = "space.bdeshi.veil"
|
||||
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 SELF_HIDDEN_MIGRATED_KEY = "self_hidden_migrated"
|
||||
const val PROVIDER_AUTHORITY = "space.bdeshi.veil.provider"
|
||||
|
||||
const val ACCESSIBILITY = "accessibility"
|
||||
const val ADB = "adb"
|
||||
const val DEV_OPTIONS = "dev_options"
|
||||
const val MOCK_LOCATION = "mock_location"
|
||||
const val ROOT_APPS = "root_apps"
|
||||
const val BUILD_TAGS = "build_tags"
|
||||
|
||||
val ALL: Set<String> = setOf(
|
||||
ACCESSIBILITY,
|
||||
ADB,
|
||||
DEV_OPTIONS,
|
||||
MOCK_LOCATION,
|
||||
ROOT_APPS,
|
||||
BUILD_TAGS,
|
||||
)
|
||||
|
||||
val INFO: LinkedHashMap<String, FeatureInfo> = linkedMapOf(
|
||||
ACCESSIBILITY to FeatureInfo(
|
||||
"Hide accessibility services",
|
||||
"This app will see zero accessibility services running, even if you have some enabled.",
|
||||
// Shown in the settings screen. Every one of these is always applied — see VeilHook.
|
||||
val INFO: List<FeatureInfo> = listOf(
|
||||
FeatureInfo(
|
||||
"Accessibility services",
|
||||
"Apps see zero accessibility services running, even if you have some enabled.",
|
||||
),
|
||||
ADB to FeatureInfo(
|
||||
"Hide USB / wireless debugging",
|
||||
"This app will see debugging as off, regardless of your actual setting.",
|
||||
FeatureInfo(
|
||||
"USB / wireless debugging",
|
||||
"Apps see debugging as off, regardless of your actual setting.",
|
||||
),
|
||||
DEV_OPTIONS to FeatureInfo(
|
||||
"Hide developer options",
|
||||
"This app will see Developer Options as disabled.",
|
||||
FeatureInfo(
|
||||
"Developer options",
|
||||
"Apps see Developer Options as disabled.",
|
||||
),
|
||||
MOCK_LOCATION to FeatureInfo(
|
||||
"Hide mock location (legacy flag)",
|
||||
FeatureInfo(
|
||||
"Mock location (legacy flag)",
|
||||
"Only covers the old pre-Android-6 system flag — modern per-app mock location isn't covered.",
|
||||
),
|
||||
ROOT_APPS to FeatureInfo(
|
||||
"Hide root / manager apps",
|
||||
"Apps in the \"hidden packages\" list below become invisible to this app's PackageManager lookups.",
|
||||
FeatureInfo(
|
||||
"Root / manager apps",
|
||||
"The packages below are invisible to apps' PackageManager lookups.",
|
||||
),
|
||||
BUILD_TAGS to FeatureInfo(
|
||||
"Hide test-keys / userdebug build",
|
||||
"This app will see build tags as release-keys/user instead of your real build.",
|
||||
FeatureInfo(
|
||||
"Test-keys / userdebug build",
|
||||
"Apps see build tags as release-keys/user instead of your real build.",
|
||||
),
|
||||
)
|
||||
|
||||
// Veil itself belongs here too: an installed hooking module is as much a tell as a root
|
||||
// manager. Hiding it from PackageManager doesn't affect discovery, which resolves Veil's
|
||||
// provider through ActivityManager rather than the hooked ApplicationPackageManager.
|
||||
val DEFAULT_HIDDEN_PACKAGES: Set<String> = setOf(
|
||||
// Fixed at build time; edit and rebuild to change. Veil itself is here too: an installed
|
||||
// hooking module is as much a tell as a root manager.
|
||||
val HIDDEN_PACKAGES: Set<String> = setOf(
|
||||
"me.weishu.kernelsu",
|
||||
"com.topjohnwu.magisk",
|
||||
SELF_PACKAGE,
|
||||
)
|
||||
|
||||
fun keyFor(packageName: String): String = KEY_PREFIX + packageName
|
||||
}
|
||||
@@ -1,75 +1,49 @@
|
||||
package space.bdeshi.veil
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Typeface
|
||||
import android.os.Bundle
|
||||
import android.text.Editable
|
||||
import android.text.InputType
|
||||
import android.text.TextWatcher
|
||||
import android.view.Gravity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.ListView
|
||||
import android.widget.ScrollView
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.button.MaterialButton
|
||||
import com.google.android.material.card.MaterialCardView
|
||||
import com.google.android.material.checkbox.MaterialCheckBox
|
||||
import com.google.android.material.chip.Chip
|
||||
import com.google.android.material.chip.ChipGroup
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import com.google.android.material.textfield.TextInputLayout
|
||||
import java.io.File
|
||||
|
||||
private data class LaunchableApp(val label: String, val pkg: String)
|
||||
|
||||
private val HELP_TIPS: List<Pair<String, String>> = listOf(
|
||||
"Target app still won't launch?" to
|
||||
"Check KernelSU Manager → the target app's profile → make sure \"Umount modules\" " +
|
||||
"is OFF for it. When that's on, Zygisk (and therefore LSPosed) skips loading into that " +
|
||||
"app's process entirely — no Veil setting can work around that, since the module " +
|
||||
"never gets injected in the first place.",
|
||||
"app's process entirely — Veil never gets injected in the first place.",
|
||||
"Using a separate Zygisk implementation?" to
|
||||
"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, " +
|
||||
"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
|
||||
"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 " +
|
||||
"cold start — a full device reboot usually isn't necessary.",
|
||||
"Want something left visible to one app?" to
|
||||
"Take that app out of Veil's scope in LSPosed Manager. There's no per-app configuration: " +
|
||||
"hooked apps can't read anything this screen could save, so it would be silently ignored.",
|
||||
"Need a different package list?" to
|
||||
"It's fixed in this build: edit Features.HIDDEN_PACKAGES and rebuild. For per-app package " +
|
||||
"lists, Hide My Applist is built for exactly that.",
|
||||
"Scope changed but nothing happened?" to
|
||||
"Force-stop and reopen the target app. Veil is applied when that app's process starts, " +
|
||||
"so scope changes only take effect on its next cold start.",
|
||||
"How to confirm it's actually working" to
|
||||
"Turn on verbose logging in LSPosed Manager, then check its Logs tab after relaunching " +
|
||||
"the target app. No log lines from Veil there usually means it never got injected " +
|
||||
"(see the DenyList tip above), not a bug in this module.",
|
||||
"Forgot what an unconfigured app does?" to
|
||||
"Any app not in the list below gets every feature applied by default — check here " +
|
||||
"first if something's being hidden that you didn't expect.",
|
||||
"(see the DenyList tip above). Turn verbose logging off again afterwards: it writes a " +
|
||||
"line naming Veil into each hooked app's own logcat, which that app can read.",
|
||||
)
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
private lateinit var appListContainer: LinearLayout
|
||||
private lateinit var hiddenPackagesChips: ChipGroup
|
||||
private lateinit var helpContainer: View
|
||||
private lateinit var helpToggle: MaterialButton
|
||||
private lateinit var hiddenPackageInput: TextInputEditText
|
||||
private var helpVisible = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
prefs = getSharedPreferences(Features.PREFS_NAME, Context.MODE_PRIVATE)
|
||||
migrateSelfIntoHiddenList()
|
||||
|
||||
val padding = dp(16)
|
||||
val root = LinearLayout(this).apply {
|
||||
@@ -83,21 +57,20 @@ class SettingsActivity : AppCompatActivity() {
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
})
|
||||
root.addView(TextView(this).apply {
|
||||
text = "Hide device state from specific apps, per app."
|
||||
text = "Hides the same things from every app in Veil's LSPosed scope."
|
||||
textSize = 14f
|
||||
alpha = 0.7f
|
||||
setPadding(0, dp(2), 0, dp(16))
|
||||
})
|
||||
|
||||
helpToggle = MaterialButton(this).apply {
|
||||
root.addView(MaterialButton(this).apply {
|
||||
text = "Help & troubleshooting ▾"
|
||||
setOnClickListener {
|
||||
helpVisible = !helpVisible
|
||||
helpContainer.visibility = if (helpVisible) View.VISIBLE else View.GONE
|
||||
text = if (helpVisible) "Help & troubleshooting ▴" else "Help & troubleshooting ▾"
|
||||
}
|
||||
}
|
||||
root.addView(helpToggle)
|
||||
})
|
||||
|
||||
val (helpCard, helpContent) = card()
|
||||
helpCard.visibility = View.GONE
|
||||
@@ -116,57 +89,40 @@ class SettingsActivity : AppCompatActivity() {
|
||||
}
|
||||
root.addView(helpCard, marginTop(dp(8)))
|
||||
|
||||
root.addView(sectionTitle("Apps"))
|
||||
root.addView(TextView(this).apply {
|
||||
text = "Scope this module to a target app in LSPosed Manager, then open that app " +
|
||||
"once — it shows up here automatically if it can see Veil. If it doesn't, add " +
|
||||
"it below. Apps not listed here get everything hidden by default."
|
||||
textSize = 13f
|
||||
alpha = 0.8f
|
||||
setPadding(0, dp(4), 0, dp(8))
|
||||
})
|
||||
|
||||
root.addView(MaterialButton(this).apply {
|
||||
text = "Add app"
|
||||
setOnClickListener { showAppPicker() }
|
||||
})
|
||||
|
||||
appListContainer = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
setPadding(0, dp(8), 0, 0)
|
||||
root.addView(sectionTitle("What's hidden"), marginTop(dp(16)))
|
||||
val (featureCard, featureContent) = card()
|
||||
for (info in Features.INFO) {
|
||||
featureContent.addView(TextView(this).apply {
|
||||
text = info.label
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
setPadding(0, dp(6), 0, dp(2))
|
||||
})
|
||||
featureContent.addView(TextView(this).apply {
|
||||
text = info.description
|
||||
textSize = 13f
|
||||
alpha = 0.7f
|
||||
})
|
||||
}
|
||||
root.addView(appListContainer)
|
||||
root.addView(featureCard, marginTop(dp(8)))
|
||||
|
||||
root.addView(sectionTitle("Packages hidden from PackageManager"), marginTop(dp(24)))
|
||||
root.addView(sectionTitle("Hidden packages"), marginTop(dp(16)))
|
||||
root.addView(TextView(this).apply {
|
||||
text = "Used by the \"Hide root / manager apps\" checkbox above — apps here become " +
|
||||
"invisible to PackageManager lookups for any target app with that box checked."
|
||||
text = "Fixed in this build — edit Features.HIDDEN_PACKAGES and rebuild to change."
|
||||
textSize = 13f
|
||||
alpha = 0.8f
|
||||
setPadding(0, dp(4), 0, dp(8))
|
||||
})
|
||||
|
||||
hiddenPackageInput = textInput("e.g. me.weishu.kernelsu")
|
||||
root.addView(addRow(hiddenPackageInput) {
|
||||
val pkg = hiddenPackageInput.text.toString().trim()
|
||||
if (pkg.isNotEmpty() && pkg.contains(".")) {
|
||||
val current = hiddenPackages().toMutableSet()
|
||||
current.add(pkg)
|
||||
prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply()
|
||||
fixPermissions()
|
||||
hiddenPackageInput.setText("")
|
||||
refreshHiddenPackages()
|
||||
root.addView(ChipGroup(this).apply {
|
||||
for (pkg in Features.HIDDEN_PACKAGES.sorted()) {
|
||||
addView(Chip(this@SettingsActivity).apply {
|
||||
text = pkg
|
||||
isClickable = false
|
||||
isCheckable = false
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
hiddenPackagesChips = ChipGroup(this).apply {
|
||||
setPadding(0, dp(8), 0, 0)
|
||||
}
|
||||
root.addView(hiddenPackagesChips)
|
||||
|
||||
setContentView(ScrollView(this).apply { addView(root); fitsSystemWindows = true })
|
||||
refreshAppList()
|
||||
refreshHiddenPackages()
|
||||
}
|
||||
|
||||
private fun sectionTitle(text: String): TextView = TextView(this).apply {
|
||||
@@ -180,31 +136,7 @@ class SettingsActivity : AppCompatActivity() {
|
||||
topMargin = px
|
||||
}
|
||||
|
||||
private fun textInput(hintText: String): TextInputEditText {
|
||||
val editText = TextInputEditText(this).apply {
|
||||
hint = hintText
|
||||
inputType = InputType.TYPE_CLASS_TEXT
|
||||
}
|
||||
return editText
|
||||
}
|
||||
|
||||
private fun addRow(input: TextInputEditText, onAdd: () -> Unit): View {
|
||||
val layout = TextInputLayout(this).apply {
|
||||
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)
|
||||
addView(input)
|
||||
}
|
||||
return LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
addView(layout)
|
||||
addView(MaterialButton(this@SettingsActivity).apply {
|
||||
text = "Add"
|
||||
setOnClickListener { onAdd() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/** A MaterialCardView with an inner padded vertical container, added to root with bottom margin. */
|
||||
/** A MaterialCardView with an inner padded vertical container. */
|
||||
private fun card(): Pair<MaterialCardView, LinearLayout> {
|
||||
val content = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.VERTICAL
|
||||
@@ -216,234 +148,11 @@ class SettingsActivity : AppCompatActivity() {
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT,
|
||||
).apply { bottomMargin = dp(12) }
|
||||
)
|
||||
addView(content)
|
||||
}
|
||||
return cardView to content
|
||||
}
|
||||
|
||||
// A list saved before Veil joined DEFAULT_HIDDEN_PACKAGES never picks up the new default,
|
||||
// so add it once. The flag keeps a later manual removal from being undone.
|
||||
private fun migrateSelfIntoHiddenList() {
|
||||
if (prefs.getBoolean(Features.SELF_HIDDEN_MIGRATED_KEY, false)) return
|
||||
val editor = prefs.edit().putBoolean(Features.SELF_HIDDEN_MIGRATED_KEY, true)
|
||||
if (prefs.contains(Features.HIDDEN_PACKAGES_KEY)) {
|
||||
editor.putStringSet(Features.HIDDEN_PACKAGES_KEY, hiddenPackages() + Features.SELF_PACKAGE)
|
||||
}
|
||||
editor.apply()
|
||||
fixPermissions()
|
||||
}
|
||||
|
||||
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 configured = configuredPackages()
|
||||
val packages = (configured + discoveredPackages()).sorted()
|
||||
|
||||
if (packages.isEmpty()) {
|
||||
appListContainer.addView(TextView(this).apply {
|
||||
text = "No apps yet — scope Veil to a target app in LSPosed Manager and open " +
|
||||
"it once, or add one with \"Add app\"."
|
||||
textSize = 13f
|
||||
alpha = 0.8f
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
for (pkg in packages) {
|
||||
appListContainer.addView(buildPackageCard(pkg, isDiscoveredOnly = pkg !in configured))
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildPackageCard(pkg: String, isDiscoveredOnly: Boolean): View {
|
||||
val enabled = prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL
|
||||
val (cardView, content) = card()
|
||||
|
||||
val titleRow = LinearLayout(this).apply {
|
||||
orientation = LinearLayout.HORIZONTAL
|
||||
gravity = Gravity.CENTER_VERTICAL
|
||||
}
|
||||
val label = appLabel(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
|
||||
setTypeface(typeface, Typeface.BOLD)
|
||||
})
|
||||
if (label != null) {
|
||||
titles.addView(TextView(this).apply {
|
||||
text = pkg
|
||||
textSize = 12f
|
||||
alpha = 0.7f
|
||||
})
|
||||
}
|
||||
titleRow.addView(titles)
|
||||
if (isDiscoveredOnly) {
|
||||
titleRow.addView(Chip(this).apply {
|
||||
text = "Auto-detected"
|
||||
isClickable = false
|
||||
isCheckable = false
|
||||
})
|
||||
}
|
||||
content.addView(titleRow)
|
||||
|
||||
content.addView(TextView(this).apply {
|
||||
text = "Checking a box below hides that thing from this app specifically — it " +
|
||||
"stays on for every other app."
|
||||
textSize = 12f
|
||||
alpha = 0.7f
|
||||
setPadding(0, dp(2), 0, dp(8))
|
||||
})
|
||||
|
||||
for ((id, info) in Features.INFO) {
|
||||
content.addView(MaterialCheckBox(this).apply {
|
||||
text = info.label
|
||||
isChecked = id in enabled
|
||||
setOnCheckedChangeListener { _, isChecked ->
|
||||
val current = (prefs.getStringSet(Features.keyFor(pkg), Features.ALL) ?: Features.ALL).toMutableSet()
|
||||
if (isChecked) current.add(id) else current.remove(id)
|
||||
prefs.edit().putStringSet(Features.keyFor(pkg), current).apply()
|
||||
fixPermissions()
|
||||
}
|
||||
})
|
||||
content.addView(TextView(this).apply {
|
||||
text = info.description
|
||||
textSize = 13f
|
||||
alpha = 0.7f
|
||||
setPadding(dp(32), 0, 0, dp(8))
|
||||
})
|
||||
}
|
||||
|
||||
content.addView(MaterialButton(this).apply {
|
||||
text = "Apply defaults (hide everything)"
|
||||
setOnClickListener {
|
||||
prefs.edit().remove(Features.keyFor(pkg)).apply()
|
||||
fixPermissions()
|
||||
refreshAppList()
|
||||
}
|
||||
})
|
||||
|
||||
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() {
|
||||
hiddenPackagesChips.removeAllViews()
|
||||
for (pkg in hiddenPackages().sorted()) {
|
||||
hiddenPackagesChips.addView(Chip(this).apply {
|
||||
text = pkg
|
||||
isCloseIconVisible = true
|
||||
setOnCloseIconClickListener {
|
||||
val current = hiddenPackages().toMutableSet()
|
||||
current.remove(pkg)
|
||||
prefs.edit().putStringSet(Features.HIDDEN_PACKAGES_KEY, current).apply()
|
||||
fixPermissions()
|
||||
refreshHiddenPackages()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// LSPosed's own SELinux policy is what actually lets hooked processes read this file;
|
||||
// this just ensures the DAC bits don't block it too.
|
||||
private fun fixPermissions() {
|
||||
try {
|
||||
val prefsDir = File(applicationInfo.dataDir, "shared_prefs")
|
||||
prefsDir.setReadable(true, false)
|
||||
prefsDir.setExecutable(true, false)
|
||||
prefsDir.listFiles()?.forEach { it.setReadable(true, false) }
|
||||
} catch (t: Throwable) {
|
||||
// Best-effort.
|
||||
}
|
||||
}
|
||||
|
||||
private fun dp(value: Int): Int = (value * resources.displayMetrics.density).toInt()
|
||||
}
|
||||
@@ -1,139 +1,50 @@
|
||||
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
|
||||
import de.robv.android.xposed.XC_MethodReplacement
|
||||
import de.robv.android.xposed.XSharedPreferences
|
||||
import de.robv.android.xposed.XposedHelpers
|
||||
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
|
||||
|
||||
// Keyed by the Settings class each key actually lives on, so we only ever install hooks on
|
||||
// the one class that could plausibly be asked about a given feature — never both.
|
||||
private val SECURE_NULL_STRING_KEYS_BY_FEATURE = mapOf(
|
||||
Features.ACCESSIBILITY to setOf("enabled_accessibility_services"),
|
||||
// the one class that could plausibly be asked about a given key — never both.
|
||||
private val SECURE_NULL_STRING_KEYS = setOf("enabled_accessibility_services")
|
||||
|
||||
private val SECURE_ZERO_VALUE_KEYS = setOf(
|
||||
"accessibility_enabled",
|
||||
"touch_exploration_enabled",
|
||||
"mock_location",
|
||||
"allow_mock_location",
|
||||
)
|
||||
|
||||
private val SECURE_ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
|
||||
Features.ACCESSIBILITY to setOf("accessibility_enabled", "touch_exploration_enabled"),
|
||||
Features.MOCK_LOCATION to setOf("mock_location", "allow_mock_location"),
|
||||
private val GLOBAL_ZERO_VALUE_KEYS = setOf(
|
||||
"adb_enabled",
|
||||
"adb_wifi_enabled",
|
||||
"development_settings_enabled",
|
||||
)
|
||||
|
||||
private val GLOBAL_ZERO_VALUE_KEYS_BY_FEATURE = mapOf(
|
||||
Features.ADB to setOf("adb_enabled", "adb_wifi_enabled"),
|
||||
Features.DEV_OPTIONS to setOf("development_settings_enabled"),
|
||||
)
|
||||
|
||||
private fun keysForEnabledFeatures(
|
||||
enabled: Set<String>,
|
||||
byFeature: Map<String, Set<String>>,
|
||||
): Set<String> {
|
||||
if (enabled.isEmpty() || byFeature.isEmpty()) return emptySet()
|
||||
val result = mutableSetOf<String>()
|
||||
for (feature in enabled) {
|
||||
byFeature[feature]?.let { result.addAll(it) }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the same fixed baseline to every app in Veil's LSPosed scope. There is deliberately
|
||||
* no per-app configuration: a hooked app can't read anything Veil's own app saves (its data
|
||||
* dir is private to it, and the framework's shared-prefs redirect for legacy modules depends
|
||||
* on the framework hooking Veil's own process, which Vector doesn't reliably do), so settings
|
||||
* would silently be ignored. What's hidden lives in this file and Features.HIDDEN_PACKAGES;
|
||||
* change it there and rebuild.
|
||||
*/
|
||||
class VeilHook : IXposedHookLoadPackage {
|
||||
|
||||
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
||||
if (lpparam.packageName == Features.SELF_PACKAGE) return
|
||||
|
||||
reportScoped(lpparam)
|
||||
|
||||
val prefs = openPrefs()
|
||||
val enabled = readEnabledFeatures(prefs, lpparam.packageName)
|
||||
if (enabled.isEmpty()) return
|
||||
|
||||
if (Features.ACCESSIBILITY in enabled) {
|
||||
hookAccessibilityManager(lpparam)
|
||||
}
|
||||
|
||||
val secureNullKeys = keysForEnabledFeatures(enabled, SECURE_NULL_STRING_KEYS_BY_FEATURE)
|
||||
val secureZeroKeys = keysForEnabledFeatures(enabled, SECURE_ZERO_VALUE_KEYS_BY_FEATURE)
|
||||
if (secureNullKeys.isNotEmpty() || secureZeroKeys.isNotEmpty()) {
|
||||
hookSettingsClass("android.provider.Settings\$Secure", lpparam, secureNullKeys, secureZeroKeys)
|
||||
}
|
||||
|
||||
val globalZeroKeys = keysForEnabledFeatures(enabled, GLOBAL_ZERO_VALUE_KEYS_BY_FEATURE)
|
||||
if (globalZeroKeys.isNotEmpty()) {
|
||||
hookSettingsClass("android.provider.Settings\$Global", lpparam, emptySet(), globalZeroKeys)
|
||||
}
|
||||
|
||||
if (Features.ROOT_APPS in enabled) {
|
||||
val hiddenPackages = readHiddenPackages(prefs)
|
||||
if (hiddenPackages.isNotEmpty()) {
|
||||
hookPackageManager(lpparam, hiddenPackages)
|
||||
}
|
||||
}
|
||||
|
||||
if (Features.BUILD_TAGS in enabled) {
|
||||
spoofBuildTags()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
// 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(
|
||||
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(Features.SELF_PACKAGE, Features.PREFS_NAME).apply { reload() }
|
||||
} catch (t: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun readEnabledFeatures(prefs: XSharedPreferences?, packageName: String): Set<String> {
|
||||
// Config unreadable (never configured, or permissions not yet fixed) — fail open to "hide everything".
|
||||
return prefs?.getStringSet(Features.keyFor(packageName), Features.ALL) ?: Features.ALL
|
||||
}
|
||||
|
||||
private fun readHiddenPackages(prefs: XSharedPreferences?): Set<String> {
|
||||
return prefs?.getStringSet(Features.HIDDEN_PACKAGES_KEY, Features.DEFAULT_HIDDEN_PACKAGES)
|
||||
?: Features.DEFAULT_HIDDEN_PACKAGES
|
||||
hookAccessibilityManager(lpparam)
|
||||
hookSettingsClass("android.provider.Settings\$Secure", lpparam, SECURE_NULL_STRING_KEYS, SECURE_ZERO_VALUE_KEYS)
|
||||
hookSettingsClass("android.provider.Settings\$Global", lpparam, emptySet(), GLOBAL_ZERO_VALUE_KEYS)
|
||||
hookPackageManager(lpparam, Features.HIDDEN_PACKAGES)
|
||||
spoofBuildTags()
|
||||
}
|
||||
|
||||
private fun hookAccessibilityManager(lpparam: LoadPackageParam) {
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<string name="app_name">Veil</string>
|
||||
<string name="module_description">Hides accessibility, debugging, and root/hook state from apps selected per-package in the settings screen</string>
|
||||
<string name="module_description">Hides accessibility, debugging, and root/hook state from every app in its scope</string>
|
||||
</resources>
|
||||
Reference in new issue
Block a user