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.
This commit is contained in:
ShirkNeko
2025-07-02 01:37:37 +08:00
parent 3729c22dd0
commit acb7cfff1b
2 changed files with 227 additions and 67 deletions

View File

@@ -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<PackageInfo>
}
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<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 getPackages(flags: Int): ParcelableListSlice<PackageInfo> {
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<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 {
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<PackageInfo>
} catch (e: Throwable) {
Log.e(TAG, "err", e)
ArrayList()
}
}
}

View File

@@ -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<List<AppInfo>>(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) {