manager: display the same UID as a group

Co-authored-by: YuKongA <70465933+YuKongA@users.noreply.github.com>
Co-authored-by: ShirkNeko <109797057+ShirkNeko@users.noreply.github.com>
This commit is contained in:
ShirkNeko
2025-11-15 23:44:38 +08:00
parent 35b02e3c73
commit 684a5d1ccd
6 changed files with 626 additions and 735 deletions

View File

@@ -6,6 +6,7 @@
#include <android/log.h> #include <android/log.h>
#include <string.h> #include <string.h>
#include <linux/capability.h> #include <linux/capability.h>
#include <pwd.h>
NativeBridgeNP(getVersion, jint) { NativeBridgeNP(getVersion, jint) {
uint32_t version = get_version(); uint32_t version = get_version();
@@ -318,6 +319,14 @@ NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean enabled) {
return set_enhanced_security_enabled(enabled); return set_enhanced_security_enabled(enabled);
} }
NativeBridge(getUserName, jstring, jint uid) {
struct passwd *pw = getpwuid((uid_t) uid);
if (pw && pw->pw_name && pw->pw_name[0] != '\0') {
return GetEnvironment()->NewStringUTF(env, pw->pw_name);
}
return NULL;
}
// Check if KPM is enabled // Check if KPM is enabled
NativeBridgeNP(isKPMEnabled, jboolean) { NativeBridgeNP(isKPMEnabled, jboolean) {
return is_KPM_enable(); return is_KPM_enable();

View File

@@ -176,6 +176,8 @@ object Natives {
*/ */
external fun clearUidScannerEnvironment(): Boolean external fun clearUidScannerEnvironment(): Boolean
external fun getUserName(uid: Int): String?
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
private const val NOBODY_UID = 9999 private const val NOBODY_UID = 9999

View File

@@ -21,7 +21,6 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.sukisu.ultra.R import com.sukisu.ultra.R
// 菜单项数据类
data class FabMenuItem( data class FabMenuItem(
val icon: ImageVector, val icon: ImageVector,
val labelRes: Int, val labelRes: Int,
@@ -29,7 +28,6 @@ data class FabMenuItem(
val onClick: () -> Unit val onClick: () -> Unit
) )
// 动画配置
object FabAnimationConfig { object FabAnimationConfig {
const val ANIMATION_DURATION = 300 const val ANIMATION_DURATION = 300
const val STAGGER_DELAY = 50 const val STAGGER_DELAY = 50
@@ -53,23 +51,15 @@ fun VerticalExpandableFab(
) { ) {
var isExpanded by remember { mutableStateOf(false) } var isExpanded by remember { mutableStateOf(false) }
// 主按钮旋转动画
val rotationAngle by animateFloatAsState( val rotationAngle by animateFloatAsState(
targetValue = if (isExpanded) 45f else 0f, targetValue = if (isExpanded) 45f else 0f,
animationSpec = tween( animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
label = "mainButtonRotation" label = "mainButtonRotation"
) )
// 主按钮缩放动画
val mainButtonScale by animateFloatAsState( val mainButtonScale by animateFloatAsState(
targetValue = if (isExpanded) 1.1f else 1f, targetValue = if (isExpanded) 1.1f else 1f,
animationSpec = tween( animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
durationMillis = animationDurationMs,
easing = FastOutSlowInEasing
),
label = "mainButtonScale" label = "mainButtonScale"
) )
@@ -77,14 +67,9 @@ fun VerticalExpandableFab(
modifier = modifier.wrapContentSize(), modifier = modifier.wrapContentSize(),
contentAlignment = Alignment.BottomEnd contentAlignment = Alignment.BottomEnd
) { ) {
// 子菜单按钮
menuItems.forEachIndexed { index, menuItem -> menuItems.forEachIndexed { index, menuItem ->
val animatedOffsetY by animateFloatAsState( val animatedOffsetY by animateFloatAsState(
targetValue = if (isExpanded) { targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f,
-(buttonSpacing.value * (index + 1))
} else {
0f
},
animationSpec = tween( animationSpec = tween(
durationMillis = animationDurationMs, durationMillis = animationDurationMs,
delayMillis = if (isExpanded) { delayMillis = if (isExpanded) {
@@ -125,7 +110,6 @@ fun VerticalExpandableFab(
label = "fabAlpha$index" label = "fabAlpha$index"
) )
// 子按钮容器(包含标签)
Row( Row(
modifier = Modifier modifier = Modifier
.offset(y = animatedOffsetY.dp) .offset(y = animatedOffsetY.dp)
@@ -134,7 +118,6 @@ fun VerticalExpandableFab(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.End horizontalArrangement = Arrangement.End
) { ) {
// 标签
AnimatedVisibility( AnimatedVisibility(
visible = isExpanded && animatedScale > 0.5f, visible = isExpanded && animatedScale > 0.5f,
enter = slideInHorizontally( enter = slideInHorizontally(
@@ -161,7 +144,6 @@ fun VerticalExpandableFab(
} }
} }
// 子按钮
SmallFloatingActionButton( SmallFloatingActionButton(
onClick = { onClick = {
menuItem.onClick() menuItem.onClick()
@@ -193,15 +175,12 @@ fun VerticalExpandableFab(
} }
} }
// 主按钮
FloatingActionButton( FloatingActionButton(
onClick = { onClick = {
onMainButtonClick?.invoke() onMainButtonClick?.invoke()
isExpanded = !isExpanded isExpanded = !isExpanded
}, },
modifier = Modifier modifier = Modifier.size(buttonSize).scale(mainButtonScale),
.size(buttonSize)
.scale(mainButtonScale),
elevation = FloatingActionButtonDefaults.elevation( elevation = FloatingActionButtonDefaults.elevation(
defaultElevation = 6.dp, defaultElevation = 6.dp,
pressedElevation = 8.dp, pressedElevation = 8.dp,
@@ -221,7 +200,6 @@ fun VerticalExpandableFab(
} }
} }
// 预设菜单项
object FabMenuPresets { object FabMenuPresets {
fun getScrollMenuItems( fun getScrollMenuItems(
onScrollToTop: () -> Unit, onScrollToTop: () -> Unit,

View File

@@ -3,10 +3,12 @@ package com.sukisu.ultra.ui.screen
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.widget.Toast import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -17,10 +19,13 @@ import androidx.compose.material3.*
import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.getSystemService import androidx.core.content.getSystemService
import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.compose.dropUnlessResumed
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
@@ -254,3 +259,24 @@ private fun TopBar(
scrollBehavior = scrollBehavior scrollBehavior = scrollBehavior
) )
} }
@Composable
fun LabelText(label: String) {
Box(
modifier = Modifier
.padding(top = 4.dp, end = 4.dp)
.background(
Color.Black,
shape = RoundedCornerShape(4.dp)
)
) {
Text(
text = label,
modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp),
style = TextStyle(
fontSize = 8.sp,
color = Color.White,
)
)
}
}

View File

@@ -4,12 +4,12 @@ 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.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.os.* import android.os.IBinder
import android.os.Parcelable
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 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
@@ -27,7 +27,7 @@ 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 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"),
ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"), ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"),
@@ -35,13 +35,10 @@ enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT"); DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT");
companion object { companion object {
fun fromPersistKey(key: String): AppCategory { fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL
return entries.find { it.persistKey == key } ?: ALL
}
} }
} }
// 排序方式
enum class SortType(val displayNameRes: Int, val persistKey: String) { enum class SortType(val displayNameRes: Int, val persistKey: String) {
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"), NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"), NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"),
@@ -52,28 +49,24 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) {
USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ"); USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ");
companion object { companion object {
fun fromPersistKey(key: String): SortType { fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC
return entries.find { it.persistKey == key } ?: NAME_ASC
}
} }
} }
/**
* @author ShirkNeko
* @date 2025/5/31.
*/
class SuperUserViewModel : ViewModel() { class SuperUserViewModel : ViewModel() {
companion object { companion object {
private const val TAG = "SuperUserViewModel" private const val TAG = "SuperUserViewModel"
private val appsLock = Any() private val appsLock = Any()
var apps by mutableStateOf<List<AppInfo>>(emptyList()) var apps by mutableStateOf<List<AppInfo>>(emptyList())
var appGroups by mutableStateOf<List<AppGroup>>(emptyList())
@JvmStatic @JvmStatic
fun getAppIconDrawable(context: Context, packageName: String): Drawable? { fun getAppIconDrawable(context: Context, packageName: String): Drawable? {
val appList = synchronized(appsLock) { apps } val appList = synchronized(appsLock) { apps }
val appDetail = appList.find { it.packageName == packageName } return appList.find { it.packageName == packageName }
return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) ?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
} }
private const val PREFS_NAME = "settings" private const val PREFS_NAME = "settings"
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
private const val KEY_SELECTED_CATEGORY = "selected_category" private const val KEY_SELECTED_CATEGORY = "selected_category"
@@ -90,31 +83,34 @@ class SuperUserViewModel : ViewModel() {
val packageInfo: PackageInfo, val packageInfo: PackageInfo,
val profile: Natives.Profile?, val profile: Natives.Profile?,
) : Parcelable { ) : Parcelable {
val packageName: String val packageName: String get() = packageInfo.packageName
get() = packageInfo.packageName val uid: Int get() = packageInfo.applicationInfo!!.uid
val uid: Int val allowSu: Boolean get() = profile?.allowSu == true
get() = packageInfo.applicationInfo!!.uid
val allowSu: Boolean
get() = profile != null && profile.allowSu
val hasCustomProfile: Boolean val hasCustomProfile: Boolean
get() { get() = profile?.let {
if (profile == null) { if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault
return false } ?: false
}
return if (profile.allowSu) {
!profile.rootUseDefault
} else {
!profile.nonRootUseDefault
}
} }
@Parcelize
data class AppGroup(
val uid: Int,
val apps: List<AppInfo>,
val profile: Natives.Profile?
) : Parcelable {
val mainApp: AppInfo get() = apps.first()
val packageNames: List<String> get() = apps.map { it.packageName }
val allowSu: Boolean get() = profile?.allowSu == true
val userName: String? get() = Natives.getUserName(uid)
val hasCustomProfile: Boolean
get() = profile?.let {
if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault
} ?: false
} }
private val appProcessingThreadPool = ThreadPoolExecutor( private val appProcessingThreadPool = ThreadPoolExecutor(
CORE_POOL_SIZE, CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
LinkedBlockingQueue() LinkedBlockingQueue()
) { runnable -> ) { runnable ->
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply { Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
@@ -124,63 +120,40 @@ class SuperUserViewModel : ViewModel() {
}.asCoroutineDispatcher() }.asCoroutineDispatcher()
private val appListMutex = Mutex() private val appListMutex = Mutex()
private val configChangeListeners = mutableSetOf<(String) -> Unit>() private val configChangeListeners = mutableSetOf<(String) -> Unit>()
private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
var search by mutableStateOf("") var search by mutableStateOf("")
var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false))
var showSystemApps by mutableStateOf(loadShowSystemApps())
private set private set
var selectedCategory by mutableStateOf(loadSelectedCategory()) var selectedCategory by mutableStateOf(loadSelectedCategory())
private set private set
var currentSortType by mutableStateOf(loadCurrentSortType()) var currentSortType by mutableStateOf(loadCurrentSortType())
private set private set
var isRefreshing by mutableStateOf(false) var isRefreshing by mutableStateOf(false)
private set private set
// 批量操作相关状态
var showBatchActions by mutableStateOf(false) var showBatchActions by mutableStateOf(false)
internal set internal set
var selectedApps by mutableStateOf<Set<String>>(emptySet()) var selectedApps by mutableStateOf<Set<String>>(emptySet())
internal set internal set
// 加载进度状态
var loadingProgress by mutableFloatStateOf(0f) var loadingProgress by mutableFloatStateOf(0f)
private set private set
/**
* 从SharedPreferences加载显示系统应用设置
*/
private fun loadShowSystemApps(): Boolean {
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
}
/**
* 从SharedPreferences加载选择的应用分类
*/
private fun loadSelectedCategory(): AppCategory { private fun loadSelectedCategory(): AppCategory {
val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey)
?: AppCategory.ALL.persistKey
return AppCategory.fromPersistKey(categoryKey) return AppCategory.fromPersistKey(categoryKey)
} }
/**
* 从SharedPreferences加载当前排序方式
*/
private fun loadCurrentSortType(): SortType { private fun loadCurrentSortType(): SortType {
val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey)
?: SortType.NAME_ASC.persistKey
return SortType.fromPersistKey(sortKey) return SortType.fromPersistKey(sortKey)
} }
/**
* 更新显示系统应用设置并保存到SharedPreferences
*/
fun updateShowSystemApps(newValue: Boolean) { fun updateShowSystemApps(newValue: Boolean) {
showSystemApps = newValue showSystemApps = newValue
saveShowSystemApps(newValue) prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) }
notifyAppListChanged() notifyAppListChanged()
} }
@@ -190,50 +163,14 @@ class SuperUserViewModel : ViewModel() {
apps = currentApps apps = currentApps
} }
/**
* 更新选择的应用分类并保存到SharedPreferences
*/
fun updateSelectedCategory(newCategory: AppCategory) { fun updateSelectedCategory(newCategory: AppCategory) {
selectedCategory = newCategory selectedCategory = newCategory
saveSelectedCategory(newCategory) prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) }
} }
/**
* 更新当前排序方式并保存到SharedPreferences
*/
fun updateCurrentSortType(newSortType: SortType) { fun updateCurrentSortType(newSortType: SortType) {
currentSortType = newSortType currentSortType = newSortType
saveCurrentSortType(newSortType) prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) }
}
/**
* 保存显示系统应用设置到SharedPreferences
*/
private fun saveShowSystemApps(value: Boolean) {
prefs.edit {
putBoolean(KEY_SHOW_SYSTEM_APPS, value)
}
Log.d(TAG, "Saved show system apps: $value")
}
/**
* 保存选择的应用分类到SharedPreferences
*/
private fun saveSelectedCategory(category: AppCategory) {
prefs.edit {
putString(KEY_SELECTED_CATEGORY, category.persistKey)
}
Log.d(TAG, "Saved selected category: ${category.persistKey}")
}
/**
* 保存当前排序方式到SharedPreferences
*/
private fun saveCurrentSortType(sortType: SortType) {
prefs.edit {
putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey)
}
Log.d(TAG, "Saved current sort type: ${sortType.persistKey}")
} }
private val sortedList by derivedStateOf { private val sortedList by derivedStateOf {
@@ -244,34 +181,25 @@ class SuperUserViewModel : ViewModel() {
else -> 2 else -> 2
} }
}.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
apps.sortedWith(comparator).also { apps.sortedWith(comparator).also { isRefreshing = false }
isRefreshing = false
}
} }
val appList by derivedStateOf { val appList by derivedStateOf {
val filtered = sortedList.filter { sortedList.filter {
it.label.contains(search, true) || it.packageName.contains( it.label.contains(search, true) ||
search, it.packageName.contains(search, true) ||
true HanziToPinyin.getInstance().toPinyinString(it.label).contains(search, true)
) || HanziToPinyin.getInstance()
.toPinyinString(it.label).contains(search, true)
}.filter { }.filter {
it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 it.uid == 2000 || showSystemApps ||
it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0
}
} }
filtered
}
// 切换批量操作模式
fun toggleBatchMode() { fun toggleBatchMode() {
showBatchActions = !showBatchActions showBatchActions = !showBatchActions
if (!showBatchActions) { if (!showBatchActions) clearSelection()
clearSelection()
}
} }
// 切换应用选择状态
fun toggleAppSelection(packageName: String) { fun toggleAppSelection(packageName: String) {
selectedApps = if (selectedApps.contains(packageName)) { selectedApps = if (selectedApps.contains(packageName)) {
selectedApps - packageName selectedApps - packageName
@@ -280,35 +208,14 @@ class SuperUserViewModel : ViewModel() {
} }
} }
// 清除所有选择
fun clearSelection() { fun clearSelection() {
selectedApps = emptySet() selectedApps = emptySet()
} }
// 批量更新权限
suspend fun updateBatchPermissions(allowSu: Boolean) {
selectedApps.forEach { packageName ->
val app = apps.find { it.packageName == packageName }
app?.let {
val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy(allowSu = allowSu)
if (Natives.setAppProfile(updatedProfile)) {
updateAppProfileLocally(packageName, updatedProfile)
notifyConfigChange(packageName)
}
}
}
clearSelection()
showBatchActions = false
refreshAppConfigurations()
}
// 批量更新权限和umount模块设置
suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) { suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) {
selectedApps.forEach { packageName -> selectedApps.forEach { packageName ->
val app = apps.find { it.packageName == packageName } apps.find { it.packageName == packageName }?.let { app ->
app?.let { val profile = Natives.getAppProfile(packageName, app.uid)
val profile = Natives.getAppProfile(packageName, it.uid)
val updatedProfile = profile.copy( val updatedProfile = profile.copy(
allowSu = allowSu, allowSu = allowSu,
umountModules = umountModules ?: profile.umountModules, umountModules = umountModules ?: profile.umountModules,
@@ -325,7 +232,6 @@ class SuperUserViewModel : ViewModel() {
refreshAppConfigurations() refreshAppConfigurations()
} }
// 更新本地应用配置
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
appListMutex.tryLock().let { locked -> appListMutex.tryLock().let { locked ->
if (locked) { if (locked) {
@@ -333,9 +239,7 @@ class SuperUserViewModel : ViewModel() {
apps = apps.map { app -> apps = apps.map { app ->
if (app.packageName == packageName) { if (app.packageName == packageName) {
app.copy(profile = updatedProfile) app.copy(profile = updatedProfile)
} else { } else app
app
}
} }
} finally { } finally {
appListMutex.unlock() appListMutex.unlock()
@@ -354,15 +258,11 @@ class SuperUserViewModel : ViewModel() {
} }
} }
/**
* 刷新应用配置状态
*/
suspend fun refreshAppConfigurations() { suspend fun refreshAppConfigurations() {
withContext(appProcessingThreadPool) { withContext(appProcessingThreadPool) {
supervisorScope { supervisorScope {
val currentApps = apps.toList() val currentApps = apps.toList()
val batches = currentApps.chunked(BATCH_SIZE) val batches = currentApps.chunked(BATCH_SIZE)
loadingProgress = 0f loadingProgress = 0f
val updatedApps = batches.mapIndexed { batchIndex, batch -> val updatedApps = batches.mapIndexed { batchIndex, batch ->
@@ -376,49 +276,35 @@ class SuperUserViewModel : ViewModel() {
app app
} }
} }
loadingProgress = (batchIndex + 1).toFloat() / batches.size
val progress = (batchIndex + 1).toFloat() / batches.size
loadingProgress = progress
batchResult batchResult
} }
}.awaitAll().flatten() }.awaitAll().flatten()
appListMutex.withLock { appListMutex.withLock { apps = updatedApps }
apps = updatedApps
}
loadingProgress = 1f loadingProgress = 1f
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
} }
} }
} }
private var serviceConnection: ServiceConnection? = null private var serviceConnection: ServiceConnection? = null
private suspend fun connectKsuService( private suspend fun connectKsuService(onDisconnect: () -> Unit = {}): IBinder? =
onDisconnect: () -> Unit = {} suspendCoroutine { continuation ->
): IBinder? = suspendCoroutine { continuation ->
val connection = object : ServiceConnection { val connection = object : ServiceConnection {
override fun onServiceDisconnected(name: ComponentName?) { override fun onServiceDisconnected(name: ComponentName?) {
onDisconnect() onDisconnect()
serviceConnection = null serviceConnection = null
} }
override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
continuation.resume(binder) continuation.resume(binder)
} }
} }
serviceConnection = connection serviceConnection = connection
val intent = Intent(ksuApp, KsuService::class.java) val intent = Intent(ksuApp, KsuService::class.java)
try { try {
val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask(
intent, intent, Shell.EXECUTOR, connection
Shell.EXECUTOR,
connection
) )
task?.let { Shell.getShell().execTask(it) } task?.let { Shell.getShell().execTask(it) }
} catch (e: Exception) { } catch (e: Exception) {
@@ -428,7 +314,7 @@ class SuperUserViewModel : ViewModel() {
} }
private fun stopKsuService() { private fun stopKsuService() {
serviceConnection?.let { _ -> serviceConnection?.let {
try { try {
val intent = Intent(ksuApp, KsuService::class.java) val intent = Intent(ksuApp, KsuService::class.java)
com.topjohnwu.superuser.ipc.RootService.stop(intent) com.topjohnwu.superuser.ipc.RootService.stop(intent)
@@ -443,9 +329,7 @@ class SuperUserViewModel : ViewModel() {
isRefreshing = true isRefreshing = true
loadingProgress = 0f loadingProgress = 0f
val binder = connectKsuService() ?: run { val binder = connectKsuService() ?: run { isRefreshing = false; return }
isRefreshing = false; return
}
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val pm = ksuApp.packageManager val pm = ksuApp.packageManager
@@ -468,27 +352,54 @@ class SuperUserViewModel : ViewModel() {
) )
} }
} }
start += page.size start += page.size
loadingProgress = start.toFloat() / total loadingProgress = start.toFloat() / total
} }
synchronized(appsLock) {
apps
}
stopKsuService() stopKsuService()
appListMutex.withLock { appListMutex.withLock {
apps = result.filter { it.packageName != ksuApp.packageName } val filteredApps = result.filter { it.packageName != ksuApp.packageName }
apps = filteredApps
appGroups = groupAppsByUid(filteredApps)
} }
loadingProgress = 1f loadingProgress = 1f
} }
isRefreshing = false isRefreshing = false
} }
/**
* 清理资源 val appGroupList by derivedStateOf {
*/ appGroups.filter { group ->
group.apps.any { app ->
app.label.contains(search, true) ||
app.packageName.contains(search, true) ||
HanziToPinyin.getInstance().toPinyinString(app.label).contains(search, true)
}
}.filter { group ->
group.uid == 2000 || showSystemApps ||
group.apps.any { it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 }
}
}
private fun groupAppsByUid(appList: List<AppInfo>): List<AppGroup> {
return appList.groupBy { it.uid }
.map { (uid, apps) ->
val sortedApps = apps.sortedBy { it.label }
val profile = apps.firstOrNull()?.let { Natives.getAppProfile(it.packageName, uid) }
AppGroup(uid = uid, apps = sortedApps, profile = profile)
}
.sortedWith(
compareBy<AppGroup> {
when {
it.allowSu -> 0
it.hasCustomProfile -> 1
else -> 2
}
}.thenBy(Collator.getInstance(Locale.getDefault())) {
it.userName?.takeIf { name -> name.isNotBlank() } ?: it.uid.toString()
}.thenBy(Collator.getInstance(Locale.getDefault())) { it.mainApp.label }
)
}
override fun onCleared() { override fun onCleared() {
super.onCleared() super.onCleared()
try { try {