manager: Reuse the aidi reflection to retrieve the list of applications

This commit is contained in:
ShirkNeko
2025-10-17 01:59:56 +08:00
parent 62635879e0
commit 8db55f56a9
6 changed files with 93 additions and 171 deletions

View File

@@ -51,6 +51,7 @@ android {
} }
buildFeatures { buildFeatures {
aidl = true
buildConfig = true buildConfig = true
compose = true compose = true
prefab = true prefab = true

View File

@@ -43,4 +43,6 @@
-keep class com.dergoogler.mmrl.webui.interfaces.** { *; } -keep class com.dergoogler.mmrl.webui.interfaces.** { *; }
-keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } -keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; }
-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } -keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; }
-keep interface com.sukisu.zako.** { *; }

View File

@@ -0,0 +1,10 @@
// IKsuInterface.aidl
package com.sukisu.zako;
import android.content.pm.PackageInfo;
import java.util.List;
interface IKsuInterface {
int getPackageCount();
List<PackageInfo> getPackages(int start, int maxCount);
}

View File

@@ -1,137 +1,75 @@
package com.sukisu.ultra.ui package com.sukisu.ultra.ui
import android.annotation.SuppressLint
import android.content.Intent import android.content.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.* import android.os.*
import android.util.Log import android.util.Log
import com.topjohnwu.superuser.ipc.RootService import com.topjohnwu.superuser.ipc.RootService
import rikka.parcelablelist.ParcelableListSlice import com.sukisu.zako.IKsuInterface
import java.lang.reflect.Method
/** /**
* @author ShirkNeko * @author ShirkNeko
* @date 2025/7/2. * @date 2025/10/17.
*/ */
class KsuService : RootService() { class KsuService : RootService() {
companion object { private val TAG = "KsuService"
private const val TAG = "KsuService"
private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface" private val cacheLock = Object()
private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0 private var _all: List<PackageInfo>? = null
private val allPackages: List<PackageInfo>
get() = synchronized(cacheLock) {
_all ?: loadAllPackages().also { _all = it }
}
private fun loadAllPackages(): List<PackageInfo> {
val tmp = arrayListOf<PackageInfo>()
for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) {
val userId = user.getUserIdCompat()
tmp += getInstalledPackagesAsUser(userId)
}
return tmp
} }
interface IKsuInterface : IInterface { internal inner class Stub : IKsuInterface.Stub() {
fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> override fun getPackageCount(): Int = allPackages.size
}
abstract class Stub : Binder(), IKsuInterface { override fun getPackages(start: Int, maxCount: Int): List<PackageInfo> {
init { val list = allPackages
attachInterface(this, DESCRIPTOR) val end = (start + maxCount).coerceAtMost(list.size)
} return if (start >= list.size) emptyList()
else list.subList(start, end)
companion object {
fun asInterface(obj: IBinder?): IKsuInterface? {
if (obj == null) return null
val iin = obj.queryLocalInterface(DESCRIPTOR)
return if (iin != null && iin is IKsuInterface) {
iin
} else {
Proxy(obj)
}
}
}
override fun asBinder(): IBinder = this
override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean {
val descriptor = DESCRIPTOR
when (code) {
INTERFACE_TRANSACTION -> {
reply?.writeString(descriptor)
return true
}
TRANSACTION_GET_PACKAGES -> {
data.enforceInterface(descriptor)
val flagsArg = data.readInt()
val result = getPackages(flagsArg)
reply?.writeNoException()
reply?.writeInt(1)
result.writeToParcel(reply!!, Parcelable.PARCELABLE_WRITE_RETURN_VALUE)
return true
}
}
return super.onTransact(code, data, reply, flags)
}
private class Proxy(private val mRemote: IBinder) : IKsuInterface {
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val data = Parcel.obtain()
val reply = Parcel.obtain()
return try {
data.writeInterfaceToken(DESCRIPTOR)
data.writeInt(flags)
mRemote.transact(TRANSACTION_GET_PACKAGES, data, reply, 0)
reply.readException()
if (reply.readInt() != 0) {
@Suppress("UNCHECKED_CAST")
ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice<PackageInfo>
} else {
ParcelableListSlice(emptyList<PackageInfo>())
}
} finally {
reply.recycle()
data.recycle()
}
}
override fun asBinder(): IBinder = mRemote
} }
} }
inner class KsuInterfaceImpl : Stub() { override fun onBind(intent: Intent): IBinder = Stub()
override fun getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
val list = getInstalledPackagesAll(flags)
Log.i(TAG, "getPackages: ${list.size}")
return ParcelableListSlice(list)
}
}
override fun onBind(intent: Intent): IBinder { @SuppressLint("PrivateApi")
return KsuInterfaceImpl() private fun getInstalledPackagesAsUser(userId: Int): List<PackageInfo> {
}
private fun getUserIds(): List<Int> {
val result = mutableListOf<Int>()
val um = getSystemService(USER_SERVICE) as UserManager
val userProfiles = um.userProfiles
for (userProfile in userProfiles) {
result.add(userProfile.hashCode())
}
return result
}
private fun getInstalledPackagesAll(flags: Int): ArrayList<PackageInfo> {
val packages = ArrayList<PackageInfo>()
for (userId in getUserIds()) {
Log.i(TAG, "getInstalledPackagesAll: $userId")
packages.addAll(getInstalledPackagesAsUser(flags, userId))
}
return packages
}
private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List<PackageInfo> {
return try { return try {
val pm = packageManager val pm = packageManager
val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod( val m = pm.javaClass.getDeclaredMethod(
"getInstalledPackagesAsUser", "getInstalledPackagesAsUser",
Int::class.java, Int::class.java,
Int::class.java Int::class.java
) )
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
getInstalledPackagesAsUser.invoke(pm, flags, userId) as List<PackageInfo> m.invoke(pm, 0, userId) as List<PackageInfo>
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, "err", e) Log.e(TAG, "getInstalledPackagesAsUser", e)
ArrayList() emptyList()
}
}
private fun UserHandle.getUserIdCompat(): Int {
return try {
javaClass.getDeclaredField("identifier").apply { isAccessible = true }.getInt(this)
} catch (_: NoSuchFieldException) {
javaClass.getDeclaredMethod("getIdentifier").invoke(this) as Int
} catch (e: Throwable) {
Log.e("KsuService", "getUserIdCompat", e)
0
} }
} }
} }

