Step 3: Added theme mode switching, introduced uninstall path manager and user-mode scanning toggle

This commit is contained in:
ShirkNeko
2025-11-19 23:39:07 +08:00
parent ba1aaaa160
commit bc3399fd1b
18 changed files with 956 additions and 26 deletions

View File

@@ -98,7 +98,7 @@ jobs:
./randomizer ./randomizer
- name: Write key - name: Write key
if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }} if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/miuix' )) || github.ref_type == 'tag' }}
run: | run: |
if [ ! -z "${{ secrets.KEYSTORE }}" ]; then if [ ! -z "${{ secrets.KEYSTORE }}" ]; then
{ {
@@ -169,14 +169,14 @@ jobs:
- name: Upload build artifact - name: Upload build artifact
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }} if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/miuix' )) || github.ref_type == 'tag' }}
with: with:
name: ${{ steps.determine.outputs.title }} name: ${{ steps.determine.outputs.title }}
path: manager/app/build/outputs/apk/release/*.apk path: manager/app/build/outputs/apk/release/*.apk
- name: Upload mappings - name: Upload mappings
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }} if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/miuix' )) || github.ref_type == 'tag' }}
with: with:
name: "${{ steps.determine.outputs.title }}-mappings" name: "${{ steps.determine.outputs.title }}-mappings"
path: "manager/app/build/outputs/mapping/release/" path: "manager/app/build/outputs/mapping/release/"

Binary file not shown.

Binary file not shown.

View File

@@ -15,14 +15,4 @@ add_library(kernelsu
find_library(log-lib log) find_library(log-lib log)
if(ANDROID_ABI STREQUAL "arm64-v8a") target_link_libraries(kernelsu ${log-lib})
set(zakosign-lib ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libzakosign.so)
elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
set(zakosign-lib ${CMAKE_SOURCE_DIR}/../jniLibs/armeabi-v7a/libzakosign.so)
endif()
if(ANDROID_ABI STREQUAL "arm64-v8a" OR ANDROID_ABI STREQUAL "armeabi-v7a")
target_link_libraries(kernelsu ${log-lib} ${zakosign-lib})
else()
target_link_libraries(kernelsu ${log-lib})
endif()

View File

@@ -419,7 +419,7 @@ NativeBridgeNP(getManagersList, jobject) {
LogDebug("getManagersList: count=%d", managerListInfo.count); LogDebug("getManagersList: count=%d", managerListInfo.count);
return obj; return obj;
} }
#if 0
NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) { NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) {
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
if (!modulePath) { if (!modulePath) {
@@ -438,6 +438,7 @@ NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) {
return false; return false;
#endif #endif
} }
#endif
NativeBridgeNP(isUidScannerEnabled, jboolean) { NativeBridgeNP(isUidScannerEnabled, jboolean) {
return is_uid_scanner_enabled(); return is_uid_scanner_enabled();

View File

@@ -14,6 +14,7 @@
#include "prelude.h" #include "prelude.h"
#include "ksu.h" #include "ksu.h"
#if 0
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
// Zako extern declarations // Zako extern declarations
@@ -23,6 +24,7 @@ extern uint32_t zako_file_verify_esig(int fd, uint32_t flags);
extern const char* zako_file_verrcidx2str(uint8_t index); extern const char* zako_file_verrcidx2str(uint8_t index);
#endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM #endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM
#endif
static int fd = -1; static int fd = -1;
@@ -348,6 +350,7 @@ bool clear_uid_scanner_environment(void)
return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd);
} }
#if 0
bool verify_module_signature(const char* input) { bool verify_module_signature(const char* input) {
#if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM)
if (input == NULL) { if (input == NULL) {
@@ -404,3 +407,4 @@ bool verify_module_signature(const char* input) {
return false; return false;
#endif #endif
} }
#endif

View File

@@ -119,7 +119,9 @@ bool clear_dynamic_manager();
bool get_managers_list(struct manager_list_info* info); bool get_managers_list(struct manager_list_info* info);
#if 0
bool verify_module_signature(const char* input); bool verify_module_signature(const char* input);
#endif
bool is_uid_scanner_enabled(); bool is_uid_scanner_enabled();

View File

@@ -371,6 +371,7 @@ private fun StatusCard(
text = workingText, text = workingText,
fontSize = 20.sp, fontSize = 20.sp,
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
color = if (isSystemInDarkTheme()) Color(0xFFB8E6C5) else Color(0xFF1A5A2E)
) )
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
Text( Text(
@@ -378,6 +379,7 @@ private fun StatusCard(
text = stringResource(R.string.home_working_version, ksuVersion), text = stringResource(R.string.home_working_version, ksuVersion),
fontSize = 14.sp, fontSize = 14.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.Medium,
color = if (isSystemInDarkTheme()) Color(0xFF9DD4AC) else Color(0xFF2D7A4A)
) )
} }
} }

View File

@@ -0,0 +1,149 @@
package com.sukisu.ultra.ui.screen
import android.app.Activity
import android.content.Context
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.add
import androidx.compose.foundation.layout.displayCutout
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
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.unit.dp
import androidx.core.content.edit
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.SuperDropdown
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Back
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.getWindowSize
import top.yukonga.miuix.kmp.utils.overScrollVertical
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
@Composable
@Destination<RootGraph>
fun Personalization(
navigator: DestinationsNavigator
) {
val scrollBehavior = MiuixScrollBehavior()
val hazeState = remember { HazeState() }
val hazeStyle = HazeStyle(
backgroundColor = colorScheme.background,
tint = HazeTint(colorScheme.background.copy(0.8f))
)
Scaffold(
topBar = {
TopAppBar(
modifier = Modifier.hazeEffect(hazeState) {
style = hazeStyle
blurRadius = 30.dp
noiseFactor = 0f
},
color = Color.Transparent,
title = stringResource(R.string.personalization),
scrollBehavior = scrollBehavior,
navigationIcon = {
IconButton(
onClick = {
navigator.popBackStack()
}
) {
Icon(
imageVector = MiuixIcons.Useful.Back,
contentDescription = "Back"
)
}
}
)
},
popupHost = { },
contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
) { innerPadding ->
val context = LocalContext.current
val activity = context as? Activity
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
LazyColumn(
modifier = Modifier
.height(getWindowSize().height.dp)
.scrollEndHaptic()
.overScrollVertical()
.nestedScroll(scrollBehavior.nestedScrollConnection)
.hazeSource(state = hazeState)
.padding(horizontal = 12.dp),
contentPadding = innerPadding,
overscrollEffect = null,
) {
item {
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
val themeItems = listOf(
stringResource(id = R.string.theme_follow_system),
stringResource(id = R.string.theme_light),
stringResource(id = R.string.theme_dark),
)
var themeMode by rememberSaveable {
mutableIntStateOf(prefs.getInt("theme_mode", 0))
}
SuperDropdown(
title = stringResource(id = R.string.theme_mode),
summary = stringResource(id = R.string.theme_mode_summary),
items = themeItems,
leftAction = {
Icon(
Icons.Rounded.Palette,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(id = R.string.theme_mode),
tint = colorScheme.onBackground
)
},
selectedIndex = themeMode,
onSelectedIndexChange = { index ->
prefs.edit {
putInt("theme_mode", index)
}
themeMode = index
activity?.recreate()
}
)
}
}
}
}
}

View File

@@ -1,8 +1,13 @@
package com.sukisu.ultra.ui.screen package com.sukisu.ultra.ui.screen
import android.content.Context import android.content.Context
import androidx.compose.foundation.layout.Column import android.content.SharedPreferences
import androidx.compose.foundation.layout.Row import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.WindowInsetsSides
@@ -13,9 +18,11 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CleaningServices
import androidx.compose.material.icons.filled.Groups
import androidx.compose.material.icons.filled.Scanner
import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.Adb
import androidx.compose.material.icons.rounded.BugReport import androidx.compose.material.icons.rounded.BugReport
import androidx.compose.material.icons.rounded.ContactPage import androidx.compose.material.icons.rounded.ContactPage
@@ -25,14 +32,14 @@ import androidx.compose.material.icons.rounded.DeveloperMode
import androidx.compose.material.icons.rounded.EnhancedEncryption import androidx.compose.material.icons.rounded.EnhancedEncryption
import androidx.compose.material.icons.rounded.Fence import androidx.compose.material.icons.rounded.Fence
import androidx.compose.material.icons.rounded.FolderDelete import androidx.compose.material.icons.rounded.FolderDelete
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.RemoveCircle import androidx.compose.material.icons.rounded.RemoveCircle
import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material.icons.rounded.RemoveModerator
import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.RestartAlt
import androidx.compose.material.icons.rounded.Security
import androidx.compose.material.icons.rounded.Update import androidx.compose.material.icons.rounded.Update
import androidx.compose.material.icons.rounded.UploadFile import androidx.compose.material.icons.rounded.UploadFile
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@@ -54,6 +61,8 @@ import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination import com.ramcosta.composedestinations.generated.destinations.AboutScreenDestination
import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination
import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination import com.ramcosta.composedestinations.generated.destinations.LogViewerDestination
import com.ramcosta.composedestinations.generated.destinations.PersonalizationDestination
import com.ramcosta.composedestinations.generated.destinations.UmountManagerDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeStyle
@@ -62,27 +71,30 @@ import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.hazeSource
import com.sukisu.ultra.Natives import com.sukisu.ultra.Natives
import com.sukisu.ultra.R import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.component.DynamicManagerCard import com.sukisu.ultra.ui.component.DynamicManagerCard
import com.sukisu.ultra.ui.component.KsuIsValid import com.sukisu.ultra.ui.component.KsuIsValid
import com.sukisu.ultra.ui.component.SendLogDialog import com.sukisu.ultra.ui.component.SendLogDialog
import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.SuperDropdown
import com.sukisu.ultra.ui.component.UninstallDialog import com.sukisu.ultra.ui.component.UninstallDialog
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.rememberLoadingDialog import com.sukisu.ultra.ui.component.rememberLoadingDialog
import com.sukisu.ultra.ui.util.cleanRuntimeEnvironment
import com.sukisu.ultra.ui.util.execKsud import com.sukisu.ultra.ui.util.execKsud
import com.sukisu.ultra.ui.util.getUidMultiUserScan
import com.sukisu.ultra.ui.util.readUidScannerFile
import com.sukisu.ultra.ui.util.setUidAutoScan
import com.sukisu.ultra.ui.util.setUidMultiUserScan
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.ButtonDefaults
import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperArrow
import top.yukonga.miuix.kmp.extra.SuperDialog
import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.extra.SuperSwitch
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.getWindowSize import top.yukonga.miuix.kmp.utils.getWindowSize
@@ -148,6 +160,31 @@ fun SettingPager(
mutableStateOf(prefs.getBoolean("check_update", true)) mutableStateOf(prefs.getBoolean("check_update", true))
} }
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
val personalization = stringResource(id = R.string.personalization)
SuperArrow(
title = personalization,
summary = stringResource(id = R.string.personalization_summary),
leftAction = {
Icon(
Icons.Rounded.Palette,
modifier = Modifier.padding(end = 16.dp),
contentDescription = personalization,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(PersonalizationDestination) {
launchSingleTop = true
}
}
)
}
Card( Card(
modifier = Modifier modifier = Modifier
.padding(top = 12.dp) .padding(top = 12.dp)
@@ -493,6 +530,20 @@ fun SettingPager(
DynamicManagerCard() DynamicManagerCard()
} }
KsuIsValid {
val context = LocalContext.current
val scope = rememberCoroutineScope()
val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) }
Card(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth(),
) {
UidScannerSection(prefs = prefs, scope = scope, context = context)
}
}
KsuIsValid { KsuIsValid {
val lkmMode = Natives.isLkmMode val lkmMode = Natives.isLkmMode
if (lkmMode) { if (lkmMode) {
@@ -544,6 +595,28 @@ fun SettingPager(
} }
) )
} }
KsuIsValid {
val lkmMode = Natives.isLkmMode
if (lkmMode) {
val umontManager = stringResource(id = R.string.umount_path_manager)
SuperArrow(
title = umontManager,
leftAction = {
Icon(
Icons.Rounded.FolderDelete,
modifier = Modifier.padding(end = 16.dp),
contentDescription = umontManager,
tint = colorScheme.onBackground
)
},
onClick = {
navigator.navigate(UmountManagerDestination) {
}
}
)
}
}
SuperArrow( SuperArrow(
title = stringResource(id = R.string.send_log), title = stringResource(id = R.string.send_log),
leftAction = { leftAction = {
@@ -602,3 +675,149 @@ enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int
NONE(Icons.Rounded.Adb, 0, 0) NONE(Icons.Rounded.Adb, 0, 0)
} }
@Composable
fun UidScannerSection(
prefs: SharedPreferences,
scope: CoroutineScope,
context: Context
) {
val realAuto = Natives.isUidScannerEnabled()
val realMulti = getUidMultiUserScan()
var autoOn by remember { mutableStateOf(realAuto) }
var multiOn by remember { mutableStateOf(realMulti) }
LaunchedEffect(Unit) {
autoOn = realAuto
multiOn = realMulti
prefs.edit {
putBoolean("uid_auto_scan", autoOn)
putBoolean("uid_multi_user_scan", multiOn)
}
}
SuperSwitch(
title = stringResource(R.string.uid_auto_scan_title),
summary = stringResource(R.string.uid_auto_scan_summary),
leftAction = {
Icon(
Icons.Filled.Scanner,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_auto_scan_title),
tint = colorScheme.onBackground
)
},
checked = autoOn,
onCheckedChange = { target ->
autoOn = target
if (!target) multiOn = false
scope.launch(Dispatchers.IO) {
setUidAutoScan(target)
val actual = Natives.isUidScannerEnabled() || readUidScannerFile()
withContext(Dispatchers.Main) {
autoOn = actual
if (!actual) multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", actual)
putBoolean("uid_multi_user_scan", multiOn)
}
if (actual != target) {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
SuperSwitch(
title = stringResource(R.string.uid_multi_user_scan_title),
summary = stringResource(R.string.uid_multi_user_scan_summary),
leftAction = {
Icon(
Icons.Filled.Groups,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.uid_multi_user_scan_title),
tint = colorScheme.onBackground
)
},
checked = multiOn,
onCheckedChange = { target ->
scope.launch(Dispatchers.IO) {
val ok = setUidMultiUserScan(target)
withContext(Dispatchers.Main) {
if (ok) {
multiOn = target
prefs.edit { putBoolean("uid_multi_user_scan", target) }
} else {
Toast.makeText(
context,
context.getString(R.string.uid_scanner_setting_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
AnimatedVisibility(
visible = autoOn,
enter = fadeIn() + expandVertically(),
exit = fadeOut() + shrinkVertically()
) {
val confirmDialog = rememberConfirmDialog()
SuperArrow(
title = stringResource(R.string.clean_runtime_environment),
summary = stringResource(R.string.clean_runtime_environment_summary),
leftAction = {
Icon(
Icons.Filled.CleaningServices,
modifier = Modifier.padding(end = 16.dp),
contentDescription = stringResource(R.string.clean_runtime_environment),
tint = colorScheme.onBackground
)
},
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.clean_runtime_environment),
content = context.getString(R.string.clean_runtime_environment_confirm)
) == ConfirmResult.Confirmed
) {
if (cleanRuntimeEnvironment()) {
autoOn = false
multiOn = false
prefs.edit {
putBoolean("uid_auto_scan", false)
putBoolean("uid_multi_user_scan", false)
}
Natives.setUidScannerEnabled(false)
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_success),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.clean_runtime_environment_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
}

View File

@@ -0,0 +1,469 @@
package com.sukisu.ultra.ui.screen
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.Folder
import androidx.compose.material.icons.outlined.Info
import androidx.compose.material.icons.rounded.Add
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
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.unit.dp
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.sukisu.ultra.R
import com.sukisu.ultra.ui.component.rememberConfirmDialog
import com.sukisu.ultra.ui.component.ConfirmResult
import com.sukisu.ultra.ui.util.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import top.yukonga.miuix.kmp.basic.Button
import top.yukonga.miuix.kmp.basic.Card
import top.yukonga.miuix.kmp.basic.FloatingActionButton
import top.yukonga.miuix.kmp.basic.Icon
import top.yukonga.miuix.kmp.basic.IconButton
import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior
import top.yukonga.miuix.kmp.basic.Scaffold
import top.yukonga.miuix.kmp.basic.Text
import top.yukonga.miuix.kmp.basic.TextButton
import top.yukonga.miuix.kmp.basic.TextField
import top.yukonga.miuix.kmp.basic.TopAppBar
import top.yukonga.miuix.kmp.extra.SuperDialog
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.icons.useful.Back
import top.yukonga.miuix.kmp.icon.icons.useful.Delete
import top.yukonga.miuix.kmp.icon.icons.useful.Refresh
import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme
import top.yukonga.miuix.kmp.utils.getWindowSize
import top.yukonga.miuix.kmp.utils.overScrollVertical
import top.yukonga.miuix.kmp.utils.scrollEndHaptic
private val SPACING_SMALL = 3.dp
private val SPACING_MEDIUM = 8.dp
private val SPACING_LARGE = 16.dp
data class UmountPathEntry(
val path: String,
val flags: Int,
val isDefault: Boolean
)
@Destination<RootGraph>
@Composable
fun UmountManager(navigator: DestinationsNavigator) {
val scrollBehavior = MiuixScrollBehavior()
val context = LocalContext.current
val scope = rememberCoroutineScope()
val confirmDialog = rememberConfirmDialog()
var pathList by remember { mutableStateOf<List<UmountPathEntry>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var showAddDialog by remember { mutableStateOf(false) }
fun loadPaths() {
scope.launch(Dispatchers.IO) {
isLoading = true
val result = listUmountPaths()
val entries = parseUmountPaths(result)
withContext(Dispatchers.Main) {
pathList = entries
isLoading = false
}
}
}
LaunchedEffect(Unit) {
loadPaths()
}
Scaffold(
topBar = {
TopAppBar(
title = stringResource(R.string.umount_path_manager),
navigationIcon = {
IconButton(onClick = { navigator.navigateUp() }) {
Icon(
imageVector = MiuixIcons.Useful.Back,
contentDescription = null
)
}
},
actions = {
IconButton(onClick = { loadPaths() }) {
Icon(
imageVector = MiuixIcons.Useful.Refresh,
contentDescription = null
)
}
},
color = Color.Transparent,
scrollBehavior = scrollBehavior
)
},
floatingActionButton = {
FloatingActionButton(
onClick = { showAddDialog = true }
) {
Icon(
imageVector = Icons.Rounded.Add,
contentDescription = null,
tint = Color.White
)
}
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
.height(getWindowSize().height.dp)
.scrollEndHaptic()
.overScrollVertical()
.nestedScroll(scrollBehavior.nestedScrollConnection)
) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE)
) {
Row(
modifier = Modifier.padding(SPACING_LARGE),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Info,
contentDescription = null,
tint = colorScheme.primary
)
Spacer(modifier = Modifier.width(SPACING_MEDIUM))
Text(
text = stringResource(R.string.umount_path_restart_notice)
)
}
}
if (isLoading) {
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
top.yukonga.miuix.kmp.basic.CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM),
verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
items(pathList, key = { it.path }) { entry ->
UmountPathCard(
entry = entry,
onDelete = {
scope.launch(Dispatchers.IO) {
val success = removeUmountPath(entry.path)
withContext(Dispatchers.Main) {
if (success) {
Toast.makeText(
context,
context.getString(R.string.umount_path_removed),
Toast.LENGTH_SHORT
).show()
loadPaths()
} else {
Toast.makeText(
context,
context.getString(R.string.operation_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
item {
Spacer(modifier = Modifier.height(SPACING_LARGE))
}
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = SPACING_LARGE),
horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)
) {
Button(
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.confirm_action),
content = context.getString(R.string.confirm_clear_custom_paths)
) == ConfirmResult.Confirmed) {
withContext(Dispatchers.IO) {
val success = clearCustomUmountPaths()
withContext(Dispatchers.Main) {
if (success) {
Toast.makeText(
context,
context.getString(R.string.custom_paths_cleared),
Toast.LENGTH_SHORT
).show()
loadPaths()
} else {
Toast.makeText(
context,
context.getString(R.string.operation_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
},
modifier = Modifier.weight(1f)
) {
Text(text = stringResource(R.string.clear_custom_paths))
}
Button(
onClick = {
scope.launch(Dispatchers.IO) {
val success = applyUmountConfigToKernel()
withContext(Dispatchers.Main) {
if (success) {
Toast.makeText(
context,
context.getString(R.string.config_applied),
Toast.LENGTH_SHORT
).show()
} else {
Toast.makeText(
context,
context.getString(R.string.operation_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
},
modifier = Modifier.weight(1f)
) {
Text(text = stringResource(R.string.apply_config))
}
}
}
}
}
}
if (showAddDialog) {
AddUmountPathDialog(
onDismiss = { showAddDialog = false },
onConfirm = { path, flags ->
showAddDialog = false
scope.launch(Dispatchers.IO) {
val success = addUmountPath(path, flags)
withContext(Dispatchers.Main) {
if (success) {
saveUmountConfig()
Toast.makeText(
context,
context.getString(R.string.umount_path_added),
Toast.LENGTH_SHORT
).show()
loadPaths()
} else {
Toast.makeText(
context,
context.getString(R.string.operation_failed),
Toast.LENGTH_SHORT
).show()
}
}
}
}
)
}
}
}
@Composable
fun UmountPathCard(
entry: UmountPathEntry,
onDelete: () -> Unit
) {
val confirmDialog = rememberConfirmDialog()
val scope = rememberCoroutineScope()
val context = LocalContext.current
Card(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(SPACING_LARGE),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Outlined.Folder,
contentDescription = null,
tint = if (entry.isDefault)
colorScheme.primary
else
colorScheme.secondary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(SPACING_LARGE))
Column(modifier = Modifier.weight(1f)) {
Text(
text = entry.path
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = buildString {
append(context.getString(R.string.flags))
append(": ")
append(entry.flags.toUmountFlagName(context))
if (entry.isDefault) {
append(" | ")
append(context.getString(R.string.default_entry))
}
},
color = colorScheme.onSurfaceVariantSummary
)
}
if (!entry.isDefault) {
IconButton(
onClick = {
scope.launch {
if (confirmDialog.awaitConfirm(
title = context.getString(R.string.confirm_delete),
content = context.getString(R.string.confirm_delete_umount_path, entry.path)
) == ConfirmResult.Confirmed) {
onDelete()
}
}
}
) {
Icon(
imageVector = MiuixIcons.Useful.Delete,
contentDescription = null,
tint = colorScheme.primary
)
}
}
}
}
}
@Composable
fun AddUmountPathDialog(
onDismiss: () -> Unit,
onConfirm: (String, Int) -> Unit
) {
var path by rememberSaveable { mutableStateOf("") }
var flags by rememberSaveable { mutableStateOf("-1") }
val showDialog = remember { mutableStateOf(true) }
SuperDialog(
show = showDialog,
title = stringResource(R.string.add_umount_path),
onDismissRequest = {
showDialog.value = false
onDismiss()
}
) {
TextField(
value = path,
onValueChange = { path = it },
label = stringResource(R.string.mount_path),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
TextField(
value = flags,
onValueChange = { flags = it },
label = stringResource(R.string.umount_flags),
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
Spacer(modifier = Modifier.height(SPACING_SMALL))
Text(
text = stringResource(R.string.umount_flags_hint),
color = colorScheme.onSurfaceVariantSummary,
modifier = Modifier.padding(start = SPACING_MEDIUM)
)
Spacer(modifier = Modifier.height(SPACING_MEDIUM))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
TextButton(
text = stringResource(android.R.string.cancel),
onClick = {
showDialog.value = false
onDismiss()
},
modifier = Modifier.weight(1f)
)
Spacer(Modifier.width(20.dp))
TextButton(
text = stringResource(android.R.string.ok),
onClick = {
val flagsInt = flags.toIntOrNull() ?: -1
showDialog.value = false
onConfirm(path, flagsInt)
},
modifier = Modifier.weight(1f),
enabled = path.isNotBlank()
)
}
}
}
private fun parseUmountPaths(output: String): List<UmountPathEntry> {
val lines = output.lines().filter { it.isNotBlank() }
if (lines.size < 2) return emptyList()
return lines.drop(2).mapNotNull { line ->
val parts = line.trim().split(Regex("\\s+"))
if (parts.size >= 3) {
UmountPathEntry(
path = parts[0],
flags = parts[1].toIntOrNull() ?: -1,
isDefault = parts[2].equals("Yes", ignoreCase = true)
)
} else null
}
}
private fun Int.toUmountFlagName(context: Context): String {
return when (this) {
-1 -> context.getString(R.string.mnt_detach)
else -> this.toString()
}
}

View File

@@ -1,7 +1,9 @@
package com.sukisu.ultra.ui.theme package com.sukisu.ultra.ui.theme
import android.content.Context
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme
import top.yukonga.miuix.kmp.theme.darkColorScheme import top.yukonga.miuix.kmp.theme.darkColorScheme
import top.yukonga.miuix.kmp.theme.lightColorScheme import top.yukonga.miuix.kmp.theme.lightColorScheme
@@ -11,8 +13,18 @@ fun KernelSUTheme(
darkTheme: Boolean = isSystemInDarkTheme(), darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit content: @Composable () -> Unit
) { ) {
val context = LocalContext.current
val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE)
val themeMode = prefs.getInt("theme_mode", 0)
val useDarkTheme = when (themeMode) {
1 -> false // 浅色
2 -> true // 深色
else -> darkTheme // 跟随系统
}
val colorScheme = when { val colorScheme = when {
darkTheme -> darkColorScheme() useDarkTheme -> darkColorScheme()
else -> lightColorScheme() else -> lightColorScheme()
} }
MiuixTheme( MiuixTheme(

View File

@@ -206,4 +206,44 @@
<string name="invalid_sign_config">无效的管理器配置</string> <string name="invalid_sign_config">无效的管理器配置</string>
<string name="dynamic_manager_disabled_success">动态管理器已禁用</string> <string name="dynamic_manager_disabled_success">动态管理器已禁用</string>
<string name="dynamic_manager_clear_failed">清空动态管理器配置失败</string> <string name="dynamic_manager_clear_failed">清空动态管理器配置失败</string>
<string name="uid_auto_scan_title">用户态扫描应用列表</string>
<string name="uid_auto_scan_summary">开启后将使用用户态扫描应用列表,提高稳定性 (因内核扫描应用列表出现卡死等问题可以尝试打开此选项) </string>
<string name="uid_multi_user_scan_title">多用户应用扫描</string>
<string name="uid_multi_user_scan_summary">开启后将扫描所有用户的应用,包括工作资料等</string>
<string name="uid_scanner_setting_failed">设置失败,请检查权限</string>
<string name="clean_runtime_environment">清理运行环境</string>
<string name="clean_runtime_environment_summary">清理运行时文件并停止扫描服务</string>
<string name="clean_runtime_environment_confirm">您确定要清理运行环境吗?这将停止扫描服务并删除相关文件</string>
<string name="clean_runtime_environment_success">运行环境清理成功</string>
<string name="clean_runtime_environment_failed">运行环境清理失败</string>
<!-- umount Manager -->
<string name="umount_path_manager">Umount 路径管理</string>
<string name="umount_path_manager_summary">管理内核卸载路径</string>
<string name="umount_path_restart_notice">添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。</string>
<string name="add_umount_path">添加 Umount 路径</string>
<string name="mount_path">挂载路径</string>
<string name="umount_flags">卸载标志</string>
<string name="umount_flags_hint">0=正常卸载, 8=MNT_DETACH, -1=自动</string>
<string name="flags">标志</string>
<string name="default_entry">默认条目</string>
<string name="confirm_delete">确认删除</string>
<string name="confirm_delete_umount_path">确定要删除路径 %s 吗?</string>
<string name="umount_path_added">路径已添加,重启后生效</string>
<string name="umount_path_removed">路径已删除,重启后生效</string>
<string name="operation_failed">操作失败</string>
<string name="confirm_action">确认操作</string>
<string name="confirm_clear_custom_paths">确定要清除所有自定义路径吗?(默认路径将保留)</string>
<string name="custom_paths_cleared">自定义路径已清除</string>
<string name="clear_custom_paths">清除自定义</string>
<string name="apply_config">应用配置</string>
<string name="config_applied">配置已应用到内核</string>
<!-- Personalization Settings -->
<string name="personalization">个性化</string>
<string name="personalization_summary">自定义应用外观和主题</string>
<string name="theme_settings">主题设置</string>
<string name="theme_mode">主题模式</string>
<string name="theme_mode_summary">选择应用的显示主题</string>
<string name="theme_light">浅色</string>
<string name="theme_dark">深色</string>
<string name="theme_follow_system">跟随系统</string>
</resources> </resources>

View File

@@ -208,4 +208,46 @@
<string name="invalid_sign_config">Invalid Manager configuration</string> <string name="invalid_sign_config">Invalid Manager configuration</string>
<string name="dynamic_manager_disabled_success">Dynamic Manager disabled</string> <string name="dynamic_manager_disabled_success">Dynamic Manager disabled</string>
<string name="dynamic_manager_clear_failed">Failed to clear dynamic Manager</string> <string name="dynamic_manager_clear_failed">Failed to clear dynamic Manager</string>
<!-- UID Scanner Settings -->
<string name="uid_auto_scan_title">User-mode scanning application list</string>
<string name="uid_auto_scan_summary">Enabling this option will use user-mode scanning for the application list, improving stability. (If you encounter issues such as freezing during kernel scanning of the application list, you may try enabling this option.)</string>
<string name="uid_multi_user_scan_title">Multi-User Application Scanning</string>
<string name="uid_multi_user_scan_summary">When enabled, scans applications for all users, including work profiles</string>
<string name="uid_scanner_setting_failed">Setting failed, please check permissions</string>
<string name="clean_runtime_environment">Clean Runtime Environment</string>
<string name="clean_runtime_environment_summary">Clean up runtime files and stop the scanner service</string>
<string name="clean_runtime_environment_confirm">Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files.</string>
<string name="clean_runtime_environment_success">Runtime environment cleaned successfully</string>
<string name="clean_runtime_environment_failed">Failed to clean runtime environment</string>
<!-- umount Manager -->
<string name="umount_path_manager">Umount Path Management</string>
<string name="umount_path_manager_summary">Manage kernel unmount paths</string>
<string name="umount_path_restart_notice">A reboot is required for changes to take effect. The system will apply the new configuration on the next boot.</string>
<string name="add_umount_path">Add Umount Path</string>
<string name="mount_path">Mount Path</string>
<string name="umount_flags">Unmount Flags</string>
<string name="umount_flags_hint">0=Normal unmount, 8=MNT_DETACH, -1=Auto</string>
<string name="flags">Flags</string>
<string name="default_entry">Default Entry</string>
<string name="confirm_delete">Confirm Delete</string>
<string name="confirm_delete_umount_path">Are you sure you want to delete the path %s?</string>
<string name="umount_path_added">Path added, will take effect after reboot</string>
<string name="umount_path_removed">Path removed, will take effect after reboot</string>
<string name="operation_failed">Operation failed</string>
<string name="confirm_action">Confirm Action</string>
<string name="confirm_clear_custom_paths">Are you sure you want to clear all custom paths? (Default paths will be preserved)</string>
<string name="custom_paths_cleared">Custom paths cleared</string>
<string name="clear_custom_paths">Clear Custom Paths</string>
<string name="apply_config">Apply Configuration</string>
<string name="config_applied">Configuration applied to kernel</string>
<string name="mnt_detach">MNT_DETACH</string>
<!-- Personalization Settings -->
<string name="personalization">Personalization</string>
<string name="personalization_summary">Customize the app\'s appearance and theme</string>
<string name="theme_settings">Theme Settings</string>
<string name="theme_mode">Theme Mode</string>
<string name="theme_mode_summary">Select the app\'s display theme</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<string name="theme_follow_system">Follow System</string>
</resources> </resources>