Add getInstalledPackages to Xposed module API

Expose IPackageManager.getInstalledPackages via binder transaction,
allowing per-app proxy to list packages without ROOT when Xposed is active.
This commit is contained in:
世界
2026-01-17 03:09:39 +08:00
parent 2cebff3d7e
commit 0fe4a3b6a1
5 changed files with 109 additions and 3 deletions

View File

@@ -1,6 +1,8 @@
package io.nekohasekai.sfa.utils
import android.content.Context
import android.content.pm.PackageInfo
import io.nekohasekai.sfa.bg.ParceledListSlice
import io.nekohasekai.sfa.xposed.HookStatusKeys
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -46,4 +48,19 @@ object HookStatusClient {
)
}
}
fun getInstalledPackages(context: Context, flags: Long, userId: Int): List<PackageInfo>? {
val binder = ConnectivityBinderUtils.getBinder(context) ?: return null
return ConnectivityBinderUtils.withParcel { data, reply ->
data.writeInterfaceToken(HookStatusKeys.DESCRIPTOR)
data.writeLong(flags)
data.writeInt(userId)
val ok = binder.transact(HookStatusKeys.TRANSACTION_GET_INSTALLED_PACKAGES, data, reply, 0)
if (!ok) return@withParcel null
reply.readException()
val slice = ParceledListSlice.CREATOR.createFromParcel(reply, PackageInfo::class.java.classLoader)
@Suppress("UNCHECKED_CAST")
(slice as ParceledListSlice<PackageInfo>).list
}
}
}

View File

@@ -6,4 +6,5 @@ object HookStatusKeys {
const val TRANSACTION_UPDATE_PRIVILEGE_SETTINGS = 0x5F01
const val TRANSACTION_GET_ERRORS = 0x5F02
const val TRANSACTION_EXPORT_DEBUG_INFO = 0x5F03
const val TRANSACTION_GET_INSTALLED_PACKAGES = 0x5F04
}

View File

@@ -1,6 +1,7 @@
package io.nekohasekai.sfa.xposed.hooks
import android.content.Context
import android.content.pm.PackageInfo
import android.os.Binder
import android.os.Parcel
import de.robv.android.xposed.XposedHelpers
@@ -35,7 +36,8 @@ class HookIConnectivityManagerOnTransact(
val code = param.args[0] as Int
if (code != HookStatusKeys.TRANSACTION_STATUS &&
code != HookStatusKeys.TRANSACTION_UPDATE_PRIVILEGE_SETTINGS &&
code != HookStatusKeys.TRANSACTION_GET_ERRORS) {
code != HookStatusKeys.TRANSACTION_GET_ERRORS &&
code != HookStatusKeys.TRANSACTION_GET_INSTALLED_PACKAGES) {
return
}
val data = param.args[1] as Parcel
@@ -72,6 +74,15 @@ class HookIConnectivityManagerOnTransact(
param.result = true
return
}
if (code == HookStatusKeys.TRANSACTION_GET_INSTALLED_PACKAGES) {
val flags = data.readLong()
val userId = data.readInt()
val packages = getInstalledPackages(flags, userId)
reply!!.writeNoException()
ParceledListSlice(packages).writeToParcel(reply, 0)
param.result = true
return
}
val enabled = data.readInt() != 0
val slice = ParceledListSlice.CREATOR.createFromParcel(data, PackageEntry::class.java.classLoader)
val packages = HashSet<String>()
@@ -117,4 +128,73 @@ class HookIConnectivityManagerOnTransact(
false
}
}
private fun getInstalledPackages(flags: Long, userId: Int): List<PackageInfo> {
return binderLocalScope {
val pm = getPackageManager() ?: return@binderLocalScope emptyList()
getInstalledPackagesCompat(pm, flags, userId)
}
}
private inline fun <T> binderLocalScope(block: () -> T): T {
val token = Binder.clearCallingIdentity()
return try {
block()
} finally {
Binder.restoreCallingIdentity(token)
}
}
private fun getPackageManager(): Any? {
return try {
val appGlobals = Class.forName("android.app.AppGlobals")
val method = appGlobals.getMethod("getPackageManager")
method.invoke(null)
} catch (e: Throwable) {
HookErrorStore.e(SOURCE, "getPackageManager failed", e)
null
}
}
private fun getInstalledPackagesCompat(pm: Any, flags: Long, userId: Int): List<PackageInfo> {
val result = try {
val method = pm.javaClass.getMethod(
"getInstalledPackages",
Long::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
method.invoke(pm, flags, userId)
} catch (_: Throwable) {
try {
val method = pm.javaClass.getMethod(
"getInstalledPackages",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType,
)
method.invoke(pm, flags.toInt(), userId)
} catch (e: Throwable) {
HookErrorStore.e(SOURCE, "getInstalledPackages failed", e)
return emptyList()
}
}
return unwrapParceledListSlice(result)
}
private fun unwrapParceledListSlice(raw: Any?): List<PackageInfo> {
if (raw == null) return emptyList()
if (raw is List<*>) {
return raw.filterIsInstance<PackageInfo>()
}
return try {
val method = raw.javaClass.getMethod("getList")
val list = method.invoke(raw)
if (list is List<*>) {
list.filterIsInstance<PackageInfo>()
} else {
emptyList()
}
} catch (_: Throwable) {
emptyList()
}
}
}

View File

@@ -54,7 +54,11 @@ object PackageQueryManager {
suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List<PackageInfo> {
return when (val s = strategy) {
is PackageQueryStrategy.ForcedRoot -> RootClient.getInstalledPackages(flags)
is PackageQueryStrategy.ForcedRoot -> {
val userId = android.os.Process.myUserHandle().hashCode()
HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId)
?: RootClient.getInstalledPackages(flags)
}
is PackageQueryStrategy.UserSelected -> RootClient.getInstalledPackages(flags)
is PackageQueryStrategy.Direct -> getPackagesViaPackageManager(flags, retryFlags)
}

View File

@@ -62,7 +62,11 @@ object PackageQueryManager {
suspend fun getInstalledPackages(flags: Int, retryFlags: Int): List<PackageInfo> {
return when (val s = strategy) {
is PackageQueryStrategy.ForcedRoot -> RootClient.getInstalledPackages(flags)
is PackageQueryStrategy.ForcedRoot -> {
val userId = android.os.Process.myUserHandle().hashCode()
HookStatusClient.getInstalledPackages(Application.application, flags.toLong(), userId)
?: RootClient.getInstalledPackages(flags)
}
is PackageQueryStrategy.UserSelected -> when (s.mode) {
Settings.PACKAGE_QUERY_MODE_ROOT -> RootClient.getInstalledPackages(flags)
else -> ShizukuPackageManager.getInstalledPackages(flags)