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..0135021 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/AppListCache.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.graphics.drawable.Drawable +import android.os.Process import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job @@ -23,6 +24,7 @@ internal data class AppSummary( val label: String, val icon: Drawable?, val isSystem: Boolean, + val userIds: List = emptyList(), ) /** @@ -73,21 +75,122 @@ 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, )