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:
@@ -6,6 +6,7 @@
|
||||
#include <android/log.h>
|
||||
#include <string.h>
|
||||
#include <linux/capability.h>
|
||||
#include <pwd.h>
|
||||
|
||||
NativeBridgeNP(getVersion, jint) {
|
||||
uint32_t version = get_version();
|
||||
@@ -318,6 +319,14 @@ NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean 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
|
||||
NativeBridgeNP(isKPMEnabled, jboolean) {
|
||||
return is_KPM_enable();
|
||||
|
||||
@@ -176,6 +176,8 @@ object Natives {
|
||||
*/
|
||||
external fun clearUidScannerEnvironment(): Boolean
|
||||
|
||||
external fun getUserName(uid: Int): String?
|
||||
|
||||
private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$"
|
||||
private const val NOBODY_UID = 9999
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.sukisu.ultra.R
|
||||
|
||||
// 菜单项数据类
|
||||
data class FabMenuItem(
|
||||
val icon: ImageVector,
|
||||
val labelRes: Int,
|
||||
@@ -29,7 +28,6 @@ data class FabMenuItem(
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
// 动画配置
|
||||
object FabAnimationConfig {
|
||||
const val ANIMATION_DURATION = 300
|
||||
const val STAGGER_DELAY = 50
|
||||
@@ -53,23 +51,15 @@ fun VerticalExpandableFab(
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
// 主按钮旋转动画
|
||||
val rotationAngle by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 45f else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
|
||||
label = "mainButtonRotation"
|
||||
)
|
||||
|
||||
// 主按钮缩放动画
|
||||
val mainButtonScale by animateFloatAsState(
|
||||
targetValue = if (isExpanded) 1.1f else 1f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
easing = FastOutSlowInEasing
|
||||
),
|
||||
animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing),
|
||||
label = "mainButtonScale"
|
||||
)
|
||||
|
||||
@@ -77,14 +67,9 @@ fun VerticalExpandableFab(
|
||||
modifier = modifier.wrapContentSize(),
|
||||
contentAlignment = Alignment.BottomEnd
|
||||
) {
|
||||
// 子菜单按钮
|
||||
menuItems.forEachIndexed { index, menuItem ->
|
||||
val animatedOffsetY by animateFloatAsState(
|
||||
targetValue = if (isExpanded) {
|
||||
-(buttonSpacing.value * (index + 1))
|
||||
} else {
|
||||
0f
|
||||
},
|
||||
targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f,
|
||||
animationSpec = tween(
|
||||
durationMillis = animationDurationMs,
|
||||
delayMillis = if (isExpanded) {
|
||||
@@ -125,7 +110,6 @@ fun VerticalExpandableFab(
|
||||
label = "fabAlpha$index"
|
||||
)
|
||||
|
||||
// 子按钮容器(包含标签)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.offset(y = animatedOffsetY.dp)
|
||||
@@ -134,7 +118,6 @@ fun VerticalExpandableFab(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.End
|
||||
) {
|
||||
// 标签
|
||||
AnimatedVisibility(
|
||||
visible = isExpanded && animatedScale > 0.5f,
|
||||
enter = slideInHorizontally(
|
||||
@@ -161,7 +144,6 @@ fun VerticalExpandableFab(
|
||||
}
|
||||
}
|
||||
|
||||
// 子按钮
|
||||
SmallFloatingActionButton(
|
||||
onClick = {
|
||||
menuItem.onClick()
|
||||
@@ -193,15 +175,12 @@ fun VerticalExpandableFab(
|
||||
}
|
||||
}
|
||||
|
||||
// 主按钮
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
onMainButtonClick?.invoke()
|
||||
isExpanded = !isExpanded
|
||||
},
|
||||
modifier = Modifier
|
||||
.size(buttonSize)
|
||||
.scale(mainButtonScale),
|
||||
modifier = Modifier.size(buttonSize).scale(mainButtonScale),
|
||||
elevation = FloatingActionButtonDefaults.elevation(
|
||||
defaultElevation = 6.dp,
|
||||
pressedElevation = 8.dp,
|
||||
@@ -221,7 +200,6 @@ fun VerticalExpandableFab(
|
||||
}
|
||||
}
|
||||
|
||||
// 预设菜单项
|
||||
object FabMenuPresets {
|
||||
fun getScrollMenuItems(
|
||||
onScrollToTop: () -> Unit,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,10 +3,12 @@ package com.sukisu.ultra.ui.screen
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.widget.Toast
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.icons.Icons
|
||||
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.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.compose.dropUnlessResumed
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
@@ -253,4 +258,25 @@ private fun TopBar(
|
||||
windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal),
|
||||
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,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import android.content.*
|
||||
import android.content.pm.ApplicationInfo
|
||||
import android.content.pm.PackageInfo
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.*
|
||||
import android.os.IBinder
|
||||
import android.os.Parcelable
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.core.content.edit
|
||||
import androidx.lifecycle.ViewModel
|
||||
import java.io.*
|
||||
import com.sukisu.ultra.Natives
|
||||
import com.sukisu.ultra.ksuApp
|
||||
import com.sukisu.ultra.ui.KsuService
|
||||
@@ -27,7 +27,7 @@ import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import com.sukisu.zako.IKsuInterface
|
||||
// 应用分类
|
||||
|
||||
enum class AppCategory(val displayNameRes: Int, val persistKey: String) {
|
||||
ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"),
|
||||
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");
|
||||
|
||||
companion object {
|
||||
fun fromPersistKey(key: String): AppCategory {
|
||||
return entries.find { it.persistKey == key } ?: ALL
|
||||
}
|
||||
fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL
|
||||
}
|
||||
}
|
||||
|
||||
// 排序方式
|
||||
enum class SortType(val displayNameRes: Int, val persistKey: String) {
|
||||
NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"),
|
||||
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");
|
||||
|
||||
companion object {
|
||||
fun fromPersistKey(key: String): SortType {
|
||||
return entries.find { it.persistKey == key } ?: NAME_ASC
|
||||
}
|
||||
fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @author ShirkNeko
|
||||
* @date 2025/5/31.
|
||||
*/
|
||||
class SuperUserViewModel : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "SuperUserViewModel"
|
||||
private val appsLock = Any()
|
||||
var apps by mutableStateOf<List<AppInfo>>(emptyList())
|
||||
var appGroups by mutableStateOf<List<AppGroup>>(emptyList())
|
||||
|
||||
@JvmStatic
|
||||
fun getAppIconDrawable(context: Context, packageName: String): Drawable? {
|
||||
val appList = synchronized(appsLock) { apps }
|
||||
val appDetail = appList.find { it.packageName == packageName }
|
||||
return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
|
||||
return appList.find { it.packageName == packageName }
|
||||
?.packageInfo?.applicationInfo?.loadIcon(context.packageManager)
|
||||
}
|
||||
|
||||
private const val PREFS_NAME = "settings"
|
||||
private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps"
|
||||
private const val KEY_SELECTED_CATEGORY = "selected_category"
|
||||
@@ -90,31 +83,34 @@ class SuperUserViewModel : ViewModel() {
|
||||
val packageInfo: PackageInfo,
|
||||
val profile: Natives.Profile?,
|
||||
) : Parcelable {
|
||||
val packageName: String
|
||||
get() = packageInfo.packageName
|
||||
val uid: Int
|
||||
get() = packageInfo.applicationInfo!!.uid
|
||||
|
||||
val allowSu: Boolean
|
||||
get() = profile != null && profile.allowSu
|
||||
val packageName: String get() = packageInfo.packageName
|
||||
val uid: Int get() = packageInfo.applicationInfo!!.uid
|
||||
val allowSu: Boolean get() = profile?.allowSu == true
|
||||
val hasCustomProfile: Boolean
|
||||
get() {
|
||||
if (profile == null) {
|
||||
return false
|
||||
}
|
||||
return if (profile.allowSu) {
|
||||
!profile.rootUseDefault
|
||||
} else {
|
||||
!profile.nonRootUseDefault
|
||||
}
|
||||
}
|
||||
get() = profile?.let {
|
||||
if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault
|
||||
} ?: false
|
||||
}
|
||||
|
||||
@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(
|
||||
CORE_POOL_SIZE,
|
||||
MAX_POOL_SIZE,
|
||||
KEEP_ALIVE_TIME,
|
||||
TimeUnit.SECONDS,
|
||||
CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS,
|
||||
LinkedBlockingQueue()
|
||||
) { runnable ->
|
||||
Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply {
|
||||
@@ -124,63 +120,40 @@ class SuperUserViewModel : ViewModel() {
|
||||
}.asCoroutineDispatcher()
|
||||
|
||||
private val appListMutex = Mutex()
|
||||
|
||||
private val configChangeListeners = mutableSetOf<(String) -> Unit>()
|
||||
|
||||
private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
var search by mutableStateOf("")
|
||||
|
||||
var showSystemApps by mutableStateOf(loadShowSystemApps())
|
||||
var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false))
|
||||
private set
|
||||
|
||||
var selectedCategory by mutableStateOf(loadSelectedCategory())
|
||||
private set
|
||||
|
||||
var currentSortType by mutableStateOf(loadCurrentSortType())
|
||||
private set
|
||||
var isRefreshing by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
// 批量操作相关状态
|
||||
var showBatchActions by mutableStateOf(false)
|
||||
internal set
|
||||
var selectedApps by mutableStateOf<Set<String>>(emptySet())
|
||||
internal set
|
||||
|
||||
// 加载进度状态
|
||||
var loadingProgress by mutableFloatStateOf(0f)
|
||||
private set
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载显示系统应用设置
|
||||
*/
|
||||
private fun loadShowSystemApps(): Boolean {
|
||||
return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载选择的应用分类
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 从SharedPreferences加载当前排序方式
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新显示系统应用设置并保存到SharedPreferences
|
||||
*/
|
||||
fun updateShowSystemApps(newValue: Boolean) {
|
||||
showSystemApps = newValue
|
||||
saveShowSystemApps(newValue)
|
||||
prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) }
|
||||
notifyAppListChanged()
|
||||
}
|
||||
|
||||
@@ -190,50 +163,14 @@ class SuperUserViewModel : ViewModel() {
|
||||
apps = currentApps
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新选择的应用分类并保存到SharedPreferences
|
||||
*/
|
||||
fun updateSelectedCategory(newCategory: AppCategory) {
|
||||
selectedCategory = newCategory
|
||||
saveSelectedCategory(newCategory)
|
||||
prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前排序方式并保存到SharedPreferences
|
||||
*/
|
||||
fun updateCurrentSortType(newSortType: SortType) {
|
||||
currentSortType = newSortType
|
||||
saveCurrentSortType(newSortType)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存显示系统应用设置到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}")
|
||||
prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) }
|
||||
}
|
||||
|
||||
private val sortedList by derivedStateOf {
|
||||
@@ -244,34 +181,25 @@ class SuperUserViewModel : ViewModel() {
|
||||
else -> 2
|
||||
}
|
||||
}.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label))
|
||||
apps.sortedWith(comparator).also {
|
||||
isRefreshing = false
|
||||
}
|
||||
apps.sortedWith(comparator).also { isRefreshing = false }
|
||||
}
|
||||
|
||||
val appList by derivedStateOf {
|
||||
val filtered = sortedList.filter {
|
||||
it.label.contains(search, true) || it.packageName.contains(
|
||||
search,
|
||||
true
|
||||
) || HanziToPinyin.getInstance()
|
||||
.toPinyinString(it.label).contains(search, true)
|
||||
sortedList.filter {
|
||||
it.label.contains(search, true) ||
|
||||
it.packageName.contains(search, true) ||
|
||||
HanziToPinyin.getInstance().toPinyinString(it.label).contains(search, true)
|
||||
}.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() {
|
||||
showBatchActions = !showBatchActions
|
||||
if (!showBatchActions) {
|
||||
clearSelection()
|
||||
}
|
||||
if (!showBatchActions) clearSelection()
|
||||
}
|
||||
|
||||
// 切换应用选择状态
|
||||
fun toggleAppSelection(packageName: String) {
|
||||
selectedApps = if (selectedApps.contains(packageName)) {
|
||||
selectedApps - packageName
|
||||
@@ -280,35 +208,14 @@ class SuperUserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
// 清除所有选择
|
||||
fun clearSelection() {
|
||||
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) {
|
||||
selectedApps.forEach { packageName ->
|
||||
val app = apps.find { it.packageName == packageName }
|
||||
app?.let {
|
||||
val profile = Natives.getAppProfile(packageName, it.uid)
|
||||
apps.find { it.packageName == packageName }?.let { app ->
|
||||
val profile = Natives.getAppProfile(packageName, app.uid)
|
||||
val updatedProfile = profile.copy(
|
||||
allowSu = allowSu,
|
||||
umountModules = umountModules ?: profile.umountModules,
|
||||
@@ -325,7 +232,6 @@ class SuperUserViewModel : ViewModel() {
|
||||
refreshAppConfigurations()
|
||||
}
|
||||
|
||||
// 更新本地应用配置
|
||||
fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) {
|
||||
appListMutex.tryLock().let { locked ->
|
||||
if (locked) {
|
||||
@@ -333,9 +239,7 @@ class SuperUserViewModel : ViewModel() {
|
||||
apps = apps.map { app ->
|
||||
if (app.packageName == packageName) {
|
||||
app.copy(profile = updatedProfile)
|
||||
} else {
|
||||
app
|
||||
}
|
||||
} else app
|
||||
}
|
||||
} finally {
|
||||
appListMutex.unlock()
|
||||
@@ -354,15 +258,11 @@ class SuperUserViewModel : ViewModel() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新应用配置状态
|
||||
*/
|
||||
suspend fun refreshAppConfigurations() {
|
||||
withContext(appProcessingThreadPool) {
|
||||
supervisorScope {
|
||||
val currentApps = apps.toList()
|
||||
val batches = currentApps.chunked(BATCH_SIZE)
|
||||
|
||||
loadingProgress = 0f
|
||||
|
||||
val updatedApps = batches.mapIndexed { batchIndex, batch ->
|
||||
@@ -376,59 +276,45 @@ class SuperUserViewModel : ViewModel() {
|
||||
app
|
||||
}
|
||||
}
|
||||
|
||||
val progress = (batchIndex + 1).toFloat() / batches.size
|
||||
loadingProgress = progress
|
||||
|
||||
loadingProgress = (batchIndex + 1).toFloat() / batches.size
|
||||
batchResult
|
||||
}
|
||||
}.awaitAll().flatten()
|
||||
|
||||
appListMutex.withLock {
|
||||
apps = updatedApps
|
||||
}
|
||||
|
||||
appListMutex.withLock { apps = updatedApps }
|
||||
loadingProgress = 1f
|
||||
|
||||
Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
)
|
||||
task?.let { Shell.getShell().execTask(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to bind KsuService", e)
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
serviceConnection = connection
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
|
||||
try {
|
||||
val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask(
|
||||
intent,
|
||||
Shell.EXECUTOR,
|
||||
connection
|
||||
)
|
||||
task?.let { Shell.getShell().execTask(it) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to bind KsuService", e)
|
||||
continuation.resume(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopKsuService() {
|
||||
serviceConnection?.let { _ ->
|
||||
serviceConnection?.let {
|
||||
try {
|
||||
val intent = Intent(ksuApp, KsuService::class.java)
|
||||
com.topjohnwu.superuser.ipc.RootService.stop(intent)
|
||||
@@ -443,9 +329,7 @@ class SuperUserViewModel : ViewModel() {
|
||||
isRefreshing = true
|
||||
loadingProgress = 0f
|
||||
|
||||
val binder = connectKsuService() ?: run {
|
||||
isRefreshing = false; return
|
||||
}
|
||||
val binder = connectKsuService() ?: run { isRefreshing = false; return }
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
val pm = ksuApp.packageManager
|
||||
@@ -468,27 +352,54 @@ class SuperUserViewModel : ViewModel() {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
start += page.size
|
||||
loadingProgress = start.toFloat() / total
|
||||
}
|
||||
|
||||
synchronized(appsLock) {
|
||||
apps
|
||||
}
|
||||
|
||||
stopKsuService()
|
||||
|
||||
appListMutex.withLock {
|
||||
apps = result.filter { it.packageName != ksuApp.packageName }
|
||||
val filteredApps = result.filter { it.packageName != ksuApp.packageName }
|
||||
apps = filteredApps
|
||||
appGroups = groupAppsByUid(filteredApps)
|
||||
}
|
||||
loadingProgress = 1f
|
||||
}
|
||||
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() {
|
||||
super.onCleared()
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user