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:
141
manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt
Normal file
141
manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.sukisu.ultra.ui.viewmodel
|
package com.sukisu.ultra.ui.viewmodel
|
||||||
|
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
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.Parcelable
|
import android.os.Parcelable
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@@ -29,13 +33,13 @@ import java.util.*
|
|||||||
import java.util.concurrent.ThreadPoolExecutor
|
import java.util.concurrent.ThreadPoolExecutor
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
import java.util.concurrent.LinkedBlockingQueue
|
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 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 kotlinx.coroutines.asCoroutineDispatcher
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
// 应用分类
|
// 应用分类
|
||||||
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
|
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.
|
* @date 2025/5/31.
|
||||||
*/
|
*/
|
||||||
class SuperUserViewModel : ViewModel() {
|
class SuperUserViewModel : ViewModel() {
|
||||||
val isPlatformAlive get() = Platform.isAlive
|
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SuperUserViewModel"
|
private const val TAG = "SuperUserViewModel"
|
||||||
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
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() {
|
suspend fun fetchAppList() {
|
||||||
isRefreshing = true
|
isRefreshing = true
|
||||||
loadingProgress = 0f
|
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) {
|
withContext(Dispatchers.IO) {
|
||||||
withTimeoutOrNull(TIMEOUT_MILLIS) {
|
|
||||||
while (!isPlatformAlive) {
|
|
||||||
delay(500)
|
|
||||||
}
|
|
||||||
} ?: return@withContext // Exit early if timeout
|
|
||||||
val pm = ksuApp.packageManager
|
val pm = ksuApp.packageManager
|
||||||
val start = SystemClock.elapsedRealtime()
|
val start = SystemClock.elapsedRealtime()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val packages = Platform.getInstalledPackagesAll {
|
val service = KsuService.Stub.asInterface(result)
|
||||||
Log.e(TAG, "getInstalledPackagesAll:", it)
|
val allPackages = service?.getPackages(0)
|
||||||
}
|
|
||||||
|
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopKsuService()
|
||||||
|
}
|
||||||
loadingProgress = 0.3f
|
loadingProgress = 0.3f
|
||||||
|
|
||||||
val filteredPackages = packages.filter { it.packageName != ksuApp.packageName }
|
val packages = allPackages?.list ?: emptyList()
|
||||||
|
|
||||||
withContext(appProcessingThreadPool) {
|
apps = packages.map { packageInfo ->
|
||||||
supervisorScope {
|
val appInfo = packageInfo.applicationInfo!!
|
||||||
val batches = filteredPackages.chunked(BATCH_SIZE)
|
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 ->
|
loadingProgress = 1f
|
||||||
async {
|
|
||||||
val batchResult = batch.mapNotNull { packageInfo ->
|
|
||||||
try {
|
|
||||||
val appInfo = packageInfo.applicationInfo!!
|
|
||||||
val uid = appInfo.uid
|
|
||||||
|
|
||||||
val labelDeferred = async {
|
Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}")
|
||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Error fetching app list", e)
|
Log.e(TAG, "Error fetching app list", e)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
stopKsuService()
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isRefreshing = false
|
isRefreshing = false
|
||||||
loadingProgress = 0f
|
loadingProgress = 0f
|
||||||
@@ -482,6 +500,7 @@ class SuperUserViewModel : ViewModel() {
|
|||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
try {
|
try {
|
||||||
|
stopKsuService()
|
||||||
appProcessingThreadPool.close()
|
appProcessingThreadPool.close()
|
||||||
configChangeListeners.clear()
|
configChangeListeners.clear()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
Reference in New Issue
Block a user