diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index ee3b591d..14c20f26 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -51,6 +51,7 @@ android { } buildFeatures { + aidl = true buildConfig = true compose = true prefab = true diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 85638057..18c49c15 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -43,4 +43,6 @@ -keep class com.dergoogler.mmrl.webui.interfaces.** { *; } -keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } --keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } \ No newline at end of file +-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } + +-keep interface com.sukisu.zako.** { *; } \ No newline at end of file diff --git a/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl b/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl new file mode 100644 index 00000000..50807e89 --- /dev/null +++ b/manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl @@ -0,0 +1,10 @@ +// IKsuInterface.aidl +package com.sukisu.zako; + +import android.content.pm.PackageInfo; +import java.util.List; + +interface IKsuInterface { + int getPackageCount(); + List getPackages(int start, int maxCount); +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt index 41d0f433..39201e5c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt @@ -1,137 +1,75 @@ package com.sukisu.ultra.ui +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageInfo import android.os.* import android.util.Log import com.topjohnwu.superuser.ipc.RootService -import rikka.parcelablelist.ParcelableListSlice -import java.lang.reflect.Method +import com.sukisu.zako.IKsuInterface /** * @author ShirkNeko - * @date 2025/7/2. + * @date 2025/10/17. */ class KsuService : RootService() { - companion object { - private const val TAG = "KsuService" - private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface" - private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0 + private val TAG = "KsuService" + + private val cacheLock = Object() + private var _all: List? = null + private val allPackages: List + get() = synchronized(cacheLock) { + _all ?: loadAllPackages().also { _all = it } + } + + private fun loadAllPackages(): List { + val tmp = arrayListOf() + for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) { + val userId = user.getUserIdCompat() + tmp += getInstalledPackagesAsUser(userId) + } + return tmp } - interface IKsuInterface : IInterface { - fun getPackages(flags: Int): ParcelableListSlice - } + internal inner class Stub : IKsuInterface.Stub() { + override fun getPackageCount(): Int = allPackages.size - abstract class Stub : Binder(), IKsuInterface { - init { - attachInterface(this, DESCRIPTOR) - } - - 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 { - 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 - } else { - ParcelableListSlice(emptyList()) - } - } finally { - reply.recycle() - data.recycle() - } - } - - override fun asBinder(): IBinder = mRemote + override fun getPackages(start: Int, maxCount: Int): List { + val list = allPackages + val end = (start + maxCount).coerceAtMost(list.size) + return if (start >= list.size) emptyList() + else list.subList(start, end) } } - inner class KsuInterfaceImpl : Stub() { - override fun getPackages(flags: Int): ParcelableListSlice { - val list = getInstalledPackagesAll(flags) - Log.i(TAG, "getPackages: ${list.size}") - return ParcelableListSlice(list) - } - } + override fun onBind(intent: Intent): IBinder = Stub() - override fun onBind(intent: Intent): IBinder { - return KsuInterfaceImpl() - } - - private fun getUserIds(): List { - val result = mutableListOf() - 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 { - val packages = ArrayList() - for (userId in getUserIds()) { - Log.i(TAG, "getInstalledPackagesAll: $userId") - packages.addAll(getInstalledPackagesAsUser(flags, userId)) - } - return packages - } - - private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List { + @SuppressLint("PrivateApi") + private fun getInstalledPackagesAsUser(userId: Int): List { return try { val pm = packageManager - val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod( + val m = pm.javaClass.getDeclaredMethod( "getInstalledPackagesAsUser", Int::class.java, Int::class.java ) @Suppress("UNCHECKED_CAST") - getInstalledPackagesAsUser.invoke(pm, flags, userId) as List + m.invoke(pm, 0, userId) as List } catch (e: Throwable) { - Log.e(TAG, "err", e) - ArrayList() + Log.e(TAG, "getInstalledPackagesAsUser", e) + 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 } } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 0bd03203..88629a06 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -3,17 +3,18 @@ package com.sukisu.ultra.ui.viewmodel import android.content.* import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo -import android.os.IBinder -import android.os.Parcelable -import android.os.SystemClock +import android.os.* import android.util.Log import androidx.compose.runtime.* import androidx.core.content.edit import androidx.lifecycle.ViewModel +import java.io.* +import coil.ImageLoader +import coil.disk.DiskCache import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp 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 kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -26,7 +27,7 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine - +import com.sukisu.zako.IKsuInterface // 应用分类 enum class AppCategory(val displayNameRes: Int, val persistKey: String) { ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), @@ -143,8 +144,6 @@ class SuperUserViewModel : ViewModel() { // 加载进度状态 var loadingProgress by mutableFloatStateOf(0f) private set - var loadingMessage by mutableStateOf("") - private set /** * 从SharedPreferences加载显示系统应用设置 @@ -437,56 +436,44 @@ class SuperUserViewModel : ViewModel() { isRefreshing = true loadingProgress = 0f - val result = connectKsuService { - Log.w(TAG, "KsuService disconnected") - } - - if (result == null) { - Log.e(TAG, "Failed to connect to KsuService") - isRefreshing = false - return + val binder = connectKsuService() ?: run { + isRefreshing = false; return } withContext(Dispatchers.IO) { val pm = ksuApp.packageManager - val start = SystemClock.elapsedRealtime() + val allPackages = IKsuInterface.Stub.asInterface(binder) + val total = allPackages.packageCount + val pageSize = 100 + val result = mutableListOf() - try { - val service = KsuService.Stub.asInterface(result) - val allPackages = service?.getPackages(0) + var start = 0 + while (start < total) { + val page = allPackages.getPackages(start, pageSize) + if (page.isEmpty()) break - withContext(Dispatchers.Main) { - stopKsuService() + result += page.mapNotNull { packageInfo -> + 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() - - 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 = "" + start += page.size + loadingProgress = start.toFloat() / total } + + stopKsuService() + + appListMutex.withLock { + apps = result.filter { it.packageName != ksuApp.packageName } + } + loadingProgress = 1f } + isRefreshing = false } /** * 清理资源 diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt index 7650defe..a0f003a8 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt @@ -1,7 +1,6 @@ package com.sukisu.ultra.ui.webui import android.content.ServiceConnection -import android.content.pm.PackageInfo import android.util.Log import com.dergoogler.mmrl.platform.Platform 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) return@withContext false } -} - -fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List = - try { - val packages = mutableListOf() - 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) - } \ No newline at end of file +} \ No newline at end of file