Merge pull request #66 from AndreyGubin/issue-65

fix(lsposed): show apps from both spaces
This commit is contained in:
Danila Gornushko 2026-04-21 14:23:18 +03:00 committed by GitHub
commit 7d6ef30f1f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 118 additions and 11 deletions

View file

@ -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<Int> = 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<String, List<Int>> {
val (exitCode, raw) = suExec("pm list packages -U --user all 2>/dev/null")
if (exitCode != 0) return emptyMap()
val out = LinkedHashMap<String, List<Int>>()
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<String> {
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<String, String> {
val (exitCode, raw) = suExec("pm list packages -f --user all 2>/dev/null")
if (exitCode != 0) return emptyMap()
val out = LinkedHashMap<String, String>()
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
}
}

View file

@ -39,11 +39,14 @@ data class AppEntry(
val label: String,
val icon: Drawable?,
val isSystem: Boolean,
val userIds: List<Int> = 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,
)