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 <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();
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
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.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,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user