diff --git a/.github/workflows/build-manager.yml b/.github/workflows/build-manager.yml index 900f27b7..d3c0e335 100644 --- a/.github/workflows/build-manager.yml +++ b/.github/workflows/build-manager.yml @@ -98,7 +98,7 @@ jobs: ./randomizer - 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: | if [ ! -z "${{ secrets.KEYSTORE }}" ]; then { @@ -169,14 +169,14 @@ jobs: - name: Upload build artifact 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: name: ${{ steps.determine.outputs.title }} path: manager/app/build/outputs/apk/release/*.apk - name: Upload mappings 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: name: "${{ steps.determine.outputs.title }}-mappings" path: "manager/app/build/outputs/mapping/release/" diff --git a/manager/app/src/main/assets/5_10-mkbootfs b/manager/app/src/main/assets/5_10-mkbootfs deleted file mode 100644 index 2af1167a..00000000 Binary files a/manager/app/src/main/assets/5_10-mkbootfs and /dev/null differ diff --git a/manager/app/src/main/assets/5_15+-mkbootfs b/manager/app/src/main/assets/5_15+-mkbootfs deleted file mode 100644 index 2eca159b..00000000 Binary files a/manager/app/src/main/assets/5_15+-mkbootfs and /dev/null differ diff --git a/manager/app/src/main/assets/kpimg b/manager/app/src/main/assets/kpimg deleted file mode 100644 index e64eb858..00000000 Binary files a/manager/app/src/main/assets/kpimg and /dev/null differ diff --git a/manager/app/src/main/assets/kptools b/manager/app/src/main/assets/kptools deleted file mode 100644 index f1a2a578..00000000 Binary files a/manager/app/src/main/assets/kptools and /dev/null differ diff --git a/manager/app/src/main/cpp/CMakeLists.txt b/manager/app/src/main/cpp/CMakeLists.txt index 7fc4fdc6..22c7ac27 100644 --- a/manager/app/src/main/cpp/CMakeLists.txt +++ b/manager/app/src/main/cpp/CMakeLists.txt @@ -15,14 +15,4 @@ add_library(kernelsu find_library(log-lib log) -if(ANDROID_ABI STREQUAL "arm64-v8a") - 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() +target_link_libraries(kernelsu ${log-lib}) diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 95818d6e..2d064558 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -419,7 +419,7 @@ NativeBridgeNP(getManagersList, jobject) { LogDebug("getManagersList: count=%d", managerListInfo.count); return obj; } - +#if 0 NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) if (!modulePath) { @@ -438,6 +438,7 @@ NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) { return false; #endif } +#endif NativeBridgeNP(isUidScannerEnabled, jboolean) { return is_uid_scanner_enabled(); diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 0fc8866a..044c4919 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -14,6 +14,7 @@ #include "prelude.h" #include "ksu.h" +#if 0 #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) // 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); #endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM +#endif static int fd = -1; @@ -348,6 +350,7 @@ bool clear_uid_scanner_environment(void) return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); } +#if 0 bool verify_module_signature(const char* input) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) if (input == NULL) { @@ -404,3 +407,4 @@ bool verify_module_signature(const char* input) { return false; #endif } +#endif diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index efcaa051..b5bac7df 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -119,7 +119,9 @@ bool clear_dynamic_manager(); bool get_managers_list(struct manager_list_info* info); +#if 0 bool verify_module_signature(const char* input); +#endif bool is_uid_scanner_enabled(); diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index 6098236f..1c65348b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -371,6 +371,7 @@ private fun StatusCard( text = workingText, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, + color = if (isSystemInDarkTheme()) Color(0xFFB8E6C5) else Color(0xFF1A5A2E) ) Spacer(Modifier.height(2.dp)) Text( @@ -378,6 +379,7 @@ private fun StatusCard( text = stringResource(R.string.home_working_version, ksuVersion), fontSize = 14.sp, fontWeight = FontWeight.Medium, + color = if (isSystemInDarkTheme()) Color(0xFF9DD4AC) else Color(0xFF2D7A4A) ) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt new file mode 100644 index 00000000..881de8dc --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Personalization.kt @@ -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 +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() + } + ) + } + } + } + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 8f5b95c9..5ee2b092 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -1,8 +1,13 @@ package com.sukisu.ultra.ui.screen import android.content.Context -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row +import android.content.SharedPreferences +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.WindowInsets 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.padding import androidx.compose.foundation.layout.systemBars -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn 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.BugReport 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.Fence 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.RemoveModerator 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.UploadFile import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf 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.AppProfileTemplateScreenDestination 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 dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle @@ -62,27 +71,30 @@ import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource import com.sukisu.ultra.Natives 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.KsuIsValid import com.sukisu.ultra.ui.component.SendLogDialog import com.sukisu.ultra.ui.component.SuperDropdown 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.util.cleanRuntimeEnvironment 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.launch import kotlinx.coroutines.withContext -import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.Icon 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.SuperArrow -import top.yukonga.miuix.kmp.extra.SuperDialog import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.getWindowSize @@ -148,6 +160,31 @@ fun SettingPager( 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( modifier = Modifier .padding(top = 12.dp) @@ -493,6 +530,20 @@ fun SettingPager( 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 { val lkmMode = Natives.isLkmMode 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( title = stringResource(id = R.string.send_log), leftAction = { @@ -602,3 +675,149 @@ enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int 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() + } + } + } + } + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManager.kt new file mode 100644 index 00000000..3ff2d9a8 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManager.kt @@ -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 +@Composable +fun UmountManager(navigator: DestinationsNavigator) { + val scrollBehavior = MiuixScrollBehavior() + val context = LocalContext.current + val scope = rememberCoroutineScope() + val confirmDialog = rememberConfirmDialog() + + var pathList by remember { mutableStateOf>(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 { + 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() + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt index a06cd460..b859e31b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt @@ -1,7 +1,9 @@ package com.sukisu.ultra.ui.theme +import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.darkColorScheme import top.yukonga.miuix.kmp.theme.lightColorScheme @@ -11,8 +13,18 @@ fun KernelSUTheme( darkTheme: Boolean = isSystemInDarkTheme(), 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 { - darkTheme -> darkColorScheme() + useDarkTheme -> darkColorScheme() else -> lightColorScheme() } MiuixTheme( diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so b/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so deleted file mode 100644 index 6669436e..00000000 Binary files a/manager/app/src/main/jniLibs/arm64-v8a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so b/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so deleted file mode 100644 index 72954b6a..00000000 Binary files a/manager/app/src/main/jniLibs/armeabi-v7a/libzakosign.so and /dev/null differ diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index e6882ab3..0dc77eeb 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -206,4 +206,44 @@ 无效的管理器配置 动态管理器已禁用 清空动态管理器配置失败 + 用户态扫描应用列表 + 开启后将使用用户态扫描应用列表,提高稳定性 (因内核扫描应用列表出现卡死等问题可以尝试打开此选项) + 多用户应用扫描 + 开启后将扫描所有用户的应用,包括工作资料等 + 设置失败,请检查权限 + 清理运行环境 + 清理运行时文件并停止扫描服务 + 您确定要清理运行环境吗?这将停止扫描服务并删除相关文件 + 运行环境清理成功 + 运行环境清理失败 + + Umount 路径管理 + 管理内核卸载路径 + 添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。 + 添加 Umount 路径 + 挂载路径 + 卸载标志 + 0=正常卸载, 8=MNT_DETACH, -1=自动 + 标志 + 默认条目 + 确认删除 + 确定要删除路径 %s 吗? + 路径已添加,重启后生效 + 路径已删除,重启后生效 + 操作失败 + 确认操作 + 确定要清除所有自定义路径吗?(默认路径将保留) + 自定义路径已清除 + 清除自定义 + 应用配置 + 配置已应用到内核 + + 个性化 + 自定义应用外观和主题 + 主题设置 + 主题模式 + 选择应用的显示主题 + 浅色 + 深色 + 跟随系统 diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index 737199f1..9967d574 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -208,4 +208,46 @@ Invalid Manager configuration Dynamic Manager disabled Failed to clear dynamic Manager + + User-mode scanning application list + 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.) + Multi-User Application Scanning + When enabled, scans applications for all users, including work profiles + Setting failed, please check permissions + Clean Runtime Environment + Clean up runtime files and stop the scanner service + Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files. + Runtime environment cleaned successfully + Failed to clean runtime environment + + Umount Path Management + Manage kernel unmount paths + A reboot is required for changes to take effect. The system will apply the new configuration on the next boot. + Add Umount Path + Mount Path + Unmount Flags + 0=Normal unmount, 8=MNT_DETACH, -1=Auto + Flags + Default Entry + Confirm Delete + Are you sure you want to delete the path %s? + Path added, will take effect after reboot + Path removed, will take effect after reboot + Operation failed + Confirm Action + Are you sure you want to clear all custom paths? (Default paths will be preserved) + Custom paths cleared + Clear Custom Paths + Apply Configuration + Configuration applied to kernel + MNT_DETACH + + Personalization + Customize the app\'s appearance and theme + Theme Settings + Theme Mode + Select the app\'s display theme + Light + Dark + Follow System