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 <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();

View File

@@ -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

View File

@@ -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,

View File

@@ -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,
)
)
}
}

View File

@@ -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 {