From acb7cfff1b2124a1c989ce810e750b7b3fcfc47f Mon Sep 17 00:00:00 2001 From: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com> Date: Wed, 2 Jul 2025 01:37:37 +0800 Subject: [PATCH] Revert: Rollback some of the changes in "Add option to use WebUI X" and refactor the KsuService - Solve the problem that SuperUser is not available when opening SU compatibility. --- .../java/com/sukisu/ultra/ui/KsuService.kt | 141 ++++++++++++++++ .../ultra/ui/viewmodel/SuperUserViewModel.kt | 153 ++++++++++-------- 2 files changed, 227 insertions(+), 67 deletions(-) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt 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 new file mode 100644 index 00000000..ed42b22b --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt @@ -0,0 +1,141 @@ +package com.sukisu.ultra.ui + +import android.content.Intent +import android.content.pm.PackageInfo +import android.os.Binder +import android.os.IBinder +import android.os.IInterface +import android.os.Parcel +import android.os.UserManager +import android.util.Log +import com.topjohnwu.superuser.ipc.RootService +import rikka.parcelablelist.ParcelableListSlice +import java.lang.reflect.Method + +/** + * @author ShirkNeko + * @date 2025/7/2. + */ +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 + } + + interface IKsuInterface : IInterface { + fun getPackages(flags: Int): ParcelableListSlice + } + + 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!!, android.os.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 + } + } + + 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 { + 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 { + return try { + val pm = packageManager + val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod( + "getInstalledPackagesAsUser", + Int::class.java, + Int::class.java + ) + @Suppress("UNCHECKED_CAST") + getInstalledPackagesAsUser.invoke(pm, flags, userId) as List + } catch (e: Throwable) { + Log.e(TAG, "err", e) + ArrayList() + } + } +} \ 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 f7df235f..f6734394 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 @@ -1,9 +1,13 @@ package com.sukisu.ultra.ui.viewmodel +import android.content.ComponentName import android.content.Context +import android.content.Intent +import android.content.ServiceConnection import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo +import android.os.IBinder import android.os.Parcelable import android.os.SystemClock import android.util.Log @@ -29,13 +33,13 @@ import java.util.* import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import java.util.concurrent.LinkedBlockingQueue -import com.dergoogler.mmrl.platform.Platform -import com.dergoogler.mmrl.platform.TIMEOUT_MILLIS -import com.sukisu.ultra.ui.webui.getInstalledPackagesAll -import kotlinx.coroutines.delay -import kotlinx.coroutines.withTimeoutOrNull import androidx.core.content.edit +import com.sukisu.ultra.ui.KsuService +import com.sukisu.ultra.ui.util.KsuCli +import com.topjohnwu.superuser.Shell import kotlinx.coroutines.asCoroutineDispatcher +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine // 应用分类 enum class AppCategory(val displayNameRes: Int, val persistKey: String) { @@ -73,8 +77,6 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) { * @date 2025/5/31. */ class SuperUserViewModel : ViewModel() { - val isPlatformAlive get() = Platform.isAlive - companion object { private const val TAG = "SuperUserViewModel" var apps by mutableStateOf>(emptyList()) @@ -392,83 +394,99 @@ class SuperUserViewModel : ViewModel() { } } + private var serviceConnection: ServiceConnection? = null + + private suspend fun connectKsuService( + onDisconnect: () -> Unit = {} + ): IBinder? = suspendCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnect() + serviceConnection = null + } + + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + continuation.resume(binder) + } + } + + serviceConnection = connection + val intent = Intent(ksuApp, KsuService::class.java) + + try { + val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( + intent, + Shell.EXECUTOR, + connection + ) + val shell = KsuCli.SHELL + task?.let { shell.execTask(it) } + } catch (e: Exception) { + Log.e(TAG, "Failed to bind KsuService", e) + continuation.resume(null) + } + } + + private fun stopKsuService() { + serviceConnection?.let { connection -> + try { + val intent = Intent(ksuApp, KsuService::class.java) + com.topjohnwu.superuser.ipc.RootService.stop(intent) + serviceConnection = null + } catch (e: Exception) { + Log.e(TAG, "Failed to stop KsuService", e) + } + } + } + suspend fun fetchAppList() { 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 + } + withContext(Dispatchers.IO) { - withTimeoutOrNull(TIMEOUT_MILLIS) { - while (!isPlatformAlive) { - delay(500) - } - } ?: return@withContext // Exit early if timeout val pm = ksuApp.packageManager val start = SystemClock.elapsedRealtime() try { - val packages = Platform.getInstalledPackagesAll { - Log.e(TAG, "getInstalledPackagesAll:", it) - } + val service = KsuService.Stub.asInterface(result) + val allPackages = service?.getPackages(0) + withContext(Dispatchers.Main) { + stopKsuService() + } loadingProgress = 0.3f - val filteredPackages = packages.filter { it.packageName != ksuApp.packageName } + val packages = allPackages?.list ?: emptyList() - withContext(appProcessingThreadPool) { - supervisorScope { - val batches = filteredPackages.chunked(BATCH_SIZE) + 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 } - val processedApps = batches.mapIndexed { batchIndex, batch -> - async { - val batchResult = batch.mapNotNull { packageInfo -> - try { - val appInfo = packageInfo.applicationInfo!! - val uid = appInfo.uid + loadingProgress = 1f - val labelDeferred = async { - appInfo.loadLabel(pm).toString() - } - val profileDeferred = async { - Natives.getAppProfile(packageInfo.packageName, uid) - } - - val label = labelDeferred.await() - val profile = profileDeferred.await() - - AppInfo( - label = label, - packageInfo = packageInfo, - profile = profile, - ) - } catch (e: Exception) { - Log.e( - TAG, - "Error processing app ${packageInfo.packageName}", - e - ) - null - } - } - - val progress = 0.3f + (batchIndex + 1).toFloat() / batches.size * 0.6f - loadingProgress = progress - - batchResult - } - }.awaitAll().flatten() - - appListMutex.withLock { - apps = processedApps - } - - loadingProgress = 1f - - val elapsed = SystemClock.elapsedRealtime() - start - Log.i(TAG, "Loaded ${processedApps.size} apps in ${elapsed}ms using concurrent processing") - } - } + 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 @@ -482,6 +500,7 @@ class SuperUserViewModel : ViewModel() { override fun onCleared() { super.onCleared() try { + stopKsuService() appProcessingThreadPool.close() configChangeListeners.clear() } catch (e: Exception) {