From 329242877c14b816219fc09ed91152f79965b2fc Mon Sep 17 00:00:00 2001 From: Andrey Gubin Date: Mon, 20 Apr 2026 19:19:54 +0400 Subject: [PATCH 1/2] fix(lsposed): show apps from both spaces --- .../dev/okhsunrog/vpnhide/AppListCache.kt | 118 ++++++++++++++++-- .../dev/okhsunrog/vpnhide/AppPickerScreen.kt | 6 +- 2 files changed, 113 insertions(+), 11 deletions(-) diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt index fb16c05..d628122 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt @@ -3,6 +3,7 @@ package dev.okhsunrog.vpnhide import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager +import android.os.Process import android.graphics.drawable.Drawable import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -23,6 +24,7 @@ internal data class AppSummary( val label: String, val icon: Drawable?, val isSystem: Boolean, + val userIds: List = emptyList(), ) /** @@ -73,21 +75,117 @@ internal object AppListCache { val loaded = withContext(Dispatchers.IO) { val pm = appContext.packageManager - pm - .getInstalledApplications(0) - .map { info -> - AppSummary( - packageName = info.packageName, - label = info.loadLabel(pm).toString(), - icon = runCatching { pm.getApplicationIcon(info) }.getOrNull(), - isSystem = (info.flags and ApplicationInfo.FLAG_SYSTEM) != 0, - ) - }.sortedBy { it.label.lowercase() } + val allUserPackages = loadAllUserPackageNamesViaRoot() + val allUserIdsByPackage = loadAllUserIdsByPackageViaRoot() + val allUserApkPaths = loadAllUserApkPathsViaRoot() + if (allUserPackages.isNotEmpty()) { + allUserPackages + .map { pkg -> + val info = runCatching { pm.getApplicationInfo(pkg, 0) }.getOrNull() + val userIds = allUserIdsByPackage[pkg] ?: emptyList() + val archiveInfo = + if (info == null) loadArchiveApplicationInfo(pm, allUserApkPaths[pkg]) else null + val effectiveInfo = info ?: archiveInfo + + AppSummary( + packageName = pkg, + label = effectiveInfo?.loadLabel(pm)?.toString() ?: pkg, + icon = effectiveInfo?.let { runCatching { pm.getApplicationIcon(it) }.getOrNull() }, + isSystem = effectiveInfo?.let { (it.flags and ApplicationInfo.FLAG_SYSTEM) != 0 } ?: false, + userIds = userIds, + ) + }.sortedBy { it.label.lowercase() } + } else { + // Fallback: current-profile only (legacy behavior) + pm + .getInstalledApplications(0) + .map { info -> + AppSummary( + packageName = info.packageName, + label = info.loadLabel(pm).toString(), + icon = runCatching { pm.getApplicationIcon(info) }.getOrNull(), + isSystem = (info.flags and ApplicationInfo.FLAG_SYSTEM) != 0, + userIds = listOf(Process.myUid() / 100000), + ) + }.sortedBy { it.label.lowercase() } + } } + _apps.value = loaded _refreshCounter.value = _refreshCounter.value + 1 } finally { _loading.value = false } } + + private fun loadAllUserIdsByPackageViaRoot(): Map> { + val (exitCode, raw) = suExec("pm list packages -U --user all 2>/dev/null") + if (exitCode != 0) return emptyMap() + val out = LinkedHashMap>() + raw.lineSequence() + .map { it.trim() } + .filter { it.startsWith("package:") } + .forEach { line -> + val pkg = line.substringAfter("package:").substringBefore(" uid:").trim() + if (pkg.isEmpty()) return@forEach + val uidPart = line.substringAfter("uid:", "").trim() + if (uidPart.isEmpty()) { + out[pkg] = emptyList() + return@forEach + } + val userIds = uidPart + .split(',') + .mapNotNull { it.trim().toIntOrNull() } + .map { it / 100000 } + .distinct() + .sorted() + out[pkg] = userIds + } + return out + } + + private fun loadAllUserPackageNamesViaRoot(): List { + val (exitCode, raw) = suExec("pm list packages --user all 2>/dev/null") + if (exitCode != 0) return emptyList() + return raw + .lineSequence() + .map { it.trim() } + .filter { it.startsWith("package:") } + .map { it.removePrefix("package:").trim() } + .filter { it.isNotEmpty() } + .distinct() + .toList() + } + + + private fun loadAllUserApkPathsViaRoot(): Map { + val (exitCode, raw) = suExec("pm list packages -f --user all 2>/dev/null") + if (exitCode != 0) return emptyMap() + val out = LinkedHashMap() + raw.lineSequence() + .map { it.trim() } + .filter { it.startsWith("package:") } + .forEach { line -> + val body = line.removePrefix("package:") + val eq = body.lastIndexOf('=') + if (eq <= 0 || eq >= body.lastIndex) return@forEach + val apkPath = body.substring(0, eq).trim() + val pkg = body.substring(eq + 1).trim() + if (apkPath.isNotEmpty() && pkg.isNotEmpty() && out[pkg] == null) { + out[pkg] = apkPath + } + } + return out + } + + + @Suppress("DEPRECATION") + private fun loadArchiveApplicationInfo(pm: PackageManager, apkPath: String?): ApplicationInfo? { + if (apkPath.isNullOrBlank()) return null + val pkgInfo = runCatching { pm.getPackageArchiveInfo(apkPath, 0) }.getOrNull() ?: return null + val appinfo = pkgInfo.applicationInfo ?: return null + appinfo.sourceDir = apkPath + appinfo.publicSourceDir = apkPath + return appinfo + } } diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppPickerScreen.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppPickerScreen.kt index 9d17f35..63ee7aa 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppPickerScreen.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppPickerScreen.kt @@ -39,11 +39,14 @@ data class AppEntry( val label: String, val icon: Drawable?, val isSystem: Boolean, + val userIds: List = emptyList(), val kmod: Boolean = false, val zygisk: Boolean = false, val lsposed: Boolean = false, ) { val anySelected get() = kmod || zygisk || lsposed + val labelWithUsers: String + get() = if (userIds.isEmpty()) label else "$label (${userIds.joinToString(", ")})" } /** Which installed modules are present (detected once at load). */ @@ -109,6 +112,7 @@ fun AppPickerScreen( label = app.label, icon = app.icon, isSystem = app.isSystem, + userIds = app.userIds, kmod = app.packageName in t.kmodTargets, zygisk = app.packageName in t.zygiskTargets, lsposed = app.packageName in t.lsposedTargets, @@ -432,7 +436,7 @@ private fun AppRow( } Column(modifier = Modifier.weight(1f)) { Text( - text = app.label, + text = app.labelWithUsers, style = MaterialTheme.typography.bodyLarge, fontWeight = FontWeight.Medium, ) From b9b7bd340f26acb9d97701d3101d8494b2f9ca56 Mon Sep 17 00:00:00 2001 From: Andrey Gubin Date: Mon, 20 Apr 2026 22:04:33 +0400 Subject: [PATCH 2/2] fix lint --- .../dev/okhsunrog/vpnhide/AppListCache.kt | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt index d628122..0135021 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt @@ -3,8 +3,8 @@ package dev.okhsunrog.vpnhide import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.os.Process import android.graphics.drawable.Drawable +import android.os.Process import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -122,7 +122,8 @@ internal object AppListCache { val (exitCode, raw) = suExec("pm list packages -U --user all 2>/dev/null") if (exitCode != 0) return emptyMap() val out = LinkedHashMap>() - raw.lineSequence() + raw + .lineSequence() .map { it.trim() } .filter { it.startsWith("package:") } .forEach { line -> @@ -133,12 +134,14 @@ internal object AppListCache { out[pkg] = emptyList() return@forEach } - val userIds = uidPart - .split(',') - .mapNotNull { it.trim().toIntOrNull() } - .map { it / 100000 } - .distinct() - .sorted() + + val userIds = + uidPart + .split(',') + .mapNotNull { it.trim().toIntOrNull() } + .map { it / 100000 } + .distinct() + .sorted() out[pkg] = userIds } return out @@ -157,12 +160,12 @@ internal object AppListCache { .toList() } - private fun loadAllUserApkPathsViaRoot(): Map { val (exitCode, raw) = suExec("pm list packages -f --user all 2>/dev/null") if (exitCode != 0) return emptyMap() val out = LinkedHashMap() - raw.lineSequence() + raw + .lineSequence() .map { it.trim() } .filter { it.startsWith("package:") } .forEach { line -> @@ -178,9 +181,11 @@ internal object AppListCache { return out } - @Suppress("DEPRECATION") - private fun loadArchiveApplicationInfo(pm: PackageManager, apkPath: String?): ApplicationInfo? { + private fun loadArchiveApplicationInfo( + pm: PackageManager, + apkPath: String?, + ): ApplicationInfo? { if (apkPath.isNullOrBlank()) return null val pkgInfo = runCatching { pm.getPackageArchiveInfo(apkPath, 0) }.getOrNull() ?: return null val appinfo = pkgInfo.applicationInfo ?: return null