manager: Reuse the aidi reflection to retrieve the list of applications
This commit is contained in:
@@ -51,6 +51,7 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
|
aidl = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
compose = true
|
compose = true
|
||||||
prefab = true
|
prefab = true
|
||||||
|
|||||||
4
manager/app/proguard-rules.pro
vendored
4
manager/app/proguard-rules.pro
vendored
@@ -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.** { *; }
|
||||||
10
manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl
Normal file
10
manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl
Normal 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);
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* 清理资源
|
* 清理资源
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user