View File

@@ -3,17 +3,18 @@ package com.sukisu.ultra.ui.viewmodel
import android.content.* import android.content.*
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.IBinder import android.os.*
import android.os.Parcelable
import android.os.SystemClock
import android.util.Log import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import java.io.*
import coil.ImageLoader
import coil.disk.DiskCache
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ksuApp
import com.sukisu.ultra.ui.KsuService import com.sukisu.ultra.ui.KsuService
import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.*
import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.Shell
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.Mutex
@@ -26,7 +27,7 @@ import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import com.sukisu.zako.IKsuInterface
// 应用分类 // 应用分类
enum class AppCategory(val displayNameRes: Int, val persistKey: String) { enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
@@ -143,8 +144,6 @@ class SuperUserViewModel : ViewModel() {
// 加载进度状态 // 加载进度状态
var loadingProgress by mutableFloatStateOf(0f) var loadingProgress by mutableFloatStateOf(0f)
private set private set
var loadingMessage by mutableStateOf("")
private set
/** /**
* 从SharedPreferences加载显示系统应用设置 * 从SharedPreferences加载显示系统应用设置
@@ -437,56 +436,44 @@ class SuperUserViewModel : ViewModel() {
isRefreshing = true isRefreshing = true
loadingProgress = 0f loadingProgress = 0f
val result = connectKsuService { val binder = connectKsuService() ?: run {
Log.w(TAG, "KsuService disconnected") isRefreshing = false; return
}
if (result == null) {
Log.e(TAG, "Failed to connect to KsuService")
isRefreshing = false
return
} }
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
val start = SystemClock.elapsedRealtime() val allPackages = IKsuInterface.Stub.asInterface(binder)
val total = allPackages.packageCount
val pageSize = 100
val result = mutableListOf<AppInfo>()
try { var start = 0
val service = KsuService.Stub.asInterface(result) while (start < total) {
val allPackages = service?.getPackages(0) val page = allPackages.getPackages(start, pageSize)
if (page.isEmpty()) break
withContext(Dispatchers.Main) { result += page.mapNotNull { packageInfo ->
stopKsuService() packageInfo.applicationInfo?.let { appInfo ->
AppInfo(
label = appInfo.loadLabel(pm).toString(),
packageInfo = packageInfo,
profile = Natives.getAppProfile(packageInfo.packageName, appInfo.uid)
)
}
} }
loadingProgress = 0.3f
val packages = allPackages?.list ?: emptyList() start += page.size
loadingProgress = start.toFloat() / total
apps = packages.map { packageInfo ->
val appInfo = packageInfo.applicationInfo!!
val uid = appInfo.uid
val profile = Natives.getAppProfile(packageInfo.packageName, uid)
AppInfo(
label = appInfo.loadLabel(pm).toString(),
packageInfo = packageInfo,
profile = profile,
)
}.filter { it.packageName != ksuApp.packageName }
loadingProgress = 1f
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}")
} catch (e: Exception) {
Log.e(TAG, "Error fetching app list", e)
withContext(Dispatchers.Main) {
stopKsuService()
}
} finally {
isRefreshing = false
loadingProgress = 0f
loadingMessage = ""
} }
stopKsuService()
appListMutex.withLock {
apps = result.filter { it.packageName != ksuApp.packageName }
}
loadingProgress = 1f
} }
isRefreshing = false
} }
/** /**
* 清理资源 * 清理资源

View File

@@ -1,7 +1,6 @@
package com.sukisu.ultra.ui.webui package com.sukisu.ultra.ui.webui
import android.content.ServiceConnection import android.content.ServiceConnection
import android.content.pm.PackageInfo
import android.util.Log import android.util.Log
import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.Platform
import com.dergoogler.mmrl.platform.model.IProvider import com.dergoogler.mmrl.platform.model.IProvider
@@ -54,19 +53,4 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) {
Log.e("KsuLibSu", "Failed to initialize platform", e) Log.e("KsuLibSu", "Failed to initialize platform", e)
return@withContext false return@withContext false
} }
} }
fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List<PackageInfo> =
try {
val packages = mutableListOf<PackageInfo>()
val userInfos = userManager.getUsers()
for (userInfo in userInfos) {
packages.addAll(packageManager.getInstalledPackages(0, userInfo.id))
}
packages
} catch (e: Exception) {
catch(e)
packageManager.getInstalledPackages(0, userManager.myUserId)
}