Repository: tiann/KernelSU Branch: main Commit: 62fa92b7d1cc Files: 542 Total size: 3.0 MB Directory structure: gitextract_ypog1o43/ ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── custom.yml │ ├── dependabot.yml │ └── workflows/ │ ├── build-lkm.yml │ ├── build-manager.yml │ ├── clang-format.yml │ ├── clippy.yml │ ├── ddk-lkm.yml │ ├── deploy-website.yml │ ├── ksud.yml │ ├── ksuinit.yml │ ├── release.yml │ ├── rustfmt.yml │ └── shellcheck.yml ├── .gitignore ├── LICENSE ├── SECURITY.md ├── docs/ │ ├── README.md │ ├── README_CN.md │ ├── README_ES.md │ ├── README_ID.md │ ├── README_IN.md │ ├── README_IT.md │ ├── README_IW.md │ ├── README_JP.md │ ├── README_KR.md │ ├── README_PL.md │ ├── README_PT-BR.md │ ├── README_RU.md │ ├── README_TR.md │ ├── README_TW.md │ └── README_VI.md ├── fastlane/ │ └── metadata/ │ └── android/ │ └── en-US/ │ ├── full_description.txt │ └── short_description.txt ├── js/ │ ├── README.md │ ├── index.d.ts │ ├── index.js │ └── package.json ├── justfile ├── kernel/ │ ├── .clang-format │ ├── .clangd │ ├── .gitignore │ ├── Kbuild │ ├── Kconfig │ ├── LICENSE │ ├── Makefile │ ├── allowlist.c │ ├── allowlist.h │ ├── apk_sign.c │ ├── apk_sign.h │ ├── app_profile.c │ ├── app_profile.h │ ├── arch.h │ ├── feature.c │ ├── feature.h │ ├── file_wrapper.c │ ├── file_wrapper.h │ ├── kernel_umount.c │ ├── kernel_umount.h │ ├── klog.h │ ├── ksu.c │ ├── ksu.h │ ├── ksud.c │ ├── ksud.h │ ├── manager.h │ ├── pkg_observer.c │ ├── seccomp_cache.c │ ├── seccomp_cache.h │ ├── selinux/ │ │ ├── rules.c │ │ ├── selinux.c │ │ ├── selinux.h │ │ ├── sepolicy.c │ │ └── sepolicy.h │ ├── setuid_hook.c │ ├── setuid_hook.h │ ├── setup.sh │ ├── su_mount_ns.c │ ├── su_mount_ns.h │ ├── sucompat.c │ ├── sucompat.h │ ├── supercalls.c │ ├── supercalls.h │ ├── syscall_hook_manager.c │ ├── syscall_hook_manager.h │ ├── throne_tracker.c │ ├── throne_tracker.h │ ├── tools/ │ │ └── check_symbol.c │ ├── util.c │ └── util.h ├── manager/ │ ├── .gitignore │ ├── app/ │ │ ├── .gitignore │ │ ├── build.gradle.kts │ │ ├── proguard-rules.pro │ │ └── src/ │ │ └── main/ │ │ ├── AndroidManifest.xml │ │ ├── aidl/ │ │ │ └── me/ │ │ │ └── weishu/ │ │ │ └── kernelsu/ │ │ │ └── IKsuInterface.aidl │ │ ├── assets/ │ │ │ └── github-markdown.css │ │ ├── cpp/ │ │ │ ├── CMakeLists.txt │ │ │ ├── jni.cc │ │ │ ├── ksu.cc │ │ │ ├── ksu.h │ │ │ └── logging.h │ │ ├── java/ │ │ │ └── me/ │ │ │ └── weishu/ │ │ │ └── kernelsu/ │ │ │ ├── KernelSUApplication.kt │ │ │ ├── Kernels.kt │ │ │ ├── Natives.kt │ │ │ ├── data/ │ │ │ │ ├── model/ │ │ │ │ │ ├── AppInfo.kt │ │ │ │ │ ├── Module.kt │ │ │ │ │ ├── ModuleUpdateInfo.kt │ │ │ │ │ ├── RepoModule.kt │ │ │ │ │ └── TemplateInfo.kt │ │ │ │ └── repository/ │ │ │ │ ├── ModuleRepoRepository.kt │ │ │ │ ├── ModuleRepoRepositoryImpl.kt │ │ │ │ ├── ModuleRepository.kt │ │ │ │ ├── ModuleRepositoryImpl.kt │ │ │ │ ├── SettingsRepository.kt │ │ │ │ ├── SettingsRepositoryImpl.kt │ │ │ │ ├── SuperUserRepository.kt │ │ │ │ ├── SuperUserRepositoryImpl.kt │ │ │ │ ├── TemplateRepository.kt │ │ │ │ └── TemplateRepositoryImpl.kt │ │ │ ├── magica/ │ │ │ │ ├── AppZygotePreload.java │ │ │ │ ├── BootCompletedReceiver.java │ │ │ │ └── MagicaService.java │ │ │ ├── profile/ │ │ │ │ ├── Capabilities.kt │ │ │ │ └── Groups.kt │ │ │ └── ui/ │ │ │ ├── KsuService.kt │ │ │ ├── MainActivity.kt │ │ │ ├── UiMode.kt │ │ │ ├── animation/ │ │ │ │ ├── DampedDragAnimation.kt │ │ │ │ └── InteractiveHighlight.kt │ │ │ ├── component/ │ │ │ │ ├── AppIconImage.kt │ │ │ │ ├── FloatingBottomBar.kt │ │ │ │ ├── GithubMarkdown.kt │ │ │ │ ├── KeyEventBlocker.kt │ │ │ │ ├── KsuValidCheck.kt │ │ │ │ ├── Markdown.kt │ │ │ │ ├── MenuPositionProvider.kt │ │ │ │ ├── SearchStatus.kt │ │ │ │ ├── bottombar/ │ │ │ │ │ ├── BottomBar.kt │ │ │ │ │ ├── BottomBarMaterial.kt │ │ │ │ │ ├── BottomBarMiuix.kt │ │ │ │ │ ├── NavigationRailMaterial.kt │ │ │ │ │ └── NavigationRailMiuix.kt │ │ │ │ ├── choosekmidialog/ │ │ │ │ │ ├── ChooseKmiDialog.kt │ │ │ │ │ ├── ChooseKmiDialogMaterial.kt │ │ │ │ │ └── ChooseKmiDialogMiuix.kt │ │ │ │ ├── dialog/ │ │ │ │ │ ├── Dialog.kt │ │ │ │ │ ├── DialogMaterial.kt │ │ │ │ │ └── DialogMiuix.kt │ │ │ │ ├── filter/ │ │ │ │ │ ├── BaseFieldFilter.kt │ │ │ │ │ └── FilterNumber.kt │ │ │ │ ├── material/ │ │ │ │ │ ├── ExpressiveSwitch.kt │ │ │ │ │ ├── SearchBar.kt │ │ │ │ │ ├── SegmentedList.kt │ │ │ │ │ ├── SendLogBottomSheet.kt │ │ │ │ │ └── SettingsItem.kt │ │ │ │ ├── miuix/ │ │ │ │ │ ├── DropdownItem.kt │ │ │ │ │ ├── EditText.kt │ │ │ │ │ ├── ScaleDialog.kt │ │ │ │ │ ├── SendLogDialog.kt │ │ │ │ │ ├── SuperEditArrow.kt │ │ │ │ │ └── SuperSearchBar.kt │ │ │ │ ├── profile/ │ │ │ │ │ ├── AppProfileConfigMaterial.kt │ │ │ │ │ ├── AppProfileConfigMiuix.kt │ │ │ │ │ ├── ProfileConfig.kt │ │ │ │ │ ├── RootProfileConfigMaterial.kt │ │ │ │ │ ├── RootProfileConfigMiuix.kt │ │ │ │ │ ├── TemplateConfigMaterial.kt │ │ │ │ │ ├── TemplateConfigMiuix.kt │ │ │ │ │ └── dialogs/ │ │ │ │ │ ├── MultiSelectDialog.kt │ │ │ │ │ └── SingleSelectDialog.kt │ │ │ │ ├── rebootlistpopup/ │ │ │ │ │ ├── RebootListPopup.kt │ │ │ │ │ ├── RebootListPopupMaterial.kt │ │ │ │ │ └── RebootListPopupMiuix.kt │ │ │ │ ├── statustag/ │ │ │ │ │ ├── StatusTag.kt │ │ │ │ │ ├── StatusTagMaterial.kt │ │ │ │ │ └── StatusTagMiuix.kt │ │ │ │ └── uninstalldialog/ │ │ │ │ ├── UninstallDialog.kt │ │ │ │ ├── UninstallDialogMaterial.kt │ │ │ │ └── UninstallDialogMiuix.kt │ │ │ ├── modifier/ │ │ │ │ └── DragGestureInspector.kt │ │ │ ├── navigation3/ │ │ │ │ ├── DeepLinkResolver.kt │ │ │ │ ├── Navigator.kt │ │ │ │ └── Routes.kt │ │ │ ├── screen/ │ │ │ │ ├── about/ │ │ │ │ │ ├── AboutMaterial.kt │ │ │ │ │ ├── AboutMiuix.kt │ │ │ │ │ ├── AboutScreen.kt │ │ │ │ │ ├── AboutUiState.kt │ │ │ │ │ └── AboutUtils.kt │ │ │ │ ├── appprofile/ │ │ │ │ │ ├── AppProfileMaterial.kt │ │ │ │ │ ├── AppProfileMiuix.kt │ │ │ │ │ ├── AppProfileScreen.kt │ │ │ │ │ ├── AppProfileUiState.kt │ │ │ │ │ └── AppProfileUtils.kt │ │ │ │ ├── colorpalette/ │ │ │ │ │ ├── ColorPaletteScreen.kt │ │ │ │ │ ├── ColorPaletteScreenMaterial.kt │ │ │ │ │ ├── ColorPaletteScreenMiuix.kt │ │ │ │ │ └── ColorPaletteUiState.kt │ │ │ │ ├── executemoduleaction/ │ │ │ │ │ ├── ExecuteModuleActionMaterial.kt │ │ │ │ │ ├── ExecuteModuleActionMiuix.kt │ │ │ │ │ ├── ExecuteModuleActionScreen.kt │ │ │ │ │ ├── ExecuteModuleActionUiState.kt │ │ │ │ │ └── ExecuteModuleActionUtils.kt │ │ │ │ ├── flash/ │ │ │ │ │ ├── FlashMaterial.kt │ │ │ │ │ ├── FlashMiuix.kt │ │ │ │ │ ├── FlashScreen.kt │ │ │ │ │ ├── FlashUiState.kt │ │ │ │ │ └── FlashUtils.kt │ │ │ │ ├── home/ │ │ │ │ │ ├── HomeMaterial.kt │ │ │ │ │ ├── HomeMiuix.kt │ │ │ │ │ ├── HomeScreen.kt │ │ │ │ │ ├── HomeUiState.kt │ │ │ │ │ └── HomeUtils.kt │ │ │ │ ├── install/ │ │ │ │ │ ├── InstallMaterial.kt │ │ │ │ │ ├── InstallMiuix.kt │ │ │ │ │ ├── InstallScreen.kt │ │ │ │ │ ├── InstallUiState.kt │ │ │ │ │ └── InstallUtils.kt │ │ │ │ ├── module/ │ │ │ │ │ ├── ModuleMaterial.kt │ │ │ │ │ ├── ModuleMiuix.kt │ │ │ │ │ ├── ModuleScreen.kt │ │ │ │ │ ├── ModuleShortcutState.kt │ │ │ │ │ ├── ModuleUiState.kt │ │ │ │ │ └── ModuleUtils.kt │ │ │ │ ├── modulerepo/ │ │ │ │ │ ├── ModuleRepoMaterial.kt │ │ │ │ │ ├── ModuleRepoMiuix.kt │ │ │ │ │ ├── ModuleRepoModels.kt │ │ │ │ │ ├── ModuleRepoScreen.kt │ │ │ │ │ └── ModuleRepoUiState.kt │ │ │ │ ├── settings/ │ │ │ │ │ ├── SettingsMaterial.kt │ │ │ │ │ ├── SettingsMiuix.kt │ │ │ │ │ ├── SettingsScreen.kt │ │ │ │ │ └── SettingsUiState.kt │ │ │ │ ├── superuser/ │ │ │ │ │ ├── SuperUserMaterial.kt │ │ │ │ │ ├── SuperUserMiuix.kt │ │ │ │ │ ├── SuperUserScreen.kt │ │ │ │ │ └── SuperUserUiState.kt │ │ │ │ ├── template/ │ │ │ │ │ ├── TemplateMaterial.kt │ │ │ │ │ ├── TemplateMiuix.kt │ │ │ │ │ ├── TemplateScreen.kt │ │ │ │ │ └── TemplateUiState.kt │ │ │ │ └── templateeditor/ │ │ │ │ ├── TemplateEditorMaterial.kt │ │ │ │ ├── TemplateEditorMiuix.kt │ │ │ │ ├── TemplateEditorScreen.kt │ │ │ │ ├── TemplateEditorUiState.kt │ │ │ │ └── TemplateEditorUtils.kt │ │ │ ├── theme/ │ │ │ │ ├── Colors.kt │ │ │ │ ├── MaterialTheme.kt │ │ │ │ ├── MiuixTheme.kt │ │ │ │ └── Theme.kt │ │ │ ├── util/ │ │ │ │ ├── AppIconCache.kt │ │ │ │ ├── Colors.kt │ │ │ │ ├── CompositionProvider.kt │ │ │ │ ├── Downloader.kt │ │ │ │ ├── HanziToPinyin.java │ │ │ │ ├── HazeExt.kt │ │ │ │ ├── KsuCli.kt │ │ │ │ ├── LogEvent.kt │ │ │ │ ├── Network.kt │ │ │ │ ├── OemHelper.kt │ │ │ │ ├── SELinuxChecker.kt │ │ │ │ ├── Serialization.kt │ │ │ │ ├── UidGroupUtils.kt │ │ │ │ └── module/ │ │ │ │ ├── LatestVersionInfo.kt │ │ │ │ ├── ModuleRepoApi.kt │ │ │ │ └── Shortcut.kt │ │ │ ├── viewmodel/ │ │ │ │ ├── HomeViewModel.kt │ │ │ │ ├── MainActivityUiState.kt │ │ │ │ ├── MainActivityViewModel.kt │ │ │ │ ├── ModuleRepoViewModel.kt │ │ │ │ ├── ModuleViewModel.kt │ │ │ │ ├── SearchViewModelHelper.kt │ │ │ │ ├── SettingsViewModel.kt │ │ │ │ ├── SuperUserViewModel.kt │ │ │ │ └── TemplateViewModel.kt │ │ │ └── webui/ │ │ │ ├── AppIconUtil.kt │ │ │ ├── Insets.kt │ │ │ ├── MimeUtil.java │ │ │ ├── MonetColorsProvider.kt │ │ │ ├── SuFilePathHandler.java │ │ │ ├── WebUIActivity.kt │ │ │ ├── WebUIMaterial.kt │ │ │ ├── WebUIMiuix.kt │ │ │ ├── WebUIScreen.kt │ │ │ ├── WebUIState.kt │ │ │ ├── WebViewHelper.kt │ │ │ └── WebViewInterface.kt │ │ ├── jniLibs/ │ │ │ └── .gitignore │ │ └── res/ │ │ ├── drawable/ │ │ │ ├── ic_launcher_foreground.xml │ │ │ └── ic_launcher_monochrome.xml │ │ ├── mipmap-anydpi/ │ │ │ └── ic_launcher.xml │ │ ├── resources.properties │ │ ├── values/ │ │ │ ├── colors.xml │ │ │ ├── strings.xml │ │ │ └── themes.xml │ │ ├── values-ar/ │ │ │ └── strings.xml │ │ ├── values-az/ │ │ │ └── strings.xml │ │ ├── values-bg/ │ │ │ └── strings.xml │ │ ├── values-bn/ │ │ │ └── strings.xml │ │ ├── values-bn-rBD/ │ │ │ └── strings.xml │ │ ├── values-bs/ │ │ │ └── strings.xml │ │ ├── values-da/ │ │ │ └── strings.xml │ │ ├── values-de/ │ │ │ └── strings.xml │ │ ├── values-es/ │ │ │ └── strings.xml │ │ ├── values-et/ │ │ │ └── strings.xml │ │ ├── values-fa/ │ │ │ └── strings.xml │ │ ├── values-fil/ │ │ │ └── strings.xml │ │ ├── values-fr/ │ │ │ └── strings.xml │ │ ├── values-gl/ │ │ │ └── strings.xml │ │ ├── values-hi/ │ │ │ └── strings.xml │ │ ├── values-hr/ │ │ │ └── strings.xml │ │ ├── values-hu/ │ │ │ └── strings.xml │ │ ├── values-in/ │ │ │ └── strings.xml │ │ ├── values-it/ │ │ │ └── strings.xml │ │ ├── values-iw/ │ │ │ └── strings.xml │ │ ├── values-ja/ │ │ │ └── strings.xml │ │ ├── values-km/ │ │ │ └── strings.xml │ │ ├── values-kn/ │ │ │ └── strings.xml │ │ ├── values-ko/ │ │ │ └── strings.xml │ │ ├── values-lt/ │ │ │ └── strings.xml │ │ ├── values-lv/ │ │ │ └── strings.xml │ │ ├── values-mr/ │ │ │ └── strings.xml │ │ ├── values-ms/ │ │ │ └── strings.xml │ │ ├── values-my/ │ │ │ └── strings.xml │ │ ├── values-night/ │ │ │ └── themes.xml │ │ ├── values-nl/ │ │ │ └── strings.xml │ │ ├── values-pl/ │ │ │ └── strings.xml │ │ ├── values-pt/ │ │ │ └── strings.xml │ │ ├── values-pt-rBR/ │ │ │ └── strings.xml │ │ ├── values-ro/ │ │ │ └── strings.xml │ │ ├── values-ru/ │ │ │ └── strings.xml │ │ ├── values-sl/ │ │ │ └── strings.xml │ │ ├── values-sr/ │ │ │ └── strings.xml │ │ ├── values-te/ │ │ │ └── strings.xml │ │ ├── values-th/ │ │ │ └── strings.xml │ │ ├── values-tr/ │ │ │ └── strings.xml │ │ ├── values-uk/ │ │ │ └── strings.xml │ │ ├── values-vi/ │ │ │ └── strings.xml │ │ ├── values-zh-rCN/ │ │ │ └── strings.xml │ │ ├── values-zh-rHK/ │ │ │ └── strings.xml │ │ ├── values-zh-rTW/ │ │ │ └── strings.xml │ │ └── xml/ │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ ├── filepaths.xml │ │ └── network_security_config.xml │ ├── build.gradle.kts │ ├── gradle/ │ │ ├── libs.versions.toml │ │ └── wrapper/ │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties │ ├── gradle.properties │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle.kts │ └── sign.example.properties ├── scripts/ │ ├── allowlist.bt │ └── ksubot.py ├── userspace/ │ ├── ksud/ │ │ ├── .cargo/ │ │ │ └── config.example.toml │ │ ├── .gitignore │ │ ├── Cargo.toml │ │ ├── bin/ │ │ │ ├── .gitignore │ │ │ ├── aarch64/ │ │ │ │ ├── bootctl │ │ │ │ └── busybox │ │ │ └── x86_64/ │ │ │ └── busybox │ │ ├── build.rs │ │ └── src/ │ │ ├── apk_sign.rs │ │ ├── assets.rs │ │ ├── banner │ │ ├── boot_patch.rs │ │ ├── cli.rs │ │ ├── cli_non_android.rs │ │ ├── debug.rs │ │ ├── defs.rs │ │ ├── feature.rs │ │ ├── init_event.rs │ │ ├── installer.sh │ │ ├── ksucalls.rs │ │ ├── late_load.rs │ │ ├── magica.rs │ │ ├── main.rs │ │ ├── metamodule.rs │ │ ├── module.rs │ │ ├── module_config.rs │ │ ├── profile.rs │ │ ├── resetprop.rs │ │ ├── restorecon.rs │ │ ├── sepolicy.rs │ │ ├── su.rs │ │ └── utils.rs │ └── ksuinit/ │ ├── .gitignore │ ├── Cargo.toml │ ├── build.rs │ └── src/ │ ├── init.rs │ ├── lib.rs │ └── main.rs └── website/ ├── .gitignore ├── docs/ │ ├── .vitepress/ │ │ ├── config.ts │ │ └── locales/ │ │ ├── en.ts │ │ ├── id_ID.ts │ │ ├── index.ts │ │ ├── ja_JP.ts │ │ ├── pt_BR.ts │ │ ├── ru_RU.ts │ │ ├── vi_VN.ts │ │ ├── zh_CN.ts │ │ └── zh_TW.ts │ ├── guide/ │ │ ├── app-profile.md │ │ ├── difference-with-magisk.md │ │ ├── faq.md │ │ ├── hidden-features.md │ │ ├── how-to-build.md │ │ ├── how-to-integrate-for-non-gki.md │ │ ├── installation.md │ │ ├── metamodule.md │ │ ├── module-config.md │ │ ├── module-webui.md │ │ ├── module.md │ │ ├── rescue-from-bootloop.md │ │ ├── unofficially-support-devices.md │ │ └── what-is-kernelsu.md │ ├── id_ID/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ ├── index.md │ ├── ja_JP/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ ├── pt_BR/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ ├── public/ │ │ ├── ads.txt │ │ └── templates/ │ │ ├── .gitkeep │ │ ├── adaway.root │ │ ├── adb │ │ ├── cemiuiler.readproc │ │ ├── hyperceiler.root │ │ ├── incompetent.root │ │ ├── kernelmanager.root │ │ ├── nethunter.root │ │ ├── rootexploler.root │ │ ├── shizuku.root │ │ ├── system │ │ └── wireguard.root │ ├── repos.json │ ├── ru_RU/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ ├── vi_VN/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ ├── zh_CN/ │ │ ├── guide/ │ │ │ ├── app-profile.md │ │ │ ├── difference-with-magisk.md │ │ │ ├── faq.md │ │ │ ├── hidden-features.md │ │ │ ├── how-to-build.md │ │ │ ├── how-to-integrate-for-non-gki.md │ │ │ ├── installation.md │ │ │ ├── metamodule.md │ │ │ ├── module-config.md │ │ │ ├── module-webui.md │ │ │ ├── module.md │ │ │ ├── rescue-from-bootloop.md │ │ │ ├── unofficially-support-devices.md │ │ │ └── what-is-kernelsu.md │ │ └── index.md │ └── zh_TW/ │ ├── guide/ │ │ ├── app-profile.md │ │ ├── difference-with-magisk.md │ │ ├── faq.md │ │ ├── hidden-features.md │ │ ├── how-to-build.md │ │ ├── how-to-integrate-for-non-gki.md │ │ ├── installation.md │ │ ├── metamodule.md │ │ ├── module-config.md │ │ ├── module-webui.md │ │ ├── module.md │ │ ├── rescue-from-bootloop.md │ │ ├── unofficially-support-devices.md │ │ └── what-is-kernelsu.md │ └── index.md └── package.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ *.bat eol=crlf ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: tiann patreon: weishu ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: Bug report description: Create a report to help us improve KernelSU labels: [Bug] body: - type: checkboxes attributes: label: Please check before submitting an issue options: - label: I have searched the issues and haven't found anything relevant required: true - label: I will upload bugreport file in KernelSU Manager - Settings - Report log required: true - label: I know how to reproduce the issue which may not be specific to my device required: false - type: textarea attributes: label: Describe the bug description: A clear and concise description of what the bug is validations: required: true - type: textarea attributes: label: To Reproduce description: Steps to reproduce the behaviour placeholder: | - 1. Go to '...' - 2. Click on '....' - 3. Scroll down to '....' - 4. See error - type: textarea attributes: label: Expected behavior description: A clear and concise description of what you expected to happen. - type: textarea attributes: label: Screenshots description: If applicable, add screenshots to help explain your problem. - type: textarea attributes: label: Logs description: If applicable, add crash or any other logs to help us figure out the problem. - type: textarea attributes: label: Device info value: | - Device: - OS Version: - KernelSU Version: - Kernel Version: validations: required: true - type: textarea attributes: label: Additional context description: Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Feature Request url: https://github.com/tiann/KernelSU/issues/1705 about: "We do not accept external Feature Requests, see this link for more details." ================================================ FILE: .github/ISSUE_TEMPLATE/custom.yml ================================================ name: Custom issue template description: WARNING! If you are reporting a bug but use this template, the issue will be closed directly. title: '[Custom]' body: - type: textarea id: description attributes: label: "Describe your problem." validations: required: true ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: github-actions directory: / schedule: interval: weekly groups: actions: patterns: - "*" - package-ecosystem: cargo directory: userspace/ksud schedule: interval: weekly allow: - dependency-type: "all" groups: crates: patterns: - "*" - package-ecosystem: cargo directory: userspace/ksuinit schedule: interval: weekly allow: - dependency-type: "all" groups: crates: patterns: - "*" - package-ecosystem: gradle directory: manager schedule: interval: weekly groups: maven: patterns: - "*" - package-ecosystem: npm directory: website schedule: interval: weekly allow: - dependency-type: "all" groups: npm: patterns: - "*" ================================================ FILE: .github/workflows/build-lkm.yml ================================================ name: Build LKM for KernelSU on: workflow_call: inputs: expected_size2: description: 'Second expected signature size (for PR builds)' required: false default: '' type: string expected_hash2: description: 'Second expected signature hash (for PR builds)' required: false default: '' type: string workflow_dispatch: jobs: build-lkm: strategy: matrix: kmi: - android12-5.10 - android13-5.10 - android13-5.15 - android14-5.15 - android14-6.1 - android15-6.6 - android16-6.12 uses: ./.github/workflows/ddk-lkm.yml with: kmi: ${{ matrix.kmi }} ddk_release: '20260313' expected_size2: ${{ inputs.expected_size2 || '' }} expected_hash2: ${{ inputs.expected_hash2 || '' }} ================================================ FILE: .github/workflows/build-manager.yml ================================================ name: Build Manager on: push: branches: [ "main", "dev", "ci" ] paths: - '.github/workflows/build-manager.yml' - '.github/workflows/build-lkm.yml' - '.github/workflows/ddk-lkm.yml' - '.github/workflows/ksud.yml' - 'manager/**' - 'kernel/**' - 'userspace/**' pull_request: branches: [ "main", "dev" ] paths: - '.github/workflows/build-manager.yml' - '.github/workflows/build-lkm.yml' - '.github/workflows/ddk-lkm.yml' - '.github/workflows/ksud.yml' - 'manager/**' - 'kernel/**' - 'userspace/**' workflow_call: jobs: generate-key: runs-on: ubuntu-latest outputs: expected_size: ${{ steps.extract.outputs.expected_size }} expected_hash: ${{ steps.extract.outputs.expected_hash }} keystore: ${{ steps.gen.outputs.keystore }} keystore_password: ${{ steps.gen.outputs.keystore_password }} key_password: ${{ steps.gen.outputs.key_password }} steps: - name: Generate temporary keystore if: github.event_name == 'pull_request' id: gen run: | KEYSTORE_PASSWORD=$(openssl rand -hex 32) KEY_PASSWORD=$(openssl rand -hex 32) echo "keystore_password=$KEYSTORE_PASSWORD" >> $GITHUB_OUTPUT echo "key_password=$KEY_PASSWORD" >> $GITHUB_OUTPUT keytool -genkeypair \ -alias pr-key \ -keyalg RSA -keysize 2048 \ -validity 1 \ -storepass "$KEYSTORE_PASSWORD" \ -keypass "$KEY_PASSWORD" \ -dname "CN=KernelSU PR Build" \ -storetype JKS \ -keystore pr-key.jks echo "keystore=$(base64 -w 0 pr-key.jks)" >> $GITHUB_OUTPUT - name: Extract certificate hash and size if: github.event_name == 'pull_request' id: extract env: STORE_PASS: ${{ steps.gen.outputs.keystore_password }} run: | # Export DER certificate keytool -exportcert \ -alias pr-key \ -keystore pr-key.jks \ -storepass "$STORE_PASS" \ -file pr-cert.der # Calculate size in hex SIZE_DEC=$(stat -c%s pr-cert.der) SIZE_HEX=$(printf '0x%04x' "$SIZE_DEC") echo "expected_size=$SIZE_HEX" >> $GITHUB_OUTPUT # Calculate SHA256 hash HASH=$(sha256sum pr-cert.der | awk '{print $1}') echo "expected_hash=$HASH" >> $GITHUB_OUTPUT echo "Certificate size: $SIZE_HEX ($SIZE_DEC bytes)" echo "Certificate hash: $HASH" build-lkm: needs: generate-key uses: ./.github/workflows/build-lkm.yml with: expected_size2: ${{ needs.generate-key.outputs.expected_size || '' }} expected_hash2: ${{ needs.generate-key.outputs.expected_hash || '' }} secrets: inherit build-ksuinit: uses: ./.github/workflows/ksuinit.yml build-ksud: needs: [build-lkm, build-ksuinit] strategy: matrix: include: - target: aarch64-linux-android os: ubuntu-latest - target: x86_64-linux-android os: ubuntu-latest uses: ./.github/workflows/ksud.yml with: target: ${{ matrix.target }} os: ${{ matrix.os }} build-ksud-extra: needs: [build-lkm, build-ksuinit] strategy: matrix: include: - target: x86_64-pc-windows-gnu # windows pc os: ubuntu-latest - target: x86_64-apple-darwin # Intel mac os: macos-latest - target: aarch64-apple-darwin # M chip mac os: macos-latest - target: aarch64-unknown-linux-musl # arm64 Linux os: ubuntu-latest - target: x86_64-unknown-linux-musl # x86 Linux os: ubuntu-latest uses: ./.github/workflows/ksud.yml with: target: ${{ matrix.target }} os: ${{ matrix.os }} build-manager: needs: [build-ksud, generate-key] if: always() && needs.build-ksud.result == 'success' && needs.generate-key.result == 'success' runs-on: ubuntu-latest defaults: run: working-directory: ./manager steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup need_upload id: need_upload run: | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then echo "UPLOAD=true" >> $GITHUB_OUTPUT else echo "UPLOAD=false" >> $GITHUB_OUTPUT fi - name: Write key if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }} run: | if [ ! -z "${{ secrets.KEYSTORE }}" ]; then { echo KEYSTORE_PASSWORD='${{ secrets.KEYSTORE_PASSWORD }}' echo KEY_ALIAS='${{ secrets.KEY_ALIAS }}' echo KEY_PASSWORD='${{ secrets.KEY_PASSWORD }}' echo KEYSTORE_FILE='key.jks' } >> gradle.properties echo ${{ secrets.KEYSTORE }} | base64 -d > key.jks fi - name: Write PR key if: github.event_name == 'pull_request' env: PR_KEYSTORE: ${{ needs.generate-key.outputs.keystore }} PR_KEYSTORE_PASSWORD: ${{ needs.generate-key.outputs.keystore_password }} PR_KEY_PASSWORD: ${{ needs.generate-key.outputs.key_password }} run: | echo "$PR_KEYSTORE" | base64 -d > pr-key.jks { echo KEYSTORE_PASSWORD="$PR_KEYSTORE_PASSWORD" echo KEY_ALIAS='pr-key' echo KEY_PASSWORD="$PR_KEY_PASSWORD" echo KEYSTORE_FILE='pr-key.jks' } >> gradle.properties - name: Setup Java uses: actions/setup-java@v5 with: distribution: temurin java-version: 21 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Download arm64 ksud uses: actions/download-artifact@v7 with: name: ksud-aarch64-linux-android path: . - name: Download x86_64 ksud uses: actions/download-artifact@v7 with: name: ksud-x86_64-linux-android path: . - name: Copy ksud to app jniLibs run: | mkdir -p app/src/main/jniLibs/arm64-v8a mkdir -p app/src/main/jniLibs/x86_64 cp -f ../aarch64-linux-android/release/ksud ../manager/app/src/main/jniLibs/arm64-v8a/libksud.so cp -f ../x86_64-linux-android/release/ksud ../manager/app/src/main/jniLibs/x86_64/libksud.so - name: Build with Gradle run: | if [ "${{ github.event_name }}" = "pull_request" ]; then ./gradlew clean assembleRelease -PIS_PR_BUILD=true else ./gradlew clean assembleRelease fi - name: Upload build artifact if: ${{ github.event_name == 'pull_request' || (github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev')) || github.ref_type == 'tag' }} uses: actions/upload-artifact@v6 with: name: manager path: manager/app/build/outputs/apk/release/*.apk - name: Upload mappings uses: actions/upload-artifact@v6 if: ${{ ( github.event_name != 'pull_request' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/dev' )) || github.ref_type == 'tag' }} with: name: "mappings" path: "manager/app/build/outputs/mapping/release/" - name: Bot session cache if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' id: bot_session_cache uses: actions/cache@v5 with: path: scripts/ksubot.session key: ${{ runner.os }}-bot-session - name: Upload to telegram if: github.event_name != 'pull_request' && steps.need_upload.outputs.UPLOAD == 'true' env: CHAT_ID: ${{ secrets.CHAT_ID }} BOT_TOKEN: ${{ secrets.BOT_TOKEN }} MESSAGE_THREAD_ID: ${{ secrets.MESSAGE_THREAD_ID }} COMMIT_MESSAGE: ${{ github.event.head_commit.message }} COMMIT_URL: ${{ github.event.head_commit.url }} RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} TITLE: Manager BRANCH: ${{ github.ref_name }} run: | if [ ! -z "${{ secrets.BOT_TOKEN }}" ]; then export VERSION=$(git rev-list --count HEAD) APK=$(find ./app/build/outputs/apk/release -name "*.apk") pip3 install telethon python3 $GITHUB_WORKSPACE/scripts/ksubot.py $APK fi ================================================ FILE: .github/workflows/clang-format.yml ================================================ name: ClangFormat check on: push: branches: - 'main' paths: - '.github/workflows/clang-format.yml' - 'kernel/**/*.c' - 'kernel/**/*.h' pull_request: branches: - 'main' paths: - '.github/workflows/clang-format.yml' - 'kernel/**/*.c' - 'kernel/**/*.h' - 'kernel/.clang-format' permissions: checks: write jobs: format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: check working-directory: kernel run: make check-format ================================================ FILE: .github/workflows/clippy.yml ================================================ name: Clippy check on: push: branches: - main paths: - '.github/workflows/clippy.yml' - 'userspace/**' pull_request: branches: - main paths: - '.github/workflows/clippy.yml' - 'userspace/**' env: RUSTFLAGS: '-Dwarnings' CROSS_NO_WARNINGS: '0' jobs: clippy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - run: rustup update stable - uses: Swatinem/rust-cache@v2 with: workspaces: userspace/ksud - name: Install cross run: | RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - name: Run clippy run: | cross clippy --manifest-path userspace/ksud/Cargo.toml --target aarch64-linux-android --release cross clippy --manifest-path userspace/ksud/Cargo.toml --target x86_64-linux-android --release cross clippy --manifest-path userspace/ksuinit/Cargo.toml --target aarch64-linux-android --release cross clippy --manifest-path userspace/ksuinit/Cargo.toml --target x86_64-linux-android --release ================================================ FILE: .github/workflows/ddk-lkm.yml ================================================ name: Build KernelSU Kernel Module on: workflow_call: inputs: kmi: description: 'KMI version' required: true type: string ddk_release: description: 'DDK release version' required: false default: '20260313' type: string expected_size2: description: 'Second expected signature size (for PR builds)' required: false default: '' type: string expected_hash2: description: 'Second expected signature hash (for PR builds)' required: false default: '' type: string jobs: build-kernelsu-ko: name: Build kernelsu.ko for ${{ inputs.kmi }} runs-on: ubuntu-latest container: image: ghcr.io/ylarod/ddk-min:${{ inputs.kmi }}-${{ inputs.ddk_release }} options: --privileged steps: - name: Checkout source code uses: actions/checkout@v6 - name: Build kernelsu.ko env: EXPECTED_SIZE2: ${{ inputs.expected_size2 }} EXPECTED_HASH2: ${{ inputs.expected_hash2 }} run: | git config --global --add safe.directory /__w/KernelSU/KernelSU cd kernel echo "=== Building kernelsu.ko for KMI: ${{ inputs.kmi }} ===" EXTRA_ARGS="" if [ -n "$EXPECTED_SIZE2" ] && [ -z "$EXPECTED_HASH2" ]; then echo "ERROR: expected_hash2 must be provided when expected_size2 is set." exit 1 fi if [ -z "$EXPECTED_SIZE2" ] && [ -n "$EXPECTED_HASH2" ]; then echo "ERROR: expected_size2 must be provided when expected_hash2 is set." exit 1 fi if [ -n "$EXPECTED_SIZE2" ]; then EXTRA_ARGS="KSU_EXPECTED_SIZE2=$EXPECTED_SIZE2 KSU_EXPECTED_HASH2=$EXPECTED_HASH2" fi CONFIG_KSU=m CC=clang make $EXTRA_ARGS echo "=== Build completed ===" # Create output directory in GitHub workspace mkdir -p /github/workspace/out # Copy with KMI-specific naming OUTPUT_NAME="${{ inputs.kmi }}_kernelsu.ko" cp kernelsu.ko "/github/workspace/out/$OUTPUT_NAME" echo "Copied to: /github/workspace/out/$OUTPUT_NAME" ls -la "/github/workspace/out/$OUTPUT_NAME" echo "Size: $(du -h "/github/workspace/out/$OUTPUT_NAME" | cut -f1)" llvm-strip -d "/github/workspace/out/$OUTPUT_NAME" echo "Size after stripping: $(du -h "/github/workspace/out/$OUTPUT_NAME" | cut -f1)" - name: Upload kernelsu.ko artifact uses: actions/upload-artifact@v6 with: name: ${{ inputs.kmi }}-lkm path: /github/workspace/out/${{ inputs.kmi }}_kernelsu.ko ================================================ FILE: .github/workflows/deploy-website.yml ================================================ name: Deploy Website on: push: branches: - main - website paths: - '.github/workflows/deploy-website.yml' - 'website/**' workflow_dispatch: # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages permissions: contents: read pages: write id-token: write # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. concurrency: group: pages cancel-in-progress: false jobs: # Build job build: runs-on: ubuntu-latest defaults: run: working-directory: ./website steps: - name: Checkout uses: actions/checkout@v6 with: fetch-depth: 0 # Not needed if lastUpdated is not enabled - name: Setup Node uses: actions/setup-node@v6 with: node-version: latest cache: yarn # or pnpm / yarn cache-dependency-path: website/yarn.lock - name: Setup Pages uses: actions/configure-pages@v5 - name: Install dependencies run: yarn install --frozen-lockfile - name: Build with VitePress run: | yarn docs:build touch docs/.vitepress/dist/.nojekyll - name: Upload artifact uses: actions/upload-pages-artifact@v4 with: path: website/docs/.vitepress/dist # Deployment job deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} needs: build runs-on: ubuntu-latest name: Deploy steps: - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v4 ================================================ FILE: .github/workflows/ksud.yml ================================================ name: Build ksud on: workflow_call: inputs: target: required: true type: string os: required: false type: string default: ubuntu-latest pack_lkm: required: false type: boolean default: true pack_ksuinit: required: false type: boolean default: true use_cache: required: false type: boolean default: true jobs: build: runs-on: ${{ inputs.os }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Download artifacts uses: actions/download-artifact@v7 - name: Prepare LKM fies if: ${{ inputs.pack_lkm }} run: | cp android*-lkm/*_kernelsu.ko ./userspace/ksud/bin/aarch64/ - name: Prepare ksuinit if: ${{ inputs.pack_ksuinit }} run: | mv ksuinit/*/release/ksuinit ./userspace/ksud/bin/aarch64/ - name: Setup rustup run: | rustup update stable rustup target add x86_64-apple-darwin rustup target add aarch64-apple-darwin - uses: Swatinem/rust-cache@v2 with: workspaces: userspace/ksud cache-targets: false - name: Install cross run: | RUSTFLAGS="" cargo install cross --git https://github.com/cross-rs/cross --rev 66845c1 - name: Build ksud run: CROSS_NO_WARNINGS=0 cross build --target ${{ inputs.target }} --release --manifest-path ./userspace/ksud/Cargo.toml - name: Upload ksud artifact uses: actions/upload-artifact@v6 with: name: ksud-${{ inputs.target }} path: userspace/ksud/target/**/release/ksud* ================================================ FILE: .github/workflows/ksuinit.yml ================================================ name: Build ksuinit on: workflow_call: inputs: os: required: false type: string default: ubuntu-latest jobs: build: runs-on: ${{ inputs.os }} steps: - uses: actions/checkout@v6 with: fetch-depth: 0 - name: Setup rustup run: | rustup update stable rustup target add aarch64-unknown-linux-musl - uses: Swatinem/rust-cache@v2 with: workspaces: userspace/ksuinit cache-targets: false - name: Build ksuinit run: | LLVM_BIN=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin export CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$LLVM_BIN/aarch64-linux-android26-clang" export RUSTFLAGS="-C link-arg=-no-pie" cd userspace/ksuinit cargo build --target=aarch64-unknown-linux-musl --release - name: Upload ksuinit artifact uses: actions/upload-artifact@v6 with: name: ksuinit path: userspace/ksuinit/target/**/release/ksuinit* ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: - "v*" workflow_dispatch: jobs: build-manager: uses: ./.github/workflows/build-manager.yml secrets: inherit release: needs: - build-manager runs-on: ubuntu-latest steps: - name: Download artifacts uses: actions/download-artifact@v7 - name: Rename ksud run: | mkdir -p ksud for dir in ./ksud-*; do if [ -d "$dir" ]; then echo "----- Rename $dir -----" ksud_platform_name=$(basename "$dir") find "$dir" -type f -name "ksud" -path "*/release/*" | while read -r ksud_file; do if [ -f "$ksud_file" ]; then mv "$ksud_file" "ksud/$ksud_platform_name" fi done fi done - name: Display structure of downloaded files run: ls -R - name: release uses: softprops/action-gh-release@v2 with: files: | manager/*.apk android*-lkm/*_kernelsu.ko ksud/ksud-* ksuinit/** ================================================ FILE: .github/workflows/rustfmt.yml ================================================ name: Rustfmt check on: push: branches: - 'main' paths: - '.github/workflows/rustfmt.yml' - 'userspace/**' pull_request: branches: - 'main' paths: - '.github/workflows/rustfmt.yml' - 'userspace/**' permissions: checks: write jobs: format: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - uses: dtolnay/rust-toolchain@nightly with: components: rustfmt - uses: LoliGothick/rustfmt-check@master with: token: ${{ github.token }} working-directory: userspace/ksud - uses: LoliGothick/rustfmt-check@master with: token: ${{ github.token }} working-directory: userspace/ksuinit ================================================ FILE: .github/workflows/shellcheck.yml ================================================ name: ShellCheck on: push: branches: - 'main' paths: - '.github/workflows/shellcheck.yml' - '**/*.sh' pull_request: branches: - 'main' paths: - '.github/workflows/shellcheck.yml' - '**/*.sh' jobs: shellcheck: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - name: Run ShellCheck uses: ludeeus/action-shellcheck@2.0.0 with: ignore_names: gradlew ignore_paths: ./userspace/ksud/src/installer.sh ================================================ FILE: .gitignore ================================================ .idea .vscode CLAUDE.md AGENTS.md .DS_Store ================================================ FILE: LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . ================================================ FILE: SECURITY.md ================================================ # Reporting Security Issues The KernelSU team and community take security bugs in KernelSU seriously. We appreciate your efforts to responsibly disclose your findings, and will make every effort to acknowledge your contributions. To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/tiann/KernelSU/security/advisories/new) tab, or you can mailto [weishu](mailto:twsxtd@gmail.com) directly. The KernelSU team will send a response indicating the next steps in handling your report. After the initial reply to your report, the security team will keep you informed of the progress towards a fix and full announcement, and may ask for additional information or guidance. ================================================ FILE: docs/README.md ================================================ **English** | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo A kernel-based root solution for Android devices. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Features 1. Kernel-based `su` and root access management. 2. Module system based on [metamodules](https://kernelsu.org/guide/metamodule.html): Pluggable infrastructure for systemless modifications. 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Lock up the root power in a cage. ## Compatibility state KernelSU officially supports Android GKI 2.0 devices (kernel 5.10+). Older kernels (4.14+) are also supported, but the kernel will need to be built manually. With this, WSA, ChromeOS, and container-based Android are all supported. Currently, only the `arm64-v8a` and `x86_64` architectures are supported. ## Usage - [Installation](https://kernelsu.org/guide/installation.html) - [How to build](https://kernelsu.org/guide/how-to-build.html) - [Official website](https://kernelsu.org/) ## Translation To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR of Manager's translation is no longer accepted, because it will conflict with Weblate. ## Discussion - Telegram: [@KernelSU](https://t.me/KernelSU) ## Security For information on reporting security vulnerabilities in KernelSU, see [SECURITY.md](/SECURITY.md). ## License - Files under the `kernel` directory are [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - All other parts except the `kernel` directory are [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Credits - [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): The KernelSU idea. - [Magisk](https://github.com/topjohnwu/Magisk): The powerful root tool. - [genuine](https://github.com/brevent/genuine/): APK v2 signature validation. - [Diamorphine](https://github.com/m0nad/Diamorphine): Some rootkit skills. ================================================ FILE: docs/README_CN.md ================================================ [English](README.md) | [Español](README_ES.md) | **简体中文** | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo 一个 Android 上基于内核的 root 方案。 [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## 特性 - 基于内核的 `su` 和权限管理。 - 基于 [metamodules](https://kernelsu.org/zh_CN/guide/metamodule.html) 的模块系统:可插拔的模块架构。 - [App Profile](https://kernelsu.org/zh_CN/guide/app-profile.html): 把 Root 权限关进笼子里。 ## 兼容状态 KernelSU 官方支持 GKI 2.0 的设备(内核版本5.10以上);旧内核也是兼容的(最低4.14+),不过需要自己编译内核。 WSA, ChromeOS 和运行在容器上的 Android 也可以与 KernelSU 一起工作。 目前支持架构 : `arm64-v8a` 和 `x86_64`。 ## 使用方法 - [安装教程](https://kernelsu.org/zh_CN/guide/installation.html) - [如何构建?](https://kernelsu.org/zh_CN/guide/how-to-build.html) - [官方网站](https://kernelsu.org/zh_CN/) ## 参与翻译 要将 KernelSU 翻译成您的语言,或完善现有的翻译,请使用 [Weblate](https://hosted.weblate.org/engage/kernelsu/)。现已不再接受有关管理器翻译的PR,因为这会与Weblate冲突。 ## 讨论 - Telegram: [@KernelSU](https://t.me/KernelSU) ## 安全性 有关报告 KernelSU 安全漏洞的信息,请参阅 [SECURITY.md](/SECURITY.md)。 ## 许可证 - 目录 `kernel` 下所有文件为 [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。 - 除 `kernel` 目录的其他部分均为 [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。 ## 鸣谢 - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的灵感。 - [Magisk](https://github.com/topjohnwu/Magisk):强大的 root 工具箱。 - [genuine](https://github.com/brevent/genuine/):apk v2 签名验证。 - [Diamorphine](https://github.com/m0nad/Diamorphine):一些 rootkit 技巧。 ================================================ FILE: docs/README_ES.md ================================================ [English](README.md) | **Español** | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Una solución root basada en el kernel para dispositivos Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localización-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/Licencia-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Características 1. Binario `su` basado en el kernel y gestión de acceso root. 2. Sistema de módulos basado en [metamodules](https://kernelsu.org/guide/metamodule.html): Infraestructura conectable para modificaciones sin sistema. ## Estado de compatibilidad **KernelSU** soporta de forma oficial dispositivos Android con **GKI 2.0** (a partir de la versión **5.10** del kernel). Los kernels antiguos (a partir de la versión **4.14**) también son compatibles, pero necesitas compilarlos por tu cuenta. Con esto, WSA, ChromeOS y Android basado en contenedores están todos compatibles. Actualmente, solo se admiten las arquitecturas `arm64-v8a` y `x86_64`. ## Uso - [¿Cómo instalarlo?](https://kernelsu.org/guide/installation.html) - [¿Cómo compilarlo?](https://kernelsu.org/guide/how-to-build.html) - [Site oficial](https://kernelsu.org/) ## Traducción Para ayudar a traducir KernelSU o mejorar las traducciones existentes, utilice [Weblate](https://hosted.weblate.org/engage/kernelsu/). Ya no se aceptan PR de la traducción de Manager porque entrará en conflicto con Weblate. ## Discusión - Telegram: [@KernelSU](https://t.me/KernelSU) ## Seguridad Para obtener información sobre cómo informar vulnerabilidades de seguridad en KernelSU, consulte [SECURITY.md](/SECURITY.md). ## Licencia - Los archivos bajo el directorio `kernel` están licenciados bajo [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Todas las demás partes, a excepción del directorio `kernel`, están licenciados bajo [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Créditos - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): la idea de KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): la poderosa herramienta root. - [genuine](https://github.com/brevent/genuine/): validación de firma apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): algunas habilidades de rootkit. ================================================ FILE: docs/README_ID.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | **Indonesia** | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Solusi root berbasis Kernel untuk perangkat Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Fitur 1. Manajemen akses root dan `su` berbasis kernel. 2. Sistem modul berdasarkan [metamodules](https://kernelsu.org/id_ID/guide/metamodule.html): Infrastruktur pluggable untuk modifikasi systemless. 3. [Profil Aplikasi](https://kernelsu.org/guide/app-profile.html): Kunci daya root di dalam sangkar. ## Status Kompatibilitas KernelSU secara resmi mendukung perangkat Android GKI 2.0 (dengan kernel 5.10+), kernel lama (4.14+) juga kompatibel, tetapi Anda perlu membuat kernel sendiri. WSA, ChromeOS, dan Android berbasis wadah juga dapat bekerja dengan KernelSU terintegrasi. Dan ABI yang didukung saat ini adalah: `arm64-v8a` dan `x86_64` ## Penggunaan - [Petunjuk Instalasi](https://kernelsu.org/id_ID/guide/installation.html) - [Bagaimana cara membuat?](https://kernelsu.org/id_ID/guide/how-to-build.html) - [Situs Web Resmi](https://kernelsu.org/id_ID/) ## Terjemahan Untuk menerjemahkan KernelSU ke dalam bahasa Anda atau menyempurnakan terjemahan yang sudah ada, harap gunakan [Weblat](https://hosted.weblate.org/engage/kernelsu/). ## Diskusi - Telegram: [@KernelSU](https://t.me/KernelSU) ## Lisensi - File di bawah direktori `kernel` adalah [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Semua bagian lain kecuali direktori `kernel` adalah [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Kredit - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ide KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): alat root yang ampuh. - [genuine](https://github.com/brevent/genuine/): validasi tanda tangan apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): beberapa keterampilan rootkit. ================================================ FILE: docs/README_IN.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | **हिंदी** | [Italiano](README_IT.md) # KernelSU logo Android उपकरणों के लिए कर्नेल-आधारित रूट समाधान। [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## विशेषताएँ 1. कर्नेल-आधारित `su` और रूट एक्सेस प्रबंधन। 2. [metamodules](https://kernelsu.org/guide/metamodule.html) पर आधारित मॉड्यूल प्रणाली: Systemless संशोधनों के लिए प्लगेबल इंफ्रास्ट्रक्चर। 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Root शक्ति को पिंजरे में बंद कर दो। ## अनुकूलता अवस्था KernelSU आधिकारिक तौर पर Android GKI 2.0 डिवाइस (कर्नेल 5.10+) का समर्थन करता है। पुराने कर्नेल (4.14+) भी संगत हैं, लेकिन कर्नेल को मैन्युअल रूप से बनाना होगा। इसके साथ, WSA, ChromeOS और कंटेनर-आधारित Android सभी समर्थित हैं। वर्तमान में, केवल `arm64-v8a` और `x86_64` समर्थित हैं। ## प्रयोग - [स्थापना निर्देश](https://kernelsu.org/guide/installation.html) - [कैसे बनाना है ?](https://kernelsu.org/guide/how-to-build.html) - [आधिकारिक वेबसाइट](https://kernelsu.org/) ## अनुवाद करना KernelSU का अनुवाद करने या मौजूदा अनुवादों को बेहतर बनाने में सहायता के लिए, कृपया इसका उपयोग करें [Weblate](https://hosted.weblate.org/engage/kernelsu/). ## बहस - Telegram: [@KernelSU](https://t.me/KernelSU) ## लाइसेंस - `Kernel` निर्देशिका के अंतर्गत फ़ाइलें हैं [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) - `Kernel` निर्देशिका को छोड़कर अन्य सभी भाग हैं [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html) ## आभार सूची - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU विचार। - [Magisk](https://github.com/topjohnwu/Magisk): शक्तिशाली root उपकरण। - [genuine](https://github.com/brevent/genuine/): apk v2 हस्ताक्षर सत्यापन। - [Diamorphine](https://github.com/m0nad/Diamorphine): कुछ रूटकिट कौशल। ================================================ FILE: docs/README_IT.md ================================================ [English](REAME.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | **Italiano** # KernelSU logo Una soluzione per il root basata sul kernel per i dispositivi Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Canale Telegraml](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![Licenza componenti kernel: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![Licenza elementi non kern](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Funzionalità 1. `su` e accesso root basato sul kernel. 2. Sistema di moduli basato su [metamodules](https://kernelsu.org/guide/metamodule.html): Infrastruttura modulare per modifiche systemless. 3. [App profile](https://kernelsu.org/guide/app-profile.html): Limita i poteri dell'accesso root a permessi specifici. ## Compatibilità KernelSU supporta ufficialmente i dispositivi Android GKI 2.0 (kernel 5.10 o superiore). I kernel precedenti (kernel 4.14+) sono anche compatibili, ma il kernel deve essere compilato manualmente. Questo implica che WSA, ChromeOS e tutti le varianti di Android basate su container e virtualizzazione sono supportate. Allo stato attuale solo le architetture a 64-bit ARM (arm64-v8a) e x86 (x86_64) sono supportate. ## Utilizzo - [Istruzioni per l'installazione](https://kernelsu.org/guide/installation.html) - [Come compilare manualmente?](https://kernelsu.org/guide/how-to-build.html) - [Sito web ufficiale](https://kernelsu.org/) ## Traduzioni Per aiutare a tradurre KernelSU o migliorare le traduzioni esistenti, si è pregati di utilizzare To help translate KernelSU or improve existing translations, please use [Weblate](https://hosted.weblate.org/engage/kernelsu/). Le richieste di pull delle traduzioni del manager non saranno più accettate perché sarebbero in conflitto con Weblate. ## Discussione - Telegram: [@KernelSU](https://t.me/KernelSU) ## Securezza Per informazioni riguardo la segnalazione di vulnerabilità di sicurezza per KernelSU, leggi [SECURITY.md](/SECURITY.md). ## Licenza - I file nella cartella `kernel` sono forniti secondo la licenza [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Tutte le altre parti, ad eccezione della certella `kernel`, seguono la licenza [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Riconoscimenti e attribuzioni - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): l'idea alla base di KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): la potente utilità per il root. - [genuine](https://github.com/brevent/genuine/): verifica della firma apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): alcune capacità di rootkit. ================================================ FILE: docs/README_IW.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | **עברית** | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo פתרון לניהול root מבוסס על Kernel עבור מכשירי Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## תכונות 1. ניהול root ו־`su` מבוססים על Kernel. 2. מערכת מודולים מבוססת [metamodules](https://kernelsu.org/guide/metamodule.html): תשתית מודולרית לשינויים systemless. 3. [פרופיל אפליקציה](https://kernelsu.org/guide/app-profile.html): נעילת גישת root בכלוב. ## מצב תאימות KernelSU תומך במכשירי Android GKI 2.0 (kernel 5.10+) באופן רשמי. לליבות ישנות (4.14+) יש גם תאימות, אך יידרש לבנות את הליבה באופן ידני. באמצעות זה, תמיכה זמינה גם ל-WSA, ChromeOS ומכשירי Android המבוססים על מיכלים. כרגע, רק `arm64-v8a` ו־`x86_64` נתמכים. ## שימוש - [הוראות התקנה](https://kernelsu.org/guide/installation.html) - [איך לבנות?](https://kernelsu.org/guide/how-to-build.html) - [האתר רשמי](https://kernelsu.org/) ## תרגום כדי לעזור בתרגום של KernelSU או לשפר תרגומים קיימים, יש להשתמש ב-[Weblate](https://hosted.weblate.org/engage/kernelsu/). ## דיון - Telegram: [@KernelSU](https://t.me/KernelSU) ## רשיון - קבצים תחת הספרייה `kernel` מוגנים על פי [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - כל החלקים האחרים, למעט הספרייה `kernel`, מוגנים על פי [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## קרדיטים - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): הרעיון של KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): הכלי הסופר חזק לניהול root. - [genuine](https://github.com/brevent/genuine/): אימות חתימת apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): כמה יכולות רוט. ================================================ FILE: docs/README_JP.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | **日本語** | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Android におけるカーネルベースの root ソリューションです。 [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## 特徴 1. カーネルベースの `su` と権限管理。 2. [metamodules](https://kernelsu.org/ja_JP/guide/metamodule.html) に基づくモジュールシステム: プラグイン可能なシステムレス変更インフラストラクチャ。 3. [アプリのプロファイル](https://kernelsu.org/guide/app-profile.html): root の権限をケージ内に閉じ込めます。 ## 対応状況 KernelSU は GKI 2.0 デバイス(カーネルバージョン 5.10 以上)を公式にサポートしています。古いカーネル(4.14以上)とも互換性がありますが、自分でカーネルをビルドする必要があります。 WSA 、ChromeOS とコンテナ上で動作する Android でも KernelSU を統合して動かせます。 現在サポートしているアーキテクチャは `arm64-v8a` および `x86_64` です。 ## 使用方法 - [インストール方法はこちら](https://kernelsu.org/ja_JP/guide/installation.html) - [ビルド方法はこちら](https://kernelsu.org/guide/how-to-build.html) - [公式サイト](https://kernelsu.org/ja_JP/) ## 翻訳 KernelSU をあなたの言語に翻訳するか、既存の翻訳を改善するには、[Weblate](https://hosted.weblate.org/engage/kernelsu/) を使用してください。Manager翻訳した PR は、Weblate と競合するため受け入れられなくなりました。 ## ディスカッション - Telegram: [@KernelSU](https://t.me/KernelSU) ## ライセンス - `kernel` ディレクトリの下にあるすべてのファイル: [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)。 - `kernel` ディレクトリ以外のすべてのファイル: [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html)。 ## クレジット - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU のアイデア元。 - [Magisk](https://github.com/topjohnwu/Magisk):強力な root ツール。 - [genuine](https://github.com/brevent/genuine/):apk v2 の署名検証。 - [Diamorphine](https://github.com/m0nad/Diamorphine): rootkit のスキル。 ================================================ FILE: docs/README_KR.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | **한국어** | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo 안드로이드 기기에서 사용되는 커널 기반 루팅 솔루션입니다. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## 기능들 1. 커널 기반 `su` 및 루트 액세스 관리. 2. [metamodules](https://kernelsu.org/guide/metamodule.html) 기반 모듈 시스템: 플러그인 가능한 시스템리스 수정 인프라. 3. [App Profile](https://kernelsu.org/guide/app-profile.html): 루트 권한을 케이지에 가둡니다. ## 호환 상태 KernelSU는 공식적으로 안드로이드 GKI 2.0 디바이스(커널 5.10 이상)를 지원합니다. 오래된 커널(4.14 이상)도 사용할 수 있지만, 커널을 수동으로 빌드해야 합니다. KernelSU는 WSA, ChromeOS, 컨테이너 기반 안드로이드 모두를 지원합니다. 현재는 `arm64-v8a`와 `x86_64`만 지원됩니다. ## 사용 방법 - [설치 방법](https://kernelsu.org/guide/installation.html) - [어떻게 빌드하나요?](https://kernelsu.org/guide/how-to-build.html) - [공식 웹사이트](https://kernelsu.org/) ## 번역 KernelSU 번역을 돕거나 기존 번역을 개선하려면 [Weblate](https://hosted.weblate.org/engage/kernelsu/)를 이용해 주세요. 매니저의 번역은 Weblate와 충돌할 수 있으므로 더 이상 허용되지 않습니다. ## 토론 - 텔레그램: [@KernelSU](https://t.me/KernelSU) ## 보안 KernelSU의 보안 취약점 보고에 대한 자세한 내용은 [SECURITY.md](/SECURITY.md)를 참조하세요. ## 저작권 - `kernel` 디렉터리 아래의 파일은 [GPL-2.0 전용](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)입니다. - `kernel` 디렉토리를 제외한 다른 모든 부분은 [GPL-3.0-이상](https://www.gnu.org/licenses/gpl-3.0.html)입니다. ## 크래딧 - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU의 아이디어. - [Magisk](https://github.com/topjohnwu/Magisk): 강력한 루팅 도구. - [genuine](https://github.com/brevent/genuine/): apk v2 서명 유효성 검사. - [Diamorphine](https://github.com/m0nad/Diamorphine): 일부 rootkit 스킬. ================================================ FILE: docs/README_PL.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | **Polski** | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Rozwiązanie root oparte na jądrze dla urządzeń z systemem Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Cechy 1. Oparte na jądrze `su` i zarządzanie dostępem roota. 2. System modułów oparty na [metamodules](https://kernelsu.org/guide/metamodule.html): Wtykowa infrastruktura dla modyfikacji systemless. ## Kompatybilność KernelSU oficjalnie obsługuje urządzenia z Androidem GKI 2.0 (z jądrem 5.10+), starsze jądra (4.14+) są również kompatybilne, ale musisz sam skompilować jądro. WSA i Android oparty na kontenerach również powinny działać ze zintegrowanym KernelSU. Aktualnie obsługiwane ABI to : `arm64-v8a` i `x86_64`. ## Użycie - [Instalacja](https://kernelsu.org/guide/installation.html) - [Jak skompilować?](https://kernelsu.org/guide/how-to-build.html) ## Tłumaczenie Aby pomóc w tłumaczeniu KernelSU lub ulepszyć istniejące tłumaczenia, użyj [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR tłumaczenia Managera nie jest już akceptowany, ponieważ będzie kolidował z Weblate. ## Dyskusja - Telegram: [@KernelSU](https://t.me/KernelSU) ## Bezpieczeństwo Informacje na temat zgłaszania luk w zabezpieczeniach w KernelSU można znaleźć w pliku [SECURITY.md](/SECURITY.md). ## Licencja - Pliki w katalogu `kernel` są na licencji [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Wszystkie inne części poza katalogiem `kernel` są na licencji [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Podziękowania - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): pomysłodawca KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): implementacja sepolicy. - [genuine](https://github.com/brevent/genuine/): walidacja podpisu apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): cenna znajomość rootkitów. ================================================ FILE: docs/README_PT-BR.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | **Português (Brasil)** | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Uma solução root baseada em kernel para dispositivos Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localização-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Seguir-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/Licença-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Características 1. `su` e gerenciamento de acesso root baseado em kernel. 2. Sistema de módulos baseado em [metamodules](https://kernelsu.org/pt_BR/guide/metamodule.html): Infraestrutura plugável para modificações systemless. 3. [Perfil do Aplicativo](https://kernelsu.org/pt_BR/guide/app-profile.html): Tranque o poder root em uma gaiola. ## Estado de compatibilidade O KernelSU oferece suporte oficial a dispositivos Android GKI 2.0 (kernel 5.10+). Kernels mais antigos (4.14+) também são compatíveis, mas será necessário construir o kernel manualmente. Com isso, WSA, ChromeOS e Android baseado em contêiner são todos suportados. Atualmente, apenas as arquiteturas `arm64-v8a` e `x86_64` são compatíveis. ## Uso - [Instalação](https://kernelsu.org/pt_BR/guide/installation.html) - [Como compilar](https://kernelsu.org/pt_BR/guide/how-to-build.html) - [Site oficial](https://kernelsu.org/pt_BR/) ## Tradução Para contribuir com a tradução do KernelSU ou aprimorar traduções existentes, por favor, use o [Weblate](https://hosted.weblate.org/engage/kernelsu/). PR para a tradução do Manager não são mais aceitas, pois podem entrar em conflito com o Weblate. ## Discussão - Telegram: [@KernelSU](https://t.me/KernelSU) ## Segurança Para obter informações sobre como relatar vulnerabilidades de segurança do KernelSU, consulte [SECURITY.md](/SECURITY.md). ## Licença - Os arquivos no diretório `kernel` são [GPL-2.0-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Todas as outras partes, exceto o diretório `kernel` são [GPL-3.0-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Créditos - [Kernel-Assisted Superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): A ideia do KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): A poderosa ferramenta root. - [genuine](https://github.com/brevent/genuine/): Validação de assinatura APK v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): Algumas habilidades de rootkit. ================================================ FILE: docs/README_RU.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | **Русский** | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Решение на основе ядра root для Android-устройств. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Особенности 1. Управление `su` и root-доступом на основе ядра. 2. Система модулей на основе [metamodules](https://kernelsu.org/ru_RU/guide/metamodule.html): Подключаемая инфраструктура для безсистемных модификаций. 3. [Профиль приложений](https://kernelsu.org/ru_RU/guide/app-profile.html): Запри корневую силу в клетке. ## Совместимость KernelSU официально поддерживает устройства на базе Android GKI 2.0 (с ядром 5.10+), старые ядра (4.14+) также совместимы, но для этого необходимо собрать ядро самостоятельно. WSA и Android на основе контейнеров также должны работать с интегрированным KernelSU. В настоящее время поддерживаются следующие ABI: `arm64-v8a` и `x86_64`. ## Использование - [Установка](https://kernelsu.org/ru_RU/guide/installation.html) - [Как собрать?](https://kernelsu.org/ru_RU/guide/how-to-build.html) - [официальный сайт](https://kernelsu.org/ru_RU/) ## Обсуждение - Telegram: [@KernelSU](https://t.me/KernelSU) ## Лицензия - Файлы в директории `kernel` [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Все остальные части, кроме директории `kernel` [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Благодарности - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): идея KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): реализация sepolicy. - [genuine](https://github.com/brevent/genuine/): проверка подписи apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): некоторые навыки руткита. ================================================ FILE: docs/README_TR.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | **Türkçe** | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Android cihazlar için kernel tabanlı root çözümü. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Özellikler 1. Kernel-tabanlı `su` ve root erişimi yönetimi. 2. [metamodules](https://kernelsu.org/guide/metamodule.html)'ye dayalı modül sistemi: Systemless modifikasyonlar için takılabilir altyapı. 3. [Uygulama profili](https://kernelsu.org/guide/app-profile.html): Root gücünü bir kafese kapatın. ## Uyumluluk Durumu KernelSU resmi olarak Android GKI 2.0 cihazlarını (5.10+ kernelli) destekler, eski kernellerle de (4.14+) uyumludur, ancak kerneli kendinizin derlemeniz gerekir. Bununla birlikte; WSA, ChromeOS ve konteyner tabanlı Android'in tamamı desteklenmektedir. Şimdilik sadece `arm64-v8a` ve `x86_64` desteklenmektedir. ## Kullanım - [Yükleme yönergeleri](https://kernelsu.org/guide/installation.html) - [Nasıl derlenir?](https://kernelsu.org/guide/how-to-build.html) - [Resmi WEB sitesi](https://kernelsu.org/) ## Çeviri KernelSU'nun başka dillere çevrilmesine veya mevcut çevirilerin iyileştirilmesine yardımcı olmak için lütfen [Weblate](https://hosted.weblate.org/engage/kernelsu/) kullanın. Yönetici uygulamasının PR ile çevirisi, Weblate ile çakışacağından artık kabul edilmeyecektir. ## Tartışma - Telegram: [@KernelSU](https://t.me/KernelSU) ## Güvenlik KernelSU'daki güvenlik açıklarını bildirme hakkında bilgi için, bkz [SECURITY.md](/SECURITY.md). ## Lisans - `kernel` klasöründeki dosyalar [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) lisansı altındadır. - `kernel` klasörü dışındaki bütün diğer bölümler [GPL-3-veya-sonraki](https://www.gnu.org/licenses/gpl-3.0.html) lisansı altındadır. ## Krediler - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): KernelSU fikri. - [Magisk](https://github.com/topjohnwu/Magisk): güçlü root aracı. - [genuine](https://github.com/brevent/genuine/): apk v2 imza doğrulaması. - [Diamorphine](https://github.com/m0nad/Diamorphine): bazı rootkit becerileri. ================================================ FILE: docs/README_TW.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | **繁體中文** | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | [Tiếng Việt](README_VI.md) | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU 標誌 一套基於 Android 裝置核心的 Root 解決方案。 [![最新版本](https://img.shields.io/github/v/release/tiann/KernelSU?label=%e7%99%bc%e8%a1%8c%e7%89%88%e6%9c%ac&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/%e6%9c%ac%e5%9c%9f%e5%8c%96%e7%bf%bb%e8%ad%af-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![頻道](https://img.shields.io/badge/%e8%bf%bd%e8%b9%a4-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![授權條款:《GPL v2》](https://img.shields.io/badge/%e6%8e%88%e6%ac%8a%e6%a2%9d%e6%ac%be-%E3%80%8AGPL%20v2%E3%80%8B-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub 授權條款](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## 特色功能 1. 以核心內 `su` 管理 Root 存取。 2. 以 [metamodules](https://kernelsu.org/zh_TW/guide/metamodule.html) 運作模組系統:可插拔的無系統修改基礎架構。 3. [App Profile](https://kernelsu.org/zh_TW/guide/app-profile.html):使 Root 掌握的生殺大權受制於此。 ## 相容事態 理論上採以 Android GKI 2.0 的裝置(核心版本 5.10+),皆受 KernelSU 支援;採以老舊核心版本(4.14+)的裝置在手動建置核心後,亦受支援。 另可在 WSA、ChromeOS 一類的容器式 Android 中運作。 目前僅適用 `arm64-v8a` 以及 `x86_64` 架構。 ## 使用手冊 - [安裝教學](https://kernelsu.org/zh_TW/guide/installation.html) - [如何建置 KernelSU?](https://kernelsu.org/zh_TW/guide/how-to-build.html) - [官方網站](https://kernelsu.org/zh_TW/) ## 多語翻譯 欲要協助 KernelSU 邁向多語化,抑或改進翻譯品質,請前往 [Weblate](https://hosted.weblate.org/engage/kernelsu/) 進行翻譯。為避免與 Weblate 上的翻譯發生衝突,現已不再受理翻譯相關的管理工具 PR。 ## 綜合討論 - Telegram:[@KernelSU](https://t.me/KernelSU) ## 安全政策 欲要得知、回報 KernelSU 的安全性漏洞,請參閱 [SECURITY.md](/SECURITY.md)。 ## 授權條款 - 位於 `kernel` 資料夾的檔案以[《GPL-2.0-only》](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html)規範。 - 非位於 `kernel` 資料夾的其他檔案以[《GPL-3.0-or-later》](https://www.gnu.org/licenses/gpl-3.0.html)規範。 ## 致謝名單 - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/):KernelSU 的靈感來源。 - [Magisk](https://github.com/topjohnwu/Magisk):強而有力的 Root 工具。 - [genuine](https://github.com/brevent/genuine/):用於確效 Apk v2 簽章。 - [Diamorphine](https://github.com/m0nad/Diamorphine): 用於增進 Rootkit 技巧。 ================================================ FILE: docs/README_VI.md ================================================ [English](README.md) | [Español](README_ES.md) | [简体中文](README_CN.md) | [繁體中文](README_TW.md) | [日本語](README_JP.md) | [한국어](README_KR.md) | [Polski](README_PL.md) | [Português (Brasil)](README_PT-BR.md) | [Türkçe](README_TR.md) | [Русский](README_RU.md) | **Tiếng Việt** | [Indonesia](README_ID.md) | [עברית](README_IW.md) | [हिंदी](README_IN.md) | [Italiano](README_IT.md) # KernelSU logo Giải pháp root thông qua thay đổi trên Kernel hệ điều hành cho các thiết bị Android. [![Latest release](https://img.shields.io/github/v/release/tiann/KernelSU?label=Release&logo=github)](https://github.com/tiann/KernelSU/releases/latest) [![Weblate](https://img.shields.io/badge/Localization-Weblate-teal?logo=weblate)](https://hosted.weblate.org/engage/kernelsu) [![Channel](https://img.shields.io/badge/Follow-Telegram-blue.svg?logo=telegram)](https://t.me/KernelSU) [![License: GPL v2](https://img.shields.io/badge/License-GPL%20v2-orange.svg?logo=gnu)](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html) [![GitHub License](https://img.shields.io/github/license/tiann/KernelSU?logo=gnu)](/LICENSE) ## Tính năng 1. Hỗ trợ gói thực thi `su` và quản lý quyền root. 2. Hệ thống mô-đun thông qua [metamodules](https://kernelsu.org/vi_VN/guide/metamodule.html): Cơ sở hạ tầng có thể cắm cho các sửa đổi systemless. 3. [App Profile](https://kernelsu.org/guide/app-profile.html): Hạn chế quyền root của ứng dụng. ## Tình trạng tương thích KernelSU chính thức hỗ trợ các thiết bị Android với kernel GKI 2.0 (phiên bản kernel 5.10+), các phiên bản kernel cũ hơn (4.14+) cũng tương thích, nhưng bạn cần phải tự biên dịch. WSA, ChromeOS và Android dựa trên container(container-based) cũng được hỗ trợ bởi KernelSU. Hiên tại Giao diện nhị phân của ứng dụng (ABI) được hỗ trợ bao gồm `arm64-v8a` và `x86_64`. ## Sử dụng - [Hướng dẫn cài đặt](https://kernelsu.org/vi_VN/guide/installation.html) - [Cách để build?](https://kernelsu.org/vi_VN/guide/how-to-build.html) - [Website Chính Thức](https://kernelsu.org/vi_VN/) ## Hỗ trợ dịch Nếu bạn muốn hỗ trợ dịch KernelSU sang một ngôn ngữ khác hoặc cải thiện các bản dịch trước, vui lòng sử dụng [Weblate](https://hosted.weblate.org/engage/kernelsu/). ## Thảo luận - Telegram: [@KernelSU](https://t.me/KernelSU) ## Giấy phép - Tất cả các file trong thư mục `kernel` dùng giấy phép [GPL-2-only](https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html). - Tất cả các thành phần khác ngoại trừ thư mục `kernel` dùng giấy phép [GPL-3-or-later](https://www.gnu.org/licenses/gpl-3.0.html). ## Lời cảm ơn - [kernel-assisted-superuser](https://git.zx2c4.com/kernel-assisted-superuser/about/): ý tưởng cho KernelSU. - [Magisk](https://github.com/topjohnwu/Magisk): công cụ root mạnh mẽ. - [genuine](https://github.com/brevent/genuine/): phương pháp xác thực apk v2. - [Diamorphine](https://github.com/m0nad/Diamorphine): các phương pháp ẩn của rootkit. ================================================ FILE: fastlane/metadata/android/en-US/full_description.txt ================================================

KernelSU is a Kernel based root solution for Android devices. It features kernel-based su and root access management as well as a Module system based on overlayfs (similar to Magisk). KernelSU works whitelist-based: Only App that is granted root permission can access su, other apps cannot perceive su.

KernelSU officially supports Android GKI 2.0 devices(with kernel 5.10+), old kernels(4.14+) is also compatiable, but you need to build kernel yourself. WSA and containter-based Android should also work with KernelSU integrated.

Current supported ABIs are: arm64-v8a and x86_64.

================================================ FILE: fastlane/metadata/android/en-US/short_description.txt ================================================ Kernel based root solution for Android ================================================ FILE: js/README.md ================================================ # Library for KernelSU's module WebUI ## Install ```sh yarn add kernelsu ``` ## API ### exec Spawns a **root** shell and runs a command within that shell, returning a Promise that resolves with the `stdout` and `stderr` outputs upon completion. - `command` `` The command to run, with space-separated arguments. - `options` `` - `cwd` - Current working directory of the child process. - `env` - Environment key-value pairs. ```javascript import { exec } from 'kernelsu'; const { errno, stdout, stderr } = await exec('ls -l', { cwd: '/tmp' }); if (errno === 0) { // success console.log(stdout); } ``` ### spawn Spawns a new process using the given `command` in **root** shell, with command-line arguments in `args`. If omitted, `args` defaults to an empty array. Returns a `ChildProcess` instance. Instances of `ChildProcess` represent spawned child processes. - `command` `` The command to run. - `args` `` List of string arguments. - `options` ``: - `cwd` `` - Current working directory of the child process. - `env` `` - Environment key-value pairs. Example of running `ls -lh /data`, capturing `stdout`, `stderr`, and the exit code: ```javascript import { spawn } from 'kernelsu'; const ls = spawn('ls', ['-lh', '/data']); ls.stdout.on('data', (data) => { console.log(`stdout: ${data}`); }); ls.stderr.on('data', (data) => { console.log(`stderr: ${data}`); }); ls.on('exit', (code) => { console.log(`child process exited with code ${code}`); }); ``` #### ChildProcess ##### Event 'exit' - `code` `` The exit code if the child process exited on its own. The `'exit'` event is emitted when the child process ends. If the process exits, `code` contains the final exit code; otherwise, it is null. ##### Event 'error' - `err` `` The error. The `'error'` event is emitted whenever: - The process could not be spawned. - The process could not be killed. ##### `stdout` A `Readable Stream` that represents the child process's `stdout`. ```javascript const subprocess = spawn('ls'); subprocess.stdout.on('data', (data) => { console.log(`Received chunk ${data}`); }); ``` #### `stderr` A `Readable Stream` that represents the child process's `stderr`. ### fullScreen Request the WebView enter/exit full screen. ```javascript import { fullScreen } from 'kernelsu'; fullScreen(true); ``` ### enableEdgeToEdge Request the WebView to set padding to 0 or safeDrawing insets - tips: this is disabled by default but if you request resource from `internal/insets.css`, this will be enabled automatically. - To get insets value and enable this automatically, you can - add `@import "https://mui.kernelsu.org/internal/insets.css";` in css OR - add `` in html. ```javascript import { enableEdgeToEdge } from 'kernelsu'; enableEdgeToEdge(true); ``` ### toast Show a toast message. ```javascript import { toast } from 'kernelsu'; toast('Hello, world!'); ``` ### moduleInfo Get module info. ```javascript import { moduleInfo } from 'kernelsu'; // print moduleId in console console.log(moduleInfo()); ``` ### listPackages List installed packages. Returns an array of package names. - `type` `` The type of packages to list: "user", "system", or "all". ```javascript import { listPackages } from 'kernelsu'; // list user packages const packages = listPackages("user"); ``` - tips: when `listPackages` api is available, you can use ksu://icon/{packageName} to get app icon. ``` javascript img.src = "ksu://icon/" + packageName; ``` ### getPackagesInfo Get information for a list of packages. Returns an array of `PackagesInfo` objects. - `packages` `` The list of package names. ```javascript import { getPackagesInfo } from 'kernelsu'; const packages = getPackagesInfo(['com.android.settings', 'com.android.shell']); ``` #### PackagesInfo An object contains: - `packageName` `` Package name of the application. - `versionName` `` Version of the application. - `versionCode` `` Version code of the application. - `appLabel` `` Display name of the application. - `isSystem` `` Whether the application is a system app. - `uid` `` UID of the application. ### exit Exit the current WebUI activity. ```javascript import { exit } from 'kernelsu'; exit(); ``` ================================================ FILE: js/index.d.ts ================================================ interface ExecOptions { cwd?: string, env?: { [key: string]: string } } interface ExecResults { errno: number, stdout: string, stderr: string } declare function exec(command: string): Promise; declare function exec(command: string, options: ExecOptions): Promise; interface SpawnOptions { cwd?: string, env?: { [key: string]: string } } interface Stdio { on(event: 'data', callback: (data: string) => void) } interface ChildProcess { stdout: Stdio, stderr: Stdio, on(event: 'exit', callback: (code: number) => void) on(event: 'error', callback: (err: any) => void) } declare function spawn(command: string): ChildProcess; declare function spawn(command: string, args: string[]): ChildProcess; declare function spawn(command: string, options: SpawnOptions): ChildProcess; declare function spawn(command: string, args: string[], options: SpawnOptions): ChildProcess; declare function fullScreen(isFullScreen: boolean); declare function enableEdgeToEdge(enable: boolean); declare function toast(message: string); declare function moduleInfo(): string; interface PackagesInfo { packageName: string; versionName: string; versionCode: number; appLabel: string; isSystem: boolean; uid: number; } declare function listPackages(type: string): string[]; declare function getPackagesInfo(packages: string[]): PackagesInfo[]; declare function exit(); export { exec, spawn, fullScreen, enableEdgeToEdge, toast, moduleInfo, listPackages, getPackagesInfo, exit, } ================================================ FILE: js/index.js ================================================ let callbackCounter = 0; function getUniqueCallbackName(prefix) { return `${prefix}_callback_${Date.now()}_${callbackCounter++}`; } export function exec(command, options) { if (typeof options === "undefined") { options = {}; } return new Promise((resolve, reject) => { // Generate a unique callback function name const callbackFuncName = getUniqueCallbackName("exec"); // Define the success callback function window[callbackFuncName] = (errno, stdout, stderr) => { resolve({ errno, stdout, stderr }); cleanup(callbackFuncName); }; function cleanup(successName) { delete window[successName]; } try { ksu.exec(command, JSON.stringify(options), callbackFuncName); } catch (error) { reject(error); cleanup(callbackFuncName); } }); } function Stdio() { this.listeners = {}; } Stdio.prototype.on = function (event, listener) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(listener); }; Stdio.prototype.emit = function (event, ...args) { if (this.listeners[event]) { this.listeners[event].forEach((listener) => listener(...args)); } }; function ChildProcess() { this.listeners = {}; this.stdin = new Stdio(); this.stdout = new Stdio(); this.stderr = new Stdio(); } ChildProcess.prototype.on = function (event, listener) { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event].push(listener); }; ChildProcess.prototype.emit = function (event, ...args) { if (this.listeners[event]) { this.listeners[event].forEach((listener) => listener(...args)); } }; export function spawn(command, args, options) { if (typeof args === "undefined") { args = []; } else if (!(args instanceof Array)) { // allow for (command, options) signature options = args; } if (typeof options === "undefined") { options = {}; } const child = new ChildProcess(); const childCallbackName = getUniqueCallbackName("spawn"); window[childCallbackName] = child; function cleanup(name) { delete window[name]; } child.on("exit", code => { cleanup(childCallbackName); }); try { ksu.spawn( command, JSON.stringify(args), JSON.stringify(options), childCallbackName ); } catch (error) { child.emit("error", error); cleanup(childCallbackName); } return child; } export function fullScreen(isFullScreen) { ksu.fullScreen(isFullScreen); } export function enableEdgeToEdge(enable) { ksu.enableEdgeToEdge(enable); } export function toast(message) { ksu.toast(message); } export function moduleInfo() { return ksu.moduleInfo(); } export function listPackages(type) { try { return JSON.parse(ksu.listPackages(type)); } catch (error) { return []; } } export function getPackagesInfo(packages) { try { if (typeof packages !== "string") { packages = JSON.stringify(packages); } return JSON.parse(ksu.getPackagesInfo(packages)); } catch (error) { return []; } } export function exit() { ksu.exit(); } ================================================ FILE: js/package.json ================================================ { "name": "kernelsu", "version": "3.0.2", "description": "Library for KernelSU's module WebUI", "main": "index.js", "types": "index.d.ts", "scripts": { "test": "npm run test" }, "repository": { "type": "git", "url": "git+https://github.com/tiann/KernelSU.git" }, "keywords": [ "su", "kernelsu", "module", "webui" ], "author": "weishu", "license": "Apache-2.0", "bugs": { "url": "https://github.com/tiann/KernelSU/issues" }, "homepage": "https://github.com/tiann/KernelSU#readme" } ================================================ FILE: justfile ================================================ alias bk := build_ksud alias bm := build_manager build_ksud: cross build --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml build_manager: build_ksud cp userspace/ksud/target/aarch64-linux-android/release/ksud manager/app/src/main/jniLibs/arm64-v8a/libksud.so cd manager && ./gradlew aDebug clippy: cargo fmt --manifest-path ./userspace/ksud/Cargo.toml cross clippy --target x86_64-pc-windows-gnu --release --manifest-path ./userspace/ksud/Cargo.toml cross clippy --target aarch64-linux-android --release --manifest-path ./userspace/ksud/Cargo.toml ================================================ FILE: kernel/.clang-format ================================================ # SPDX-License-Identifier: GPL-2.0 # # clang-format configuration file. Intended for clang-format >= 4. # # For more information, see: # # Documentation/process/clang-format.rst # https://clang.llvm.org/docs/ClangFormat.html # https://clang.llvm.org/docs/ClangFormatStyleOptions.html # --- AccessModifierOffset: -4 AlignAfterOpenBracket: Align AlignConsecutiveAssignments: false AlignConsecutiveDeclarations: false #AlignEscapedNewlines: Left # Unknown to clang-format-4.0 AlignOperands: true AlignTrailingComments: false AllowAllParametersOfDeclarationOnNextLine: false AllowShortBlocksOnASingleLine: false AllowShortCaseLabelsOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AlwaysBreakAfterDefinitionReturnType: None AlwaysBreakAfterReturnType: None AlwaysBreakBeforeMultilineStrings: false AlwaysBreakTemplateDeclarations: false BinPackArguments: true BinPackParameters: true BraceWrapping: AfterClass: false AfterControlStatement: false AfterEnum: false AfterFunction: true AfterNamespace: true AfterObjCDeclaration: false AfterStruct: false AfterUnion: false #AfterExternBlock: false # Unknown to clang-format-5.0 BeforeCatch: false BeforeElse: false IndentBraces: false #SplitEmptyFunction: true # Unknown to clang-format-4.0 #SplitEmptyRecord: true # Unknown to clang-format-4.0 #SplitEmptyNamespace: true # Unknown to clang-format-4.0 BreakBeforeBinaryOperators: None BreakBeforeBraces: Custom #BreakBeforeInheritanceComma: false # Unknown to clang-format-4.0 BreakBeforeTernaryOperators: false BreakConstructorInitializersBeforeComma: false #BreakConstructorInitializers: BeforeComma # Unknown to clang-format-4.0 BreakAfterJavaFieldAnnotations: false BreakStringLiterals: false ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' #CompactNamespaces: false # Unknown to clang-format-4.0 ConstructorInitializerAllOnOneLineOrOnePerLine: false ConstructorInitializerIndentWidth: 4 ContinuationIndentWidth: 4 Cpp11BracedListStyle: false DerivePointerAlignment: false DisableFormat: false ExperimentalAutoDetectBinPacking: false #FixNamespaceComments: false # Unknown to clang-format-4.0 # Taken from: # git grep -h '^#define [^[:space:]]*for_each[^[:space:]]*(' include/ \ # | sed "s,^#define \([^[:space:]]*for_each[^[:space:]]*\)(.*$, - '\1'," \ # | sort | uniq ForEachMacros: - 'apei_estatus_for_each_section' - 'ata_for_each_dev' - 'ata_for_each_link' - '__ata_qc_for_each' - 'ata_qc_for_each' - 'ata_qc_for_each_raw' - 'ata_qc_for_each_with_internal' - 'ax25_for_each' - 'ax25_uid_for_each' - '__bio_for_each_bvec' - 'bio_for_each_bvec' - 'bio_for_each_bvec_all' - 'bio_for_each_integrity_vec' - '__bio_for_each_segment' - 'bio_for_each_segment' - 'bio_for_each_segment_all' - 'bio_list_for_each' - 'bip_for_each_vec' - 'bitmap_for_each_clear_region' - 'bitmap_for_each_set_region' - 'blkg_for_each_descendant_post' - 'blkg_for_each_descendant_pre' - 'blk_queue_for_each_rl' - 'bond_for_each_slave' - 'bond_for_each_slave_rcu' - 'bpf_for_each_spilled_reg' - 'btree_for_each_safe128' - 'btree_for_each_safe32' - 'btree_for_each_safe64' - 'btree_for_each_safel' - 'card_for_each_dev' - 'cgroup_taskset_for_each' - 'cgroup_taskset_for_each_leader' - 'cpufreq_for_each_entry' - 'cpufreq_for_each_entry_idx' - 'cpufreq_for_each_valid_entry' - 'cpufreq_for_each_valid_entry_idx' - 'css_for_each_child' - 'css_for_each_descendant_post' - 'css_for_each_descendant_pre' - 'device_for_each_child_node' - 'dma_fence_chain_for_each' - 'do_for_each_ftrace_op' - 'drm_atomic_crtc_for_each_plane' - 'drm_atomic_crtc_state_for_each_plane' - 'drm_atomic_crtc_state_for_each_plane_state' - 'drm_atomic_for_each_plane_damage' - 'drm_client_for_each_connector_iter' - 'drm_client_for_each_modeset' - 'drm_connector_for_each_possible_encoder' - 'drm_for_each_bridge_in_chain' - 'drm_for_each_connector_iter' - 'drm_for_each_crtc' - 'drm_for_each_encoder' - 'drm_for_each_encoder_mask' - 'drm_for_each_fb' - 'drm_for_each_legacy_plane' - 'drm_for_each_plane' - 'drm_for_each_plane_mask' - 'drm_for_each_privobj' - 'drm_mm_for_each_hole' - 'drm_mm_for_each_node' - 'drm_mm_for_each_node_in_range' - 'drm_mm_for_each_node_safe' - 'flow_action_for_each' - 'for_each_active_dev_scope' - 'for_each_active_drhd_unit' - 'for_each_active_iommu' - 'for_each_aggr_pgid' - 'for_each_available_child_of_node' - 'for_each_bio' - 'for_each_board_func_rsrc' - 'for_each_bvec' - 'for_each_card_auxs' - 'for_each_card_auxs_safe' - 'for_each_card_components' - 'for_each_card_dapms' - 'for_each_card_pre_auxs' - 'for_each_card_prelinks' - 'for_each_card_rtds' - 'for_each_card_rtds_safe' - 'for_each_card_widgets' - 'for_each_card_widgets_safe' - 'for_each_cgroup_storage_type' - 'for_each_child_of_node' - 'for_each_clear_bit' - 'for_each_clear_bit_from' - 'for_each_cmsghdr' - 'for_each_compatible_node' - 'for_each_component_dais' - 'for_each_component_dais_safe' - 'for_each_comp_order' - 'for_each_console' - 'for_each_cpu' - 'for_each_cpu_and' - 'for_each_cpu_not' - 'for_each_cpu_wrap' - 'for_each_dapm_widgets' - 'for_each_dev_addr' - 'for_each_dev_scope' - 'for_each_displayid_db' - 'for_each_dma_cap_mask' - 'for_each_dpcm_be' - 'for_each_dpcm_be_rollback' - 'for_each_dpcm_be_safe' - 'for_each_dpcm_fe' - 'for_each_drhd_unit' - 'for_each_dss_dev' - 'for_each_efi_memory_desc' - 'for_each_efi_memory_desc_in_map' - 'for_each_element' - 'for_each_element_extid' - 'for_each_element_id' - 'for_each_endpoint_of_node' - 'for_each_evictable_lru' - 'for_each_fib6_node_rt_rcu' - 'for_each_fib6_walker_rt' - 'for_each_free_mem_pfn_range_in_zone' - 'for_each_free_mem_pfn_range_in_zone_from' - 'for_each_free_mem_range' - 'for_each_free_mem_range_reverse' - 'for_each_func_rsrc' - 'for_each_hstate' - 'for_each_if' - 'for_each_iommu' - 'for_each_ip_tunnel_rcu' - 'for_each_irq_nr' - 'for_each_link_codecs' - 'for_each_link_cpus' - 'for_each_link_platforms' - 'for_each_lru' - 'for_each_matching_node' - 'for_each_matching_node_and_match' - 'for_each_member' - 'for_each_mem_region' - 'for_each_memblock_type' - 'for_each_memcg_cache_index' - 'for_each_mem_pfn_range' - '__for_each_mem_range' - 'for_each_mem_range' - '__for_each_mem_range_rev' - 'for_each_mem_range_rev' - 'for_each_migratetype_order' - 'for_each_msi_entry' - 'for_each_msi_entry_safe' - 'for_each_net' - 'for_each_net_continue_reverse' - 'for_each_netdev' - 'for_each_netdev_continue' - 'for_each_netdev_continue_rcu' - 'for_each_netdev_continue_reverse' - 'for_each_netdev_feature' - 'for_each_netdev_in_bond_rcu' - 'for_each_netdev_rcu' - 'for_each_netdev_reverse' - 'for_each_netdev_safe' - 'for_each_net_rcu' - 'for_each_new_connector_in_state' - 'for_each_new_crtc_in_state' - 'for_each_new_mst_mgr_in_state' - 'for_each_new_plane_in_state' - 'for_each_new_private_obj_in_state' - 'for_each_node' - 'for_each_node_by_name' - 'for_each_node_by_type' - 'for_each_node_mask' - 'for_each_node_state' - 'for_each_node_with_cpus' - 'for_each_node_with_property' - 'for_each_nonreserved_multicast_dest_pgid' - 'for_each_of_allnodes' - 'for_each_of_allnodes_from' - 'for_each_of_cpu_node' - 'for_each_of_pci_range' - 'for_each_old_connector_in_state' - 'for_each_old_crtc_in_state' - 'for_each_old_mst_mgr_in_state' - 'for_each_oldnew_connector_in_state' - 'for_each_oldnew_crtc_in_state' - 'for_each_oldnew_mst_mgr_in_state' - 'for_each_oldnew_plane_in_state' - 'for_each_oldnew_plane_in_state_reverse' - 'for_each_oldnew_private_obj_in_state' - 'for_each_old_plane_in_state' - 'for_each_old_private_obj_in_state' - 'for_each_online_cpu' - 'for_each_online_node' - 'for_each_online_pgdat' - 'for_each_pci_bridge' - 'for_each_pci_dev' - 'for_each_pci_msi_entry' - 'for_each_pcm_streams' - 'for_each_physmem_range' - 'for_each_populated_zone' - 'for_each_possible_cpu' - 'for_each_present_cpu' - 'for_each_prime_number' - 'for_each_prime_number_from' - 'for_each_process' - 'for_each_process_thread' - 'for_each_property_of_node' - 'for_each_registered_fb' - 'for_each_requested_gpio' - 'for_each_requested_gpio_in_range' - 'for_each_reserved_mem_range' - 'for_each_reserved_mem_region' - 'for_each_rtd_codec_dais' - 'for_each_rtd_codec_dais_rollback' - 'for_each_rtd_components' - 'for_each_rtd_cpu_dais' - 'for_each_rtd_cpu_dais_rollback' - 'for_each_rtd_dais' - 'for_each_set_bit' - 'for_each_set_bit_from' - 'for_each_set_clump8' - 'for_each_sg' - 'for_each_sg_dma_page' - 'for_each_sg_page' - 'for_each_sgtable_dma_page' - 'for_each_sgtable_dma_sg' - 'for_each_sgtable_page' - 'for_each_sgtable_sg' - 'for_each_sibling_event' - 'for_each_subelement' - 'for_each_subelement_extid' - 'for_each_subelement_id' - '__for_each_thread' - 'for_each_thread' - 'for_each_unicast_dest_pgid' - 'for_each_wakeup_source' - 'for_each_zone' - 'for_each_zone_zonelist' - 'for_each_zone_zonelist_nodemask' - 'fwnode_for_each_available_child_node' - 'fwnode_for_each_child_node' - 'fwnode_graph_for_each_endpoint' - 'gadget_for_each_ep' - 'genradix_for_each' - 'genradix_for_each_from' - 'hash_for_each' - 'hash_for_each_possible' - 'hash_for_each_possible_rcu' - 'hash_for_each_possible_rcu_notrace' - 'hash_for_each_possible_safe' - 'hash_for_each_rcu' - 'hash_for_each_safe' - 'hctx_for_each_ctx' - 'hlist_bl_for_each_entry' - 'hlist_bl_for_each_entry_rcu' - 'hlist_bl_for_each_entry_safe' - 'hlist_for_each' - 'hlist_for_each_entry' - 'hlist_for_each_entry_continue' - 'hlist_for_each_entry_continue_rcu' - 'hlist_for_each_entry_continue_rcu_bh' - 'hlist_for_each_entry_from' - 'hlist_for_each_entry_from_rcu' - 'hlist_for_each_entry_rcu' - 'hlist_for_each_entry_rcu_bh' - 'hlist_for_each_entry_rcu_notrace' - 'hlist_for_each_entry_safe' - '__hlist_for_each_rcu' - 'hlist_for_each_safe' - 'hlist_nulls_for_each_entry' - 'hlist_nulls_for_each_entry_from' - 'hlist_nulls_for_each_entry_rcu' - 'hlist_nulls_for_each_entry_safe' - 'i3c_bus_for_each_i2cdev' - 'i3c_bus_for_each_i3cdev' - 'ide_host_for_each_port' - 'ide_port_for_each_dev' - 'ide_port_for_each_present_dev' - 'idr_for_each_entry' - 'idr_for_each_entry_continue' - 'idr_for_each_entry_continue_ul' - 'idr_for_each_entry_ul' - 'in_dev_for_each_ifa_rcu' - 'in_dev_for_each_ifa_rtnl' - 'inet_bind_bucket_for_each' - 'inet_lhash2_for_each_icsk_rcu' - 'key_for_each' - 'key_for_each_safe' - 'klp_for_each_func' - 'klp_for_each_func_safe' - 'klp_for_each_func_static' - 'klp_for_each_object' - 'klp_for_each_object_safe' - 'klp_for_each_object_static' - 'kunit_suite_for_each_test_case' - 'kvm_for_each_memslot' - 'kvm_for_each_vcpu' - 'list_for_each' - 'list_for_each_codec' - 'list_for_each_codec_safe' - 'list_for_each_continue' - 'list_for_each_entry' - 'list_for_each_entry_continue' - 'list_for_each_entry_continue_rcu' - 'list_for_each_entry_continue_reverse' - 'list_for_each_entry_from' - 'list_for_each_entry_from_rcu' - 'list_for_each_entry_from_reverse' - 'list_for_each_entry_lockless' - 'list_for_each_entry_rcu' - 'list_for_each_entry_reverse' - 'list_for_each_entry_safe' - 'list_for_each_entry_safe_continue' - 'list_for_each_entry_safe_from' - 'list_for_each_entry_safe_reverse' - 'list_for_each_prev' - 'list_for_each_prev_safe' - 'list_for_each_safe' - 'llist_for_each' - 'llist_for_each_entry' - 'llist_for_each_entry_safe' - 'llist_for_each_safe' - 'mci_for_each_dimm' - 'media_device_for_each_entity' - 'media_device_for_each_intf' - 'media_device_for_each_link' - 'media_device_for_each_pad' - 'nanddev_io_for_each_page' - 'netdev_for_each_lower_dev' - 'netdev_for_each_lower_private' - 'netdev_for_each_lower_private_rcu' - 'netdev_for_each_mc_addr' - 'netdev_for_each_uc_addr' - 'netdev_for_each_upper_dev_rcu' - 'netdev_hw_addr_list_for_each' - 'nft_rule_for_each_expr' - 'nla_for_each_attr' - 'nla_for_each_nested' - 'nlmsg_for_each_attr' - 'nlmsg_for_each_msg' - 'nr_neigh_for_each' - 'nr_neigh_for_each_safe' - 'nr_node_for_each' - 'nr_node_for_each_safe' - 'of_for_each_phandle' - 'of_property_for_each_string' - 'of_property_for_each_u32' - 'pci_bus_for_each_resource' - 'pcm_for_each_format' - 'ping_portaddr_for_each_entry' - 'plist_for_each' - 'plist_for_each_continue' - 'plist_for_each_entry' - 'plist_for_each_entry_continue' - 'plist_for_each_entry_safe' - 'plist_for_each_safe' - 'pnp_for_each_card' - 'pnp_for_each_dev' - 'protocol_for_each_card' - 'protocol_for_each_dev' - 'queue_for_each_hw_ctx' - 'radix_tree_for_each_slot' - 'radix_tree_for_each_tagged' - 'rbtree_postorder_for_each_entry_safe' - 'rdma_for_each_block' - 'rdma_for_each_port' - 'rdma_umem_for_each_dma_block' - 'resource_list_for_each_entry' - 'resource_list_for_each_entry_safe' - 'rhl_for_each_entry_rcu' - 'rhl_for_each_rcu' - 'rht_for_each' - 'rht_for_each_entry' - 'rht_for_each_entry_from' - 'rht_for_each_entry_rcu' - 'rht_for_each_entry_rcu_from' - 'rht_for_each_entry_safe' - 'rht_for_each_from' - 'rht_for_each_rcu' - 'rht_for_each_rcu_from' - '__rq_for_each_bio' - 'rq_for_each_bvec' - 'rq_for_each_segment' - 'scsi_for_each_prot_sg' - 'scsi_for_each_sg' - 'sctp_for_each_hentry' - 'sctp_skb_for_each' - 'shdma_for_each_chan' - '__shost_for_each_device' - 'shost_for_each_device' - 'sk_for_each' - 'sk_for_each_bound' - 'sk_for_each_entry_offset_rcu' - 'sk_for_each_from' - 'sk_for_each_rcu' - 'sk_for_each_safe' - 'sk_nulls_for_each' - 'sk_nulls_for_each_from' - 'sk_nulls_for_each_rcu' - 'snd_array_for_each' - 'snd_pcm_group_for_each_entry' - 'snd_soc_dapm_widget_for_each_path' - 'snd_soc_dapm_widget_for_each_path_safe' - 'snd_soc_dapm_widget_for_each_sink_path' - 'snd_soc_dapm_widget_for_each_source_path' - 'tb_property_for_each' - 'tcf_exts_for_each_action' - 'udp_portaddr_for_each_entry' - 'udp_portaddr_for_each_entry_rcu' - 'usb_hub_for_each_child' - 'v4l2_device_for_each_subdev' - 'v4l2_m2m_for_each_dst_buf' - 'v4l2_m2m_for_each_dst_buf_safe' - 'v4l2_m2m_for_each_src_buf' - 'v4l2_m2m_for_each_src_buf_safe' - 'virtio_device_for_each_vq' - 'while_for_each_ftrace_op' - 'xa_for_each' - 'xa_for_each_marked' - 'xa_for_each_range' - 'xa_for_each_start' - 'xas_for_each' - 'xas_for_each_conflict' - 'xas_for_each_marked' - 'xbc_array_for_each_value' - 'xbc_for_each_key_value' - 'xbc_node_for_each_array_value' - 'xbc_node_for_each_child' - 'xbc_node_for_each_key_value' - 'zorro_for_each_dev' #IncludeBlocks: Preserve # Unknown to clang-format-5.0 IncludeCategories: - Regex: '.*' Priority: 1 IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false #IndentPPDirectives: None # Unknown to clang-format-5.0 IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true KeepEmptyLinesAtTheStartOfBlocks: false MacroBlockBegin: '' MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None #ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0 ObjCBlockIndentWidth: 4 ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true # Taken from git's rules #PenaltyBreakAssignment: 10 # Unknown to clang-format-4.0 PenaltyBreakBeforeFirstCallParameter: 30 PenaltyBreakComment: 10 PenaltyBreakFirstLessLess: 0 PenaltyBreakString: 10 PenaltyExcessCharacter: 100 PenaltyReturnTypeOnItsOwnLine: 60 PointerAlignment: Right ReflowComments: false SortIncludes: false #SortUsingDeclarations: false # Unknown to clang-format-4.0 SpaceAfterCStyleCast: false SpaceAfterTemplateKeyword: true SpaceBeforeAssignmentOperators: true #SpaceBeforeCtorInitializerColon: true # Unknown to clang-format-5.0 #SpaceBeforeInheritanceColon: true # Unknown to clang-format-5.0 SpaceBeforeParens: ControlStatements #SpaceBeforeRangeBasedForLoopColon: true # Unknown to clang-format-5.0 SpaceInEmptyParentheses: false SpacesBeforeTrailingComments: 1 SpacesInAngles: false SpacesInContainerLiterals: false SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp03 TabWidth: 4 UseTab: Never ... ================================================ FILE: kernel/.clangd ================================================ Diagnostics: UnusedIncludes: Strict ClangTidy: Remove: bugprone-sizeof-expression ================================================ FILE: kernel/.gitignore ================================================ .cache/ .thinlto-cache/ compile_commands.json *.ko *.o *.mod *.lds *.mod.o .*.o* .*.mod* *.ko* *.mod.c *.symvers* *.order .*.ko.cmd .tmp_versions/ libs/ obj/ CLAUDE.md .ddk-version .vscode/settings.json check_symbol ================================================ FILE: kernel/Kbuild ================================================ kernelsu-objs := ksu.o kernelsu-objs += allowlist.o kernelsu-objs += app_profile.o kernelsu-objs += apk_sign.o kernelsu-objs += sucompat.o kernelsu-objs += syscall_hook_manager.o kernelsu-objs += throne_tracker.o kernelsu-objs += pkg_observer.o kernelsu-objs += setuid_hook.o kernelsu-objs += kernel_umount.o kernelsu-objs += supercalls.o kernelsu-objs += su_mount_ns.o kernelsu-objs += feature.o kernelsu-objs += ksud.o kernelsu-objs += seccomp_cache.o kernelsu-objs += file_wrapper.o kernelsu-objs += util.o kernelsu-objs += selinux/selinux.o kernelsu-objs += selinux/sepolicy.o kernelsu-objs += selinux/rules.o ccflags-y += -I$(srctree)/security/selinux -I$(srctree)/security/selinux/include ccflags-y += -I$(objtree)/security/selinux -include $(srctree)/include/uapi/asm-generic/errno.h obj-$(CONFIG_KSU) += kernelsu.o MDIR := $(dir $(abspath $(lastword $(MAKEFILE_LIST)))) # Workaround bazel # https://github.com/tiann/KernelSU/pull/316#issuecomment-1479043219 GIT := $(shell PATH="$$PATH":/usr/bin:/usr/local/bin which git) ifneq ($(GIT),) # Check if this is a git repository # Try to detect Git repo intelligently GIT_ROOT := $(shell cd $(MDIR) && $(GIT) rev-parse --show-toplevel 2>/dev/null) ifneq ($(GIT_ROOT),) KERNEL_GIT_ROOT := $(shell cd $(srctree) && $(GIT) rev-parse --show-toplevel 2>/dev/null) ifneq ($(GIT_ROOT),$(KERNEL_GIT_ROOT)) # Only set version if it's a different repo from kernel $(shell cd $(GIT_ROOT) && [ -f .git/shallow ] && $(GIT) fetch --unshallow 2>/dev/null || true) KSU_GIT_VERSION := $(shell cd $(GIT_ROOT) && $(GIT) rev-list --count HEAD 2>/dev/null) KSU_GIT_VERSION_VALID := 1 $(info -- KernelSU: Git repo detected at $(GIT_ROOT)) endif endif else $(warning -- KernelSU: Git not detected, make sure git is installed in your PATH!) endif # Calculate version if git version is available ifdef KSU_GIT_VERSION_VALID # ksu_version: major * 10000 + git version + 200 for historical reasons $(eval KSU_VERSION=$(shell expr 30000 + $(KSU_GIT_VERSION))) $(info -- KernelSU version: $(KSU_VERSION)) ccflags-y += -DKSU_VERSION=$(KSU_VERSION) else # If there is no .git directory, use default version $(warning "KSU_GIT_VERSION not defined! It is better to make KernelSU a git repository!") ccflags-y += -DKSU_VERSION=16 endif ifndef KSU_EXPECTED_SIZE KSU_EXPECTED_SIZE := 0x033b endif ifndef KSU_EXPECTED_HASH KSU_EXPECTED_HASH := c371061b19d8c7d7d6133c6a9bafe198fa944e50c1b31c9d8daa8d7f1fc2d2d6 endif ifdef KSU_MANAGER_PACKAGE ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\" $(info -- KernelSU Manager package name: $(KSU_MANAGER_PACKAGE)) endif $(info -- KernelSU Manager signature size: $(KSU_EXPECTED_SIZE)) $(info -- KernelSU Manager signature hash: $(KSU_EXPECTED_HASH)) ccflags-y += -DEXPECTED_SIZE=$(KSU_EXPECTED_SIZE) ccflags-y += -DEXPECTED_HASH=\"$(KSU_EXPECTED_HASH)\" ifdef KSU_EXPECTED_SIZE2 ifndef KSU_EXPECTED_HASH2 $(error KSU_EXPECTED_HASH2 must be set when KSU_EXPECTED_SIZE2 is set) endif ccflags-y += -DEXPECTED_SIZE2=$(KSU_EXPECTED_SIZE2) ccflags-y += -DEXPECTED_HASH2=\"$(KSU_EXPECTED_HASH2)\" $(info -- KernelSU Manager signature size2: $(KSU_EXPECTED_SIZE2)) $(info -- KernelSU Manager signature hash2: $(KSU_EXPECTED_HASH2)) endif ccflags-y += -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -Wno-missing-prototypes ccflags-y += -Wno-declaration-after-statement -Wno-unused-function # Keep a new line here!! Because someone may append config ================================================ FILE: kernel/Kconfig ================================================ menu "KernelSU" config KSU tristate "KernelSU function support" depends on KPROBES && EXT4_FS default y help Enable kernel-level root privileges on Android System. Requires CONFIG_KPROBES for kernel hooking support. Requires CONFIG_EXT4_FS for `ext4_unregister_sysfs`. To compile as a module, choose M here: the module will be called kernelsu. config KSU_DEBUG bool "KernelSU debug mode" depends on KSU default n help Enable KernelSU debug mode. endmenu ================================================ FILE: kernel/LICENSE ================================================ GNU GENERAL PUBLIC LICENSE Version 2, June 1991 Copyright (C) 1989, 1991 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. The precise terms and conditions for copying, distribution and modification follow. GNU GENERAL PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. 1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. 2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. 3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. 4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. 5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. 6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. 7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. 8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. 9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. 10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. NO WARRANTY 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. Also add information on how to contact you by electronic and paper mail. If the program is interactive, make it output a short notice like this when it starts in an interactive mode: Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. , 1 April 1989 Ty Coon, President of Vice This General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. ================================================ FILE: kernel/Makefile ================================================ KDIR := $(KDIR) MDIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) $(info -- KDIR: $(KDIR)) $(info -- MDIR: $(MDIR)) .PHONY: all compdb clean format check-format all: check_symbol make -C $(KDIR) M=$(MDIR) modules ./check_symbol kernelsu.ko $(KDIR)/vmlinux compdb: python3 $(MDIR)/.vscode/generate_compdb.py -O $(KDIR) $(MDIR) clean: make -C $(KDIR) M=$(MDIR) clean rm check_symbol check_symbol: tools/check_symbol.c $(CC) tools/check_symbol.c -o check_symbol format: find . \( -name "*.c" -o -name "*.h" \) -print0 | xargs -0 clang-format -i check-format: find . \( -name "*.c" -o -name "*.h" \) -print0 | xargs -0 clang-format --dry-run --Werror # Keep a new line here!! Because someone may append config ================================================ FILE: kernel/allowlist.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "klog.h" // IWYU pragma: keep #include "ksu.h" #include "ksud.h" #include "selinux/selinux.h" #include "allowlist.h" #include "manager.h" #include "su_mount_ns.h" #define FILE_MAGIC 0x7f4b5355 // ' KSU', u32 #define FILE_FORMAT_VERSION 3 // u32 #define KSU_APP_PROFILE_PRESERVE_UID 9999 // NOBODY_UID #define KSU_DEFAULT_SELINUX_DOMAIN "u:r:" KERNEL_SU_DOMAIN ":s0" static DEFINE_MUTEX(allowlist_mutex); // default profiles, these may be used frequently, so we cache it static struct root_profile default_root_profile; static struct non_root_profile default_non_root_profile; static int allow_list_arr[PAGE_SIZE / sizeof(int)] __read_mostly __aligned(PAGE_SIZE); static int allow_list_pointer __read_mostly = 0; static void remove_uid_from_arr(uid_t uid) { int i; for (i = 0; i < allow_list_pointer; i++) { if (allow_list_arr[i] == uid) { int remaining = allow_list_pointer - 1 - i; if (remaining > 0) { memmove(&allow_list_arr[i], &allow_list_arr[i + 1], remaining * sizeof(allow_list_arr[0])); } allow_list_pointer--; allow_list_arr[allow_list_pointer] = -1; return; } } } static void init_default_profiles() { kernel_cap_t full_cap = CAP_FULL_SET; default_root_profile.uid = 0; default_root_profile.gid = 0; default_root_profile.groups_count = 1; default_root_profile.groups[0] = 0; memcpy(&default_root_profile.capabilities.effective, &full_cap, sizeof(default_root_profile.capabilities.effective)); default_root_profile.namespaces = KSU_NS_INHERITED; strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); // This means that we will umount modules by default! default_non_root_profile.umount_modules = true; } struct perm_data { struct list_head list; struct rcu_head rcu; struct app_profile profile; }; static struct list_head allow_list; static uint8_t allow_list_bitmap[PAGE_SIZE] __read_mostly __aligned(PAGE_SIZE); #define BITMAP_UID_MAX ((sizeof(allow_list_bitmap) * BITS_PER_BYTE) - 1) #define KERNEL_SU_ALLOWLIST "/data/adb/ksu/.allowlist" void ksu_persistent_allow_list(void); void ksu_show_allow_list(void) { struct perm_data *p = NULL; pr_info("ksu_show_allow_list\n"); rcu_read_lock(); list_for_each_entry_rcu (p, &allow_list, list) { pr_info("uid :%d, allow: %d\n", p->profile.current_uid, p->profile.allow_su); } rcu_read_unlock(); } bool ksu_get_app_profile(struct app_profile *profile) { struct perm_data *p = NULL; bool found = false; rcu_read_lock(); list_for_each_entry_rcu (p, &allow_list, list) { bool uid_match = profile->current_uid == p->profile.current_uid; if (uid_match) { // found it, override it with ours memcpy(profile, &p->profile, sizeof(*profile)); found = true; goto exit; } } exit: rcu_read_unlock(); return found; } static inline bool forbid_system_uid(uid_t uid) { #define SHELL_UID 2000 #define SYSTEM_UID 1000 return uid < SHELL_UID && uid != SYSTEM_UID; } static bool profile_valid(struct app_profile *profile) { if (!profile) { return false; } if (profile->version < KSU_APP_PROFILE_VER) { pr_info("Unsupported profile version: %d\n", profile->version); return false; } if (profile->allow_su) { if (profile->rp_config.profile.groups_count > KSU_MAX_GROUPS) { return false; } if (strlen(profile->rp_config.profile.selinux_domain) == 0) { return false; } } return true; } int ksu_set_app_profile(struct app_profile *profile) { struct perm_data *p = NULL, *np; int result = 0; u16 count = 0; if (!profile_valid(profile)) { pr_err("Failed to set app profile: invalid profile!\n"); return -EINVAL; } mutex_lock(&allowlist_mutex); list_for_each_entry (p, &allow_list, list) { ++count; // both uid and package must match, otherwise it will break multiple package with different user id if (profile->current_uid == p->profile.current_uid && !strcmp(profile->key, p->profile.key)) { // found it, just override it all! np = (struct perm_data *)kzalloc(sizeof(struct perm_data), GFP_KERNEL); if (!np) { result = -ENOMEM; goto out_unlock; } memcpy(&np->profile, profile, sizeof(*profile)); list_replace_rcu(&p->list, &np->list); kfree_rcu(p, rcu); goto out; } } if (unlikely(count == U16_MAX)) { pr_err("too many app profile\n"); result = -E2BIG; goto out_unlock; } // not found, alloc a new node! p = (struct perm_data *)kzalloc(sizeof(struct perm_data), GFP_KERNEL); if (!p) { pr_err("ksu_set_app_profile alloc failed\n"); result = -ENOMEM; goto out_unlock; } memcpy(&p->profile, profile, sizeof(*profile)); if (profile->allow_su) { pr_info("set root profile, key: %s, uid: %d, gid: %d, context: %s\n", profile->key, profile->current_uid, profile->rp_config.profile.gid, profile->rp_config.profile.selinux_domain); } else { pr_info("set app profile, key: %s, uid: %d, umount modules: %d\n", profile->key, profile->current_uid, profile->nrp_config.profile.umount_modules); } list_add_tail_rcu(&p->list, &allow_list); out: result = 0; // check if the default profiles is changed, cache it to a single struct to accelerate access. if (unlikely(!strcmp(profile->key, "$"))) { // set default non root profile memcpy(&default_non_root_profile, &profile->nrp_config.profile, sizeof(default_non_root_profile)); } else if (unlikely(!strcmp(profile->key, "#"))) { // set default root profile // TODO: Do we really need this? memcpy(&default_root_profile, &profile->rp_config.profile, sizeof(default_root_profile)); } else if (profile->current_uid <= BITMAP_UID_MAX) { if (profile->allow_su) allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] |= 1 << (profile->current_uid % BITS_PER_BYTE); else allow_list_bitmap[profile->current_uid / BITS_PER_BYTE] &= ~(1 << (profile->current_uid % BITS_PER_BYTE)); } else { if (profile->allow_su) { /* * 1024 apps with uid higher than BITMAP_UID_MAX * registered to request superuser? */ if (allow_list_pointer >= ARRAY_SIZE(allow_list_arr)) { pr_err("too many apps registered\n"); WARN_ON(1); } else { allow_list_arr[allow_list_pointer++] = profile->current_uid; } } else { remove_uid_from_arr(profile->current_uid); } } out_unlock: mutex_unlock(&allowlist_mutex); return result; } bool __ksu_is_allow_uid(uid_t uid) { int i; if (forbid_system_uid(uid)) { // do not bother going through the list if it's system return false; } if (likely(ksu_is_manager_appid_valid()) && unlikely(ksu_get_manager_appid() == uid % PER_USER_RANGE)) { // manager is always allowed! return true; } if (unlikely(allow_shell) && uid == SHELL_UID) { return true; } if (likely(uid <= BITMAP_UID_MAX)) { return !!(allow_list_bitmap[uid / BITS_PER_BYTE] & (1 << (uid % BITS_PER_BYTE))); } else { for (i = 0; i < allow_list_pointer; i++) { if (allow_list_arr[i] == uid) return true; } } return false; } bool __ksu_is_allow_uid_for_current(uid_t uid) { if (unlikely(uid == 0)) { // already root, but only allow our domain. return is_ksu_domain(); } return __ksu_is_allow_uid(uid); } bool ksu_uid_should_umount(uid_t uid) { struct app_profile profile = { .current_uid = uid }; if (likely(ksu_is_manager_appid_valid()) && unlikely(ksu_get_manager_appid() == uid % PER_USER_RANGE)) { // we should not umount on manager! return false; } bool found = ksu_get_app_profile(&profile); if (!found) { // no app profile found, it must be non root app return default_non_root_profile.umount_modules; } if (profile.allow_su) { // if found and it is granted to su, we shouldn't umount for it return false; } else { // found an app profile if (profile.nrp_config.use_default) { return default_non_root_profile.umount_modules; } else { return profile.nrp_config.profile.umount_modules; } } } void ksu_get_root_profile(uid_t uid, struct root_profile *profile) { struct perm_data *p = NULL; if (is_uid_manager(uid)) { goto use_default; } if (unlikely(allow_shell && uid == SHELL_UID)) { goto use_default; } rcu_read_lock(); list_for_each_entry_rcu (p, &allow_list, list) { if (uid == p->profile.current_uid && p->profile.allow_su) { if (!p->profile.rp_config.use_default) { memcpy(profile, &p->profile.rp_config.profile, sizeof(*profile)); rcu_read_unlock(); return; } } } rcu_read_unlock(); use_default: // use default profile memcpy(profile, &default_root_profile, sizeof(*profile)); } bool ksu_get_allow_list(int *array, u16 length, u16 *out_length, u16 *out_total, bool allow) { struct perm_data *p = NULL; u16 i = 0, j = 0; rcu_read_lock(); list_for_each_entry_rcu (p, &allow_list, list) { // pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow); if (p->profile.allow_su == allow && !is_uid_manager(p->profile.current_uid)) { if (j < length) { array[j++] = p->profile.current_uid; } ++i; } } rcu_read_unlock(); if (out_length) { *out_length = j; } if (out_total) { *out_total = i; } return true; } // TODO: move to kernel thread or work queue static void do_persistent_allow_list(struct callback_head *_cb) { u32 magic = FILE_MAGIC; u32 version = FILE_FORMAT_VERSION; struct perm_data *p = NULL; loff_t off = 0; const struct cred *saved = override_creds(ksu_cred); struct file *fp = filp_open(KERNEL_SU_ALLOWLIST, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (IS_ERR(fp)) { pr_err("save_allow_list create file failed: %ld\n", PTR_ERR(fp)); goto out; } // store magic and version if (kernel_write(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { pr_err("save_allow_list write magic failed.\n"); goto close_file; } if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("save_allow_list write version failed.\n"); goto close_file; } mutex_lock(&allowlist_mutex); list_for_each_entry (p, &allow_list, list) { pr_info("save allow list, name: %s uid :%d, allow: %d\n", p->profile.key, p->profile.current_uid, p->profile.allow_su); kernel_write(fp, &p->profile, sizeof(p->profile), &off); } mutex_unlock(&allowlist_mutex); close_file: filp_close(fp, 0); out: revert_creds(saved); kfree(_cb); } void ksu_persistent_allow_list() { struct task_struct *tsk; tsk = get_pid_task(find_vpid(1), PIDTYPE_PID); if (!tsk) { pr_err("save_allow_list find init task err\n"); return; } struct callback_head *cb = kzalloc(sizeof(struct callback_head), GFP_KERNEL); if (!cb) { pr_err("save_allow_list alloc cb err\b"); goto put_task; } cb->func = do_persistent_allow_list; if (task_work_add(tsk, cb, TWA_RESUME)) { kfree(cb); pr_warn("save_allow_list add task_work failed\n"); } put_task: put_task_struct(tsk); } void ksu_load_allow_list() { loff_t off = 0; ssize_t ret = 0; struct file *fp = NULL; u32 magic; u32 version; // load allowlist now! fp = filp_open(KERNEL_SU_ALLOWLIST, O_RDONLY, 0); if (IS_ERR(fp)) { pr_err("load_allow_list open file failed: %ld\n", PTR_ERR(fp)); return; } // verify magic if (kernel_read(fp, &magic, sizeof(magic), &off) != sizeof(magic) || magic != FILE_MAGIC) { pr_err("allowlist file invalid: %d!\n", magic); goto exit; } if (kernel_read(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("allowlist read version: %d failed\n", version); goto exit; } pr_info("allowlist version: %d\n", version); while (true) { struct app_profile profile; ret = kernel_read(fp, &profile, sizeof(profile), &off); if (ret <= 0) { pr_info("load_allow_list read err: %zd\n", ret); break; } pr_info("load_allow_uid, name: %s, uid: %d, allow: %d\n", profile.key, profile.current_uid, profile.allow_su); ksu_set_app_profile(&profile); } exit: ksu_show_allow_list(); filp_close(fp, 0); } void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), void *data) { struct perm_data *np = NULL; struct perm_data *n = NULL; if (!ksu_boot_completed) { pr_info("boot not completed, skip prune\n"); return; } bool modified = false; mutex_lock(&allowlist_mutex); list_for_each_entry_safe (np, n, &allow_list, list) { uid_t uid = np->profile.current_uid; char *package = np->profile.key; // we use this uid for special cases, don't prune it! bool is_preserved_uid = uid == KSU_APP_PROFILE_PRESERVE_UID; if (!is_preserved_uid && !is_uid_valid(uid, package, data)) { modified = true; pr_info("prune uid: %d, package: %s\n", uid, package); list_del_rcu(&np->list); kfree_rcu(np, rcu); if (likely(uid <= BITMAP_UID_MAX)) { allow_list_bitmap[uid / BITS_PER_BYTE] &= ~(1 << (uid % BITS_PER_BYTE)); } remove_uid_from_arr(uid); } } mutex_unlock(&allowlist_mutex); if (modified) { smp_mb(); ksu_persistent_allow_list(); } } void ksu_allowlist_init(void) { int i; BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE); BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE); for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++) allow_list_arr[i] = -1; INIT_LIST_HEAD(&allow_list); init_default_profiles(); } void ksu_allowlist_exit(void) { struct perm_data *np = NULL; struct perm_data *n = NULL; // free allowlist mutex_lock(&allowlist_mutex); list_for_each_entry_safe (np, n, &allow_list, list) { list_del(&np->list); kfree(np); } mutex_unlock(&allowlist_mutex); } ================================================ FILE: kernel/allowlist.h ================================================ #ifndef __KSU_H_ALLOWLIST #define __KSU_H_ALLOWLIST #include #include #include "app_profile.h" #define PER_USER_RANGE 100000 #define FIRST_APPLICATION_UID 10000 #define LAST_APPLICATION_UID 19999 #define FIRST_ISOLATED_UID 99000 #define LAST_ISOLATED_UID 99999 void ksu_allowlist_init(void); void ksu_allowlist_exit(void); void ksu_load_allow_list(void); void ksu_show_allow_list(void); // Check if the uid is in allow list bool __ksu_is_allow_uid(uid_t uid); #define ksu_is_allow_uid(uid) unlikely(__ksu_is_allow_uid(uid)) // Check if the uid is in allow list, or current is ksu domain root bool __ksu_is_allow_uid_for_current(uid_t uid); #define ksu_is_allow_uid_for_current(uid) \ unlikely(__ksu_is_allow_uid_for_current(uid)) bool ksu_get_allow_list(int *array, u16 length, u16 *out_length, u16 *out_total, bool allow); void ksu_prune_allowlist(bool (*is_uid_exist)(uid_t, char *, void *), void *data); void ksu_persistent_allow_list(); bool ksu_get_app_profile(struct app_profile *); int ksu_set_app_profile(struct app_profile *); bool ksu_uid_should_umount(uid_t uid); void ksu_get_root_profile(uid_t uid, struct root_profile *); static inline bool is_appuid(uid_t uid) { uid_t appid = uid % PER_USER_RANGE; return appid >= FIRST_APPLICATION_UID && appid <= LAST_APPLICATION_UID; } static inline bool is_isolated_process(uid_t uid) { uid_t appid = uid % PER_USER_RANGE; return appid >= FIRST_ISOLATED_UID && appid <= LAST_ISOLATED_UID; } #endif extern bool allow_shell; ================================================ FILE: kernel/apk_sign.c ================================================ #include #include #include #include #include #include #ifdef CONFIG_KSU_DEBUG #include #endif #include #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) #include #else #include #endif #include "apk_sign.h" #include "app_profile.h" #include "klog.h" // IWYU pragma: keep struct sdesc { struct shash_desc shash; char ctx[]; }; static struct sdesc *init_sdesc(struct crypto_shash *alg) { struct sdesc *sdesc; int size; size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); sdesc = kzalloc(size, GFP_KERNEL); if (!sdesc) return ERR_PTR(-ENOMEM); sdesc->shash.tfm = alg; return sdesc; } static int calc_hash(struct crypto_shash *alg, const unsigned char *data, unsigned int datalen, unsigned char *digest) { struct sdesc *sdesc; int ret; sdesc = init_sdesc(alg); if (IS_ERR(sdesc)) { pr_info("can't alloc sdesc\n"); return PTR_ERR(sdesc); } ret = crypto_shash_digest(&sdesc->shash, data, datalen, digest); kfree(sdesc); return ret; } static int ksu_sha256(const unsigned char *data, unsigned int datalen, unsigned char *digest) { struct crypto_shash *alg; char *hash_alg_name = "sha256"; int ret; alg = crypto_alloc_shash(hash_alg_name, 0, 0); if (IS_ERR(alg)) { pr_info("can't alloc alg %s\n", hash_alg_name); return PTR_ERR(alg); } ret = calc_hash(alg, data, datalen, digest); crypto_free_shash(alg); return ret; } static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, unsigned expected_size, const char *expected_sha256) { kernel_read(fp, size4, 0x4, pos); // signer-sequence length kernel_read(fp, size4, 0x4, pos); // signer length kernel_read(fp, size4, 0x4, pos); // signed data length *offset += 0x4 * 3; kernel_read(fp, size4, 0x4, pos); // digests-sequence length *pos += *size4; *offset += 0x4 + *size4; kernel_read(fp, size4, 0x4, pos); // certificates length kernel_read(fp, size4, 0x4, pos); // certificate length *offset += 0x4 * 2; if (*size4 == expected_size) { *offset += *size4; #define CERT_MAX_LENGTH 1024 char cert[CERT_MAX_LENGTH]; if (*size4 > CERT_MAX_LENGTH) { pr_info("cert length overlimit\n"); return false; } kernel_read(fp, cert, *size4, pos); unsigned char digest[SHA256_DIGEST_SIZE]; if (IS_ERR(ksu_sha256(cert, *size4, digest))) { pr_info("sha256 error\n"); return false; } char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); pr_info("sha256: %s, expected: %s\n", hash_str, expected_sha256); if (strcmp(expected_sha256, hash_str) == 0) { return true; } } return false; } struct zip_entry_header { uint32_t signature; uint16_t version; uint16_t flags; uint16_t compression; uint16_t mod_time; uint16_t mod_date; uint32_t crc32; uint32_t compressed_size; uint32_t uncompressed_size; uint16_t file_name_length; uint16_t extra_field_length; } __attribute__((packed)); // This is a necessary but not sufficient condition, but it is enough for us static bool has_v1_signature_file(struct file *fp) { struct zip_entry_header header; const char MANIFEST[] = "META-INF/MANIFEST.MF"; loff_t pos = 0; while (kernel_read(fp, &header, sizeof(struct zip_entry_header), &pos) == sizeof(struct zip_entry_header)) { if (header.signature != 0x04034b50) { // ZIP magic: 'PK' return false; } // Read the entry file name if (header.file_name_length == sizeof(MANIFEST) - 1) { char fileName[sizeof(MANIFEST)]; kernel_read(fp, fileName, header.file_name_length, &pos); fileName[header.file_name_length] = '\0'; // Check if the entry matches META-INF/MANIFEST.MF if (strncmp(MANIFEST, fileName, sizeof(MANIFEST) - 1) == 0) { return true; } } else { // Skip the entry file name pos += header.file_name_length; } // Skip to the next entry pos += header.extra_field_length + header.compressed_size; } return false; } static __always_inline bool check_v2_signature(char *path, unsigned expected_size, const char *expected_sha256) { unsigned char buffer[0x11] = { 0 }; u32 size4; u64 size8, size_of_block; loff_t pos; bool v2_signing_valid = false; int v2_signing_blocks = 0; bool v3_signing_exist = false; bool v3_1_signing_exist = false; int i; struct file *fp = filp_open(path, O_RDONLY, 0); if (IS_ERR(fp)) { pr_err("open %s error.\n", path); return false; } // disable inotify for this file fp->f_mode |= FMODE_NONOTIFY; // https://en.wikipedia.org/wiki/Zip_(file_format)#End_of_central_directory_record_(EOCD) for (i = 0;; ++i) { unsigned short n; pos = generic_file_llseek(fp, -i - 2, SEEK_END); kernel_read(fp, &n, 2, &pos); if (n == i) { pos -= 22; kernel_read(fp, &size4, 4, &pos); if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) { break; } } if (i == 0xffff) { pr_info("error: cannot find eocd\n"); goto clean; } } pos += 12; // offset kernel_read(fp, &size4, 0x4, &pos); pos = size4 - 0x18; kernel_read(fp, &size8, 0x8, &pos); kernel_read(fp, buffer, 0x10, &pos); if (strcmp((char *)buffer, "APK Sig Block 42")) { goto clean; } pos = size4 - (size8 + 0x8); kernel_read(fp, &size_of_block, 0x8, &pos); if (size_of_block != size8) { goto clean; } int loop_count = 0; while (loop_count++ < 10) { uint32_t id; uint32_t offset; kernel_read(fp, &size8, 0x8, &pos); // sequence length if (size8 == size_of_block) { break; } kernel_read(fp, &id, 0x4, &pos); // id offset = 4; if (id == 0x7109871au) { v2_signing_blocks++; v2_signing_valid = check_block(fp, &size4, &pos, &offset, expected_size, expected_sha256); } else if (id == 0xf05368c0u) { // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#73 v3_signing_exist = true; } else if (id == 0x1b93ad61u) { // http://aospxref.com/android-14.0.0_r2/xref/frameworks/base/core/java/android/util/apk/ApkSignatureSchemeV3Verifier.java#74 v3_1_signing_exist = true; } else { #ifdef CONFIG_KSU_DEBUG pr_info("Unknown id: 0x%08x\n", id); #endif } pos += (size8 - offset); } if (v2_signing_blocks != 1) { #ifdef CONFIG_KSU_DEBUG pr_err("Unexpected v2 signature count: %d\n", v2_signing_blocks); #endif v2_signing_valid = false; } if (v2_signing_valid) { int has_v1_signing = has_v1_signature_file(fp); if (has_v1_signing) { pr_err("Unexpected v1 signature scheme found!\n"); filp_close(fp, 0); return false; } } clean: filp_close(fp, 0); if (v3_signing_exist || v3_1_signing_exist) { #ifdef CONFIG_KSU_DEBUG pr_err("Unexpected v3 signature scheme found!\n"); #endif return false; } return v2_signing_valid; } #ifdef CONFIG_KSU_DEBUG int ksu_debug_manager_appid = -1; #include "manager.h" static int set_expected_size(const char *val, const struct kernel_param *kp) { int rv = param_set_uint(val, kp); ksu_set_manager_appid(ksu_debug_manager_appid); pr_info("ksu_manager_appid set to %d\n", ksu_debug_manager_appid); return rv; } static struct kernel_param_ops expected_size_ops = { .set = set_expected_size, .get = param_get_uint, }; module_param_cb(ksu_debug_manager_appid, &expected_size_ops, &ksu_debug_manager_appid, S_IRUSR | S_IWUSR); #endif int get_pkg_from_apk_path(char *pkg, const char *path) { int len = strlen(path); if (len >= KSU_MAX_PACKAGE_NAME || len < 1) return -1; const char *last_slash = NULL; const char *second_last_slash = NULL; int i; for (i = len - 1; i >= 0; i--) { if (path[i] == '/') { if (!last_slash) { last_slash = &path[i]; } else { second_last_slash = &path[i]; break; } } } if (!last_slash || !second_last_slash) return -1; const char *last_hyphen = strchr(second_last_slash, '-'); if (!last_hyphen || last_hyphen > last_slash) return -1; int pkg_len = last_hyphen - second_last_slash - 1; if (pkg_len >= KSU_MAX_PACKAGE_NAME || pkg_len <= 0) return -1; // Copying the package name strncpy(pkg, second_last_slash + 1, pkg_len); pkg[pkg_len] = '\0'; return 0; } bool is_manager_apk(char *path) { #ifdef KSU_MANAGER_PACKAGE char pkg[KSU_MAX_PACKAGE_NAME]; if (get_pkg_from_apk_path(pkg, path) < 0) { pr_err("Failed to get package name from apk path: %s\n", path); return false; } // pkg is `` if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) { return false; } #endif if (check_v2_signature(path, EXPECTED_SIZE, EXPECTED_HASH)) { return true; } #ifdef EXPECTED_SIZE2 return check_v2_signature(path, EXPECTED_SIZE2, EXPECTED_HASH2); #else return false; #endif } ================================================ FILE: kernel/apk_sign.h ================================================ #ifndef __KSU_H_APK_V2_SIGN #define __KSU_H_APK_V2_SIGN #include bool is_manager_apk(char *path); int get_pkg_from_apk_path(char *pkg, const char *path); #endif ================================================ FILE: kernel/app_profile.c ================================================ #include #include #include #include #include #include #include #include #include #include #include "allowlist.h" #include "app_profile.h" #include "klog.h" // IWYU pragma: keep #include "selinux/selinux.h" #include "su_mount_ns.h" #include "syscall_hook_manager.h" #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 7, 0) static struct group_info root_groups = { .usage = REFCOUNT_INIT(2) }; #else static struct group_info root_groups = { .usage = ATOMIC_INIT(2) }; #endif void setup_groups(struct root_profile *profile, struct cred *cred) { if (profile->groups_count > KSU_MAX_GROUPS) { pr_warn("Failed to setgroups, too large group: %d!\n", profile->uid); return; } if (profile->groups_count == 1 && profile->groups[0] == 0) { // setgroup to root and return early. if (cred->group_info) put_group_info(cred->group_info); cred->group_info = get_group_info(&root_groups); return; } u32 ngroups = profile->groups_count; struct group_info *group_info = groups_alloc(ngroups); if (!group_info) { pr_warn("Failed to setgroups, ENOMEM for: %d\n", profile->uid); return; } int i; for (i = 0; i < ngroups; i++) { gid_t gid = profile->groups[i]; kgid_t kgid = make_kgid(current_user_ns(), gid); if (!gid_valid(kgid)) { pr_warn("Failed to setgroups, invalid gid: %d\n", gid); put_group_info(group_info); return; } group_info->gid[i] = kgid; } groups_sort(group_info); set_groups(cred, group_info); put_group_info(group_info); } void seccomp_filter_release(struct task_struct *tsk); static void disable_seccomp(void) { struct task_struct *fake; fake = kmalloc(sizeof(*fake), GFP_ATOMIC); if (!fake) { pr_warn("failed to alloc fake task_struct\n"); return; } // Refer to kernel/seccomp.c: seccomp_set_mode_strict // When disabling Seccomp, ensure that current->sighand->siglock is held during the operation. spin_lock_irq(¤t->sighand->siglock); // disable seccomp #if defined(CONFIG_GENERIC_ENTRY) && \ LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) clear_syscall_work(SECCOMP); #else clear_thread_flag(TIF_SECCOMP); #endif memcpy(fake, current, sizeof(*fake)); current->seccomp.mode = 0; current->seccomp.filter = NULL; atomic_set(¤t->seccomp.filter_count, 0); spin_unlock_irq(¤t->sighand->siglock); #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 11, 0) // https://github.com/torvalds/linux/commit/bfafe5efa9754ebc991750da0bcca2a6694f3ed3#diff-45eb79a57536d8eccfc1436932f093eb5c0b60d9361c39edb46581ad313e8987R576-R577 fake->flags |= PF_EXITING; #elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) // https://github.com/torvalds/linux/commit/0d8315dddd2899f519fe1ca3d4d5cdaf44ea421e#diff-45eb79a57536d8eccfc1436932f093eb5c0b60d9361c39edb46581ad313e8987R556-R558 fake->sighand = NULL; #endif seccomp_filter_release(fake); kfree(fake); } void escape_with_root_profile(void) { struct cred *cred; struct task_struct *p = current; struct task_struct *t; struct root_profile profile; struct user_struct *new_user; cred = prepare_creds(); if (!cred) { pr_warn("prepare_creds failed!\n"); return; } if (cred->euid.val == 0) { pr_warn("Already root, don't escape!\n"); goto out_abort_creds; } ksu_get_root_profile(cred->uid.val, &profile); cred->uid.val = profile.uid; cred->suid.val = profile.uid; cred->euid.val = profile.uid; cred->fsuid.val = profile.uid; cred->gid.val = profile.gid; cred->fsgid.val = profile.gid; cred->sgid.val = profile.gid; cred->egid.val = profile.gid; cred->securebits = 0; BUILD_BUG_ON(sizeof(profile.capabilities.effective) != sizeof(kernel_cap_t)); /* * Mirror the kernel set*uid path: update cred->user first, then * cred->ucounts, before commit_creds(). commit_creds() moves * RLIMIT_NPROC accounting based on cred->user; if uid changes while * user/ucounts stay stale, the old charge can remain pinned to the * previous UID. * See kernel/sys.c:set_user() and kernel/cred.c:set_cred_ucounts() / * commit_creds(): * https://github.com/torvalds/linux/blob/v5.14/kernel/sys.c * https://github.com/torvalds/linux/blob/v5.14/kernel/cred.c */ new_user = alloc_uid(cred->uid); if (!new_user) { goto out_abort_creds; } free_uid(cred->user); cred->user = new_user; // v5.14+ added cred->ucounts, so we must refresh it after changing uid/user: // https://github.com/torvalds/linux/commit/905ae01c4ae2ae3df05bb141801b1db4b7d83c61#diff-ff6060da281bd9ef3f24e17b77a9b0b5b2ed2d7208bb69b29107bee69732bd31 // on older kernels, per-UID process accounting lives in user_struct. #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 14, 0) if (set_cred_ucounts(cred)) { goto out_abort_creds; } #endif // setup capabilities // we need CAP_DAC_READ_SEARCH becuase `/data/adb/ksud` is not accessible for non root process // we add it here but don't add it to cap_inhertiable, it would be dropped automaticly after exec! u64 cap_for_ksud = profile.capabilities.effective | CAP_DAC_READ_SEARCH; memcpy(&cred->cap_effective, &cap_for_ksud, sizeof(cred->cap_effective)); memcpy(&cred->cap_permitted, &profile.capabilities.effective, sizeof(cred->cap_permitted)); memcpy(&cred->cap_bset, &profile.capabilities.effective, sizeof(cred->cap_bset)); setup_groups(&profile, cred); setup_selinux(profile.selinux_domain, cred); commit_creds(cred); disable_seccomp(); for_each_thread (p, t) { ksu_set_task_tracepoint_flag(t); } setup_mount_ns(profile.namespaces); return; out_abort_creds: abort_creds(cred); } void escape_to_root_for_init(void) { struct cred *cred = prepare_creds(); if (!cred) { pr_err("Failed to prepare init's creds!\n"); return; } setup_selinux(KERNEL_SU_CONTEXT, cred); commit_creds(cred); } ================================================ FILE: kernel/app_profile.h ================================================ #ifndef __KSU_H_APP_PROFILE #define __KSU_H_APP_PROFILE #include // Forward declarations struct cred; #define KSU_APP_PROFILE_VER 2 #define KSU_MAX_PACKAGE_NAME 256 // NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups. #define KSU_MAX_GROUPS 32 #define KSU_SELINUX_DOMAIN 64 struct root_profile { int32_t uid; int32_t gid; int32_t groups_count; int32_t groups[KSU_MAX_GROUPS]; // kernel_cap_t is u32[2] for capabilities v3 struct { u64 effective; u64 permitted; u64 inheritable; } capabilities; char selinux_domain[KSU_SELINUX_DOMAIN]; int32_t namespaces; }; struct non_root_profile { bool umount_modules; }; struct app_profile { // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. u32 version; // this is usually the package of the app, but can be other value for special apps char key[KSU_MAX_PACKAGE_NAME]; int32_t current_uid; bool allow_su; union { struct { bool use_default; char template_name[KSU_MAX_PACKAGE_NAME]; struct root_profile profile; } rp_config; struct { bool use_default; struct non_root_profile profile; } nrp_config; }; }; // Escalate current process to root with the appropriate profile void escape_with_root_profile(void); void escape_to_root_for_init(void); #endif ================================================ FILE: kernel/arch.h ================================================ #ifndef __KSU_H_ARCH #define __KSU_H_ARCH #include #if defined(__aarch64__) #define __PT_PARM1_REG regs[0] #define __PT_PARM2_REG regs[1] #define __PT_PARM3_REG regs[2] #define __PT_SYSCALL_PARM4_REG regs[3] #define __PT_CCALL_PARM4_REG regs[3] #define __PT_PARM5_REG regs[4] #define __PT_PARM6_REG regs[5] #define __PT_RET_REG regs[30] #define __PT_FP_REG regs[29] /* Works only with CONFIG_FRAME_POINTER */ #define __PT_RC_REG regs[0] #define __PT_SP_REG sp #define __PT_IP_REG pc #define REBOOT_SYMBOL "__arm64_sys_reboot" #define SYS_READ_SYMBOL "__arm64_sys_read" #define SYS_EXECVE_SYMBOL "__arm64_sys_execve" // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/scripts/syscalltbl.sh;l=57;drc=9142be9e6443fd641ca37f820efe00d9cd890eb1 // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/scripts/syscall.tbl;l=104;drc=b36d4b6aa88ef039647228b98c59a875e92f8c8e #define SYS_FSTAT_SYMBOL "__arm64_sys_newfstat" #elif defined(__x86_64__) #define __PT_PARM1_REG di #define __PT_PARM2_REG si #define __PT_PARM3_REG dx /* syscall uses r10 for PARM4 */ #define __PT_SYSCALL_PARM4_REG r10 #define __PT_CCALL_PARM4_REG cx #define __PT_PARM5_REG r8 #define __PT_PARM6_REG r9 #define __PT_RET_REG sp #define __PT_FP_REG bp #define __PT_RC_REG ax #define __PT_SP_REG sp #define __PT_IP_REG ip #define REBOOT_SYMBOL "__x64_sys_reboot" #define SYS_READ_SYMBOL "__x64_sys_read" #define SYS_EXECVE_SYMBOL "__x64_sys_execve" #define SYS_FSTAT_SYMBOL "__x64_sys_newfstat" #else #error "Unsupported arch" #endif /* allow some architecutres to override `struct pt_regs` */ #ifndef __PT_REGS_CAST #define __PT_REGS_CAST(x) (x) #endif #define PT_REGS_PARM1(x) (__PT_REGS_CAST(x)->__PT_PARM1_REG) #define PT_REGS_PARM2(x) (__PT_REGS_CAST(x)->__PT_PARM2_REG) #define PT_REGS_PARM3(x) (__PT_REGS_CAST(x)->__PT_PARM3_REG) #define PT_REGS_SYSCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_SYSCALL_PARM4_REG) #define PT_REGS_CCALL_PARM4(x) (__PT_REGS_CAST(x)->__PT_CCALL_PARM4_REG) #define PT_REGS_PARM5(x) (__PT_REGS_CAST(x)->__PT_PARM5_REG) #define PT_REGS_PARM6(x) (__PT_REGS_CAST(x)->__PT_PARM6_REG) #define PT_REGS_RET(x) (__PT_REGS_CAST(x)->__PT_RET_REG) #define PT_REGS_FP(x) (__PT_REGS_CAST(x)->__PT_FP_REG) #define PT_REGS_RC(x) (__PT_REGS_CAST(x)->__PT_RC_REG) #define PT_REGS_SP(x) (__PT_REGS_CAST(x)->__PT_SP_REG) #define PT_REGS_IP(x) (__PT_REGS_CAST(x)->__PT_IP_REG) #define PT_REAL_REGS(regs) ((struct pt_regs *)PT_REGS_PARM1(regs)) #endif ================================================ FILE: kernel/feature.c ================================================ #include "feature.h" #include "klog.h" // IWYU pragma: keep #include static const struct ksu_feature_handler *feature_handlers[KSU_FEATURE_MAX]; static DEFINE_MUTEX(feature_mutex); int ksu_register_feature_handler(const struct ksu_feature_handler *handler) { if (!handler) { pr_err("feature: register handler is NULL\n"); return -EINVAL; } if (handler->feature_id >= KSU_FEATURE_MAX) { pr_err("feature: invalid feature_id %u\n", handler->feature_id); return -EINVAL; } if (!handler->get_handler && !handler->set_handler) { pr_err("feature: no handler provided for feature %u\n", handler->feature_id); return -EINVAL; } mutex_lock(&feature_mutex); if (feature_handlers[handler->feature_id]) { pr_warn("feature: handler for %u already registered, overwriting\n", handler->feature_id); } feature_handlers[handler->feature_id] = handler; pr_info("feature: registered handler for %s (id=%u)\n", handler->name ? handler->name : "unknown", handler->feature_id); mutex_unlock(&feature_mutex); return 0; } int ksu_unregister_feature_handler(u32 feature_id) { int ret = 0; if (feature_id >= KSU_FEATURE_MAX) { pr_err("feature: invalid feature_id %u\n", feature_id); return -EINVAL; } mutex_lock(&feature_mutex); if (!feature_handlers[feature_id]) { pr_warn("feature: no handler registered for %u\n", feature_id); ret = -ENOENT; goto out; } feature_handlers[feature_id] = NULL; pr_info("feature: unregistered handler for id=%u\n", feature_id); out: mutex_unlock(&feature_mutex); return ret; } int ksu_get_feature(u32 feature_id, u64 *value, bool *supported) { int ret = 0; const struct ksu_feature_handler *handler; if (feature_id >= KSU_FEATURE_MAX) { pr_err("feature: invalid feature_id %u\n", feature_id); return -EINVAL; } if (!value || !supported) { pr_err("feature: invalid parameters\n"); return -EINVAL; } mutex_lock(&feature_mutex); handler = feature_handlers[feature_id]; if (!handler) { *supported = false; *value = 0; pr_debug("feature: feature %u not supported\n", feature_id); goto out; } *supported = true; if (!handler->get_handler) { pr_warn("feature: no get_handler for feature %u\n", feature_id); ret = -EOPNOTSUPP; goto out; } ret = handler->get_handler(value); if (ret) { pr_err("feature: get_handler for %u failed: %d\n", feature_id, ret); } out: mutex_unlock(&feature_mutex); return ret; } int ksu_set_feature(u32 feature_id, u64 value) { int ret = 0; const struct ksu_feature_handler *handler; if (feature_id >= KSU_FEATURE_MAX) { pr_err("feature: invalid feature_id %u\n", feature_id); return -EINVAL; } mutex_lock(&feature_mutex); handler = feature_handlers[feature_id]; if (!handler) { pr_err("feature: feature %u not registered\n", feature_id); ret = -EOPNOTSUPP; goto out; } if (!handler->set_handler) { pr_warn("feature: no set_handler for feature %u\n", feature_id); ret = -EOPNOTSUPP; goto out; } ret = handler->set_handler(value); if (ret) { pr_err("feature: set_handler for %u failed: %d\n", feature_id, ret); } out: mutex_unlock(&feature_mutex); return ret; } void ksu_feature_init(void) { int i; for (i = 0; i < KSU_FEATURE_MAX; i++) { feature_handlers[i] = NULL; } pr_info("feature: feature management initialized\n"); } void ksu_feature_exit(void) { int i; mutex_lock(&feature_mutex); for (i = 0; i < KSU_FEATURE_MAX; i++) { feature_handlers[i] = NULL; } mutex_unlock(&feature_mutex); pr_info("feature: feature management cleaned up\n"); } ================================================ FILE: kernel/feature.h ================================================ #ifndef __KSU_H_FEATURE #define __KSU_H_FEATURE #include enum ksu_feature_id { KSU_FEATURE_SU_COMPAT = 0, KSU_FEATURE_KERNEL_UMOUNT = 1, KSU_FEATURE_MAX }; typedef int (*ksu_feature_get_t)(u64 *value); typedef int (*ksu_feature_set_t)(u64 value); struct ksu_feature_handler { u32 feature_id; const char *name; ksu_feature_get_t get_handler; ksu_feature_set_t set_handler; }; int ksu_register_feature_handler(const struct ksu_feature_handler *handler); int ksu_unregister_feature_handler(u32 feature_id); int ksu_get_feature(u32 feature_id, u64 *value, bool *supported); int ksu_set_feature(u32 feature_id, u64 value); void ksu_feature_init(void); void ksu_feature_exit(void); #endif // __KSU_H_FEATURE ================================================ FILE: kernel/file_wrapper.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "objsec.h" #include "klog.h" // IWYU pragma: keep #include "selinux/selinux.h" #include "ksud.h" #include "file_wrapper.h" struct ksu_file_wrapper { struct file *orig; struct file_operations ops; }; static struct ksu_file_wrapper *ksu_create_file_wrapper(struct file *fp); static int ksu_wrapper_open(struct inode *ino, struct file *fp) { struct path *orig_path = fp->f_path.dentry->d_fsdata; struct file *orig_file = dentry_open(orig_path, fp->f_flags, current_cred()); if (IS_ERR(orig_file)) { return PTR_ERR(orig_file); } struct ksu_file_wrapper *wrapper = ksu_create_file_wrapper(orig_file); if (IS_ERR(wrapper)) { filp_close(orig_file, current->files); return PTR_ERR(wrapper); } fp->private_data = wrapper; const struct file_operations *new_fops = fops_get(&wrapper->ops); replace_fops(fp, new_fops); return 0; } static const struct file_operations ksu_file_wrapper_inode_fops = { .owner = THIS_MODULE, .open = ksu_wrapper_open }; static loff_t ksu_wrapper_llseek(struct file *fp, loff_t off, int flags) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->llseek(data->orig, off, flags); } static ssize_t ksu_wrapper_read(struct file *fp, char __user *ptr, size_t sz, loff_t *off) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->read(orig, ptr, sz, off); } static ssize_t ksu_wrapper_write(struct file *fp, const char __user *ptr, size_t sz, loff_t *off) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->write(orig, ptr, sz, off); } static ssize_t ksu_wrapper_read_iter(struct kiocb *iocb, struct iov_iter *iovi) { struct ksu_file_wrapper *data = iocb->ki_filp->private_data; struct file *orig = data->orig; iocb->ki_filp = orig; return orig->f_op->read_iter(iocb, iovi); } static ssize_t ksu_wrapper_write_iter(struct kiocb *iocb, struct iov_iter *iovi) { struct ksu_file_wrapper *data = iocb->ki_filp->private_data; struct file *orig = data->orig; iocb->ki_filp = orig; return orig->f_op->write_iter(iocb, iovi); } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) static int ksu_wrapper_iopoll(struct kiocb *kiocb, struct io_comp_batch *icb, unsigned int v) { struct ksu_file_wrapper *data = kiocb->ki_filp->private_data; struct file *orig = data->orig; kiocb->ki_filp = orig; return orig->f_op->iopoll(kiocb, icb, v); } #else static int ksu_wrapper_iopoll(struct kiocb *kiocb, bool spin) { struct ksu_file_wrapper *data = kiocb->ki_filp->private_data; struct file *orig = data->orig; kiocb->ki_filp = orig; return orig->f_op->iopoll(kiocb, spin); } #endif #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) static int ksu_wrapper_iterate(struct file *fp, struct dir_context *dc) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->iterate(orig, dc); } #endif static int ksu_wrapper_iterate_shared(struct file *fp, struct dir_context *dc) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->iterate_shared(orig, dc); } static __poll_t ksu_wrapper_poll(struct file *fp, struct poll_table_struct *pts) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->poll(orig, pts); } static long ksu_wrapper_unlocked_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->unlocked_ioctl(orig, cmd, arg); } static long ksu_wrapper_compat_ioctl(struct file *fp, unsigned int cmd, unsigned long arg) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->compat_ioctl(orig, cmd, arg); } static int ksu_wrapper_mmap(struct file *fp, struct vm_area_struct *vma) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->mmap(orig, vma); } static int ksu_wrapper_flush(struct file *fp, fl_owner_t id) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->flush(orig, id); } static int ksu_wrapper_fsync(struct file *fp, loff_t off1, loff_t off2, int datasync) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->fsync(orig, off1, off2, datasync); } static int ksu_wrapper_fasync(int arg, struct file *fp, int arg2) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->fasync(arg, orig, arg2); } static int ksu_wrapper_lock(struct file *fp, int arg1, struct file_lock *fl) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; return orig->f_op->lock(orig, arg1, fl); } #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) static ssize_t ksu_wrapper_sendpage(struct file *fp, struct page *pg, int arg1, size_t sz, loff_t *off, int arg2) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->sendpage) { return orig->f_op->sendpage(orig, pg, arg1, sz, off, arg2); } return -EINVAL; } #endif static unsigned long ksu_wrapper_get_unmapped_area(struct file *fp, unsigned long arg1, unsigned long arg2, unsigned long arg3, unsigned long arg4) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->get_unmapped_area) { return orig->f_op->get_unmapped_area(orig, arg1, arg2, arg3, arg4); } return -EINVAL; } // static int ksu_wrapper_check_flags(int arg) {} static int ksu_wrapper_flock(struct file *fp, int arg1, struct file_lock *fl) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->flock) { return orig->f_op->flock(orig, arg1, fl); } return -EINVAL; } static ssize_t ksu_wrapper_splice_write(struct pipe_inode_info *pii, struct file *fp, loff_t *off, size_t sz, unsigned int arg1) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->splice_write) { return orig->f_op->splice_write(pii, orig, off, sz, arg1); } return -EINVAL; } static ssize_t ksu_wrapper_splice_read(struct file *fp, loff_t *off, struct pipe_inode_info *pii, size_t sz, unsigned int arg1) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->splice_read) { return orig->f_op->splice_read(orig, off, pii, sz, arg1); } return -EINVAL; } #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) void ksu_wrapper_splice_eof(struct file *fp) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->splice_eof) { return orig->f_op->splice_eof(orig); } } #endif #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) static int ksu_wrapper_setlease(struct file *fp, int arg1, struct file_lease **fl, void **p) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->setlease) { return orig->f_op->setlease(orig, arg1, fl, p); } return -EINVAL; } #elif LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) static int ksu_wrapper_setlease(struct file *fp, int arg1, struct file_lock **fl, void **p) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->setlease) { return orig->f_op->setlease(orig, arg1, fl, p); } return -EINVAL; } #else static int ksu_wrapper_setlease(struct file *fp, long arg1, struct file_lock **fl, void **p) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->setlease) { return orig->f_op->setlease(orig, arg1, fl, p); } return -EINVAL; } #endif static long ksu_wrapper_fallocate(struct file *fp, int mode, loff_t offset, loff_t len) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->fallocate) { return orig->f_op->fallocate(orig, mode, offset, len); } return -EINVAL; } static void ksu_wrapper_show_fdinfo(struct seq_file *m, struct file *f) { struct ksu_file_wrapper *data = f->private_data; struct file *orig = data->orig; if (orig->f_op->show_fdinfo) { orig->f_op->show_fdinfo(m, orig); } } // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/read_write.c;l=1593-1606;drc=398da7defe218d3e51b0f3bdff75147e28125b60 static ssize_t ksu_wrapper_copy_file_range(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, size_t len, unsigned int flags) { struct ksu_file_wrapper *data = file_out->private_data; struct file *orig = data->orig; return orig->f_op->copy_file_range(file_in, pos_in, orig, pos_out, len, flags); } // no REMAP_FILE_DEDUP: use file_in // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/read_write.c;l=1598-1599;drc=398da7defe218d3e51b0f3bdff75147e28125b60 // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/remap_range.c;l=403-404;drc=398da7defe218d3e51b0f3bdff75147e28125b60 // REMAP_FILE_DEDUP: use file_out // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/remap_range.c;l=483-484;drc=398da7defe218d3e51b0f3bdff75147e28125b60 static loff_t ksu_wrapper_remap_file_range(struct file *file_in, loff_t pos_in, struct file *file_out, loff_t pos_out, loff_t len, unsigned int remap_flags) { if (remap_flags & REMAP_FILE_DEDUP) { struct ksu_file_wrapper *data = file_out->private_data; struct file *orig = data->orig; return orig->f_op->remap_file_range(file_in, pos_in, orig, pos_out, len, remap_flags); } else { struct ksu_file_wrapper *data = file_in->private_data; struct file *orig = data->orig; return orig->f_op->remap_file_range(orig, pos_in, file_out, pos_out, len, remap_flags); } } static int ksu_wrapper_fadvise(struct file *fp, loff_t off1, loff_t off2, int flags) { struct ksu_file_wrapper *data = fp->private_data; struct file *orig = data->orig; if (orig->f_op->fadvise) { return orig->f_op->fadvise(orig, off1, off2, flags); } return -EINVAL; } static void ksu_release_file_wrapper(struct ksu_file_wrapper *data); static int ksu_wrapper_release(struct inode *inode, struct file *filp) { // https://cs.android.com/android/kernel/superproject/+/common-android-mainline:common/fs/file_table.c;l=467-473;drc=3be0b283b562eabbc2b1f3bb534dc8903079bbaa // f_op->release is called before fops_put(f_op), so we put it manually. fops_put(filp->f_op); // prevent it from being put again filp->f_op = NULL; ksu_release_file_wrapper(filp->private_data); return 0; } static struct ksu_file_wrapper *ksu_create_file_wrapper(struct file *fp) { struct ksu_file_wrapper *p = kcalloc(1, sizeof(struct ksu_file_wrapper), GFP_KERNEL); if (!p) { return ERR_PTR(-ENOMEM); } get_file(fp); p->orig = fp; p->ops.owner = THIS_MODULE; p->ops.llseek = fp->f_op->llseek ? ksu_wrapper_llseek : NULL; p->ops.read = fp->f_op->read ? ksu_wrapper_read : NULL; p->ops.write = fp->f_op->write ? ksu_wrapper_write : NULL; p->ops.read_iter = fp->f_op->read_iter ? ksu_wrapper_read_iter : NULL; p->ops.write_iter = fp->f_op->write_iter ? ksu_wrapper_write_iter : NULL; p->ops.iopoll = fp->f_op->iopoll ? ksu_wrapper_iopoll : NULL; #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) p->ops.iterate = fp->f_op->iterate ? ksu_wrapper_iterate : NULL; #endif p->ops.iterate_shared = fp->f_op->iterate_shared ? ksu_wrapper_iterate_shared : NULL; p->ops.poll = fp->f_op->poll ? ksu_wrapper_poll : NULL; p->ops.unlocked_ioctl = fp->f_op->unlocked_ioctl ? ksu_wrapper_unlocked_ioctl : NULL; p->ops.compat_ioctl = fp->f_op->compat_ioctl ? ksu_wrapper_compat_ioctl : NULL; p->ops.mmap = fp->f_op->mmap ? ksu_wrapper_mmap : NULL; #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) p->ops.fop_flags = fp->f_op->fop_flags; #else p->ops.mmap_supported_flags = fp->f_op->mmap_supported_flags; #endif p->ops.flush = fp->f_op->flush ? ksu_wrapper_flush : NULL; p->ops.release = ksu_wrapper_release; p->ops.fsync = fp->f_op->fsync ? ksu_wrapper_fsync : NULL; p->ops.fasync = fp->f_op->fasync ? ksu_wrapper_fasync : NULL; p->ops.lock = fp->f_op->lock ? ksu_wrapper_lock : NULL; #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) p->ops.sendpage = fp->f_op->sendpage ? ksu_wrapper_sendpage : NULL; #endif p->ops.get_unmapped_area = fp->f_op->get_unmapped_area ? ksu_wrapper_get_unmapped_area : NULL; p->ops.check_flags = fp->f_op->check_flags; p->ops.flock = fp->f_op->flock ? ksu_wrapper_flock : NULL; p->ops.splice_write = fp->f_op->splice_write ? ksu_wrapper_splice_write : NULL; p->ops.splice_read = fp->f_op->splice_read ? ksu_wrapper_splice_read : NULL; p->ops.setlease = fp->f_op->setlease ? ksu_wrapper_setlease : NULL; p->ops.fallocate = fp->f_op->fallocate ? ksu_wrapper_fallocate : NULL; p->ops.show_fdinfo = fp->f_op->show_fdinfo ? ksu_wrapper_show_fdinfo : NULL; p->ops.copy_file_range = fp->f_op->copy_file_range ? ksu_wrapper_copy_file_range : NULL; p->ops.remap_file_range = fp->f_op->remap_file_range ? ksu_wrapper_remap_file_range : NULL; p->ops.fadvise = fp->f_op->fadvise ? ksu_wrapper_fadvise : NULL; #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 6, 0) p->ops.splice_eof = fp->f_op->splice_eof ? ksu_wrapper_splice_eof : NULL; #endif return p; } static void ksu_release_file_wrapper(struct ksu_file_wrapper *data) { fput((struct file *)data->orig); kfree(data); } static char *ksu_wrapper_d_dname(struct dentry *dentry, char *buffer, int buflen) { struct path *orig_path = dentry->d_fsdata; return d_path(orig_path, buffer, buflen); } static void ksu_wrapper_d_release(struct dentry *dentry) { struct path *orig_path = dentry->d_fsdata; path_put(orig_path); kfree(orig_path); } static const struct dentry_operations ksu_file_wrapper_d_ops = { .d_dname = ksu_wrapper_d_dname, .d_release = ksu_wrapper_d_release }; #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 8, 0) #define ksu_anon_inode_create_getfile_compat anon_inode_create_getfile #elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 16, 0) #define ksu_anon_inode_create_getfile_compat anon_inode_getfile_secure #else // There is no anon_inode_create_getfile before 5.16, but it's not difficult to implement it. // https://cs.android.com/android/kernel/superproject/+/common-android12-5.10:common/fs/anon_inodes.c;l=58-125;drc=0d34ce8aa78e38affbb501690bcabec4df88620e // Borrow kernel's anon_inode_mnt, so that we don't need to mount one by ourselves. static struct vfsmount *anon_inode_mnt __read_mostly; static struct inode * ksu_anon_inode_make_secure_inode(const char *name, const struct inode *context_inode) { struct inode *inode; const struct qstr qname = QSTR_INIT(name, strlen(name)); int error; if (unlikely(!anon_inode_mnt)) { return ERR_PTR(-ENODEV); } inode = alloc_anon_inode(anon_inode_mnt->mnt_sb); if (IS_ERR(inode)) return inode; inode->i_flags &= ~S_PRIVATE; error = security_inode_init_security_anon(inode, &qname, context_inode); if (error) { iput(inode); return ERR_PTR(error); } return inode; } static struct file *ksu_anon_inode_create_getfile_compat( const char *name, const struct file_operations *fops, void *priv, int flags, const struct inode *context_inode) { struct inode *inode; struct file *file; if (fops->owner && !try_module_get(fops->owner)) return ERR_PTR(-ENOENT); inode = ksu_anon_inode_make_secure_inode(name, context_inode); if (IS_ERR(inode)) { file = ERR_CAST(inode); goto err; } file = alloc_file_pseudo(inode, anon_inode_mnt, name, flags & (O_ACCMODE | O_NONBLOCK), fops); if (IS_ERR(file)) goto err_iput; file->f_mapping = inode->i_mapping; file->private_data = priv; return file; err_iput: iput(inode); err: module_put(fops->owner); return file; } #endif int ksu_install_file_wrapper(int fd) { int out_fd, ret; struct file *orig_file = fget(fd); if (!orig_file) { return -EBADF; } out_fd = get_unused_fd_flags(O_CLOEXEC); if (out_fd < 0) { ret = out_fd; goto done; } struct ksu_file_wrapper *file_wrapper_data = ksu_create_file_wrapper(orig_file); if (IS_ERR(file_wrapper_data)) { ret = PTR_ERR(file_wrapper_data); goto out_put_fd; } struct file *wrapper_file = ksu_anon_inode_create_getfile_compat( "[ksu_fdwrapper]", &file_wrapper_data->ops, file_wrapper_data, orig_file->f_flags, NULL); if (IS_ERR(wrapper_file)) { pr_err("ksu_fdwrapper: getfile failed: %ld\n", PTR_ERR(wrapper_file)); ret = PTR_ERR(wrapper_file); goto out_release_wrapper; } // Now do magic on inode and dentry. // It should be safe to modify them since the file hasn't been published. struct inode *wrapper_inode = file_inode(wrapper_file); // libc's stdio relies on the fstat() result of the fd to determine its buffer type. wrapper_inode->i_mode = file_inode(orig_file)->i_mode; struct inode_security_struct *wrapper_sec = selinux_inode(wrapper_inode); // Use ksu_file_sid to bypass SELinux check. // When we call `su` from terminal app, this is useful. if (wrapper_sec) { wrapper_sec->sid = ksu_file_sid; } // Install open file operation for inode. wrapper_inode->i_fop = &ksu_file_wrapper_inode_fops; struct path *orig_path = kmalloc(sizeof(struct path), GFP_KERNEL); if (!orig_path) { ret = -ENOMEM; goto out_put_wrapper_file; } *orig_path = orig_file->f_path; path_get(orig_path); // Some applications (such as screen) won't work if the tty's path is weird, // Therefore, we use d_dname to spoof it to return the path to the original file. wrapper_file->f_path.dentry->d_fsdata = orig_path; wrapper_file->f_path.dentry->d_op = &ksu_file_wrapper_d_ops; fd_install(out_fd, wrapper_file); ret = out_fd; goto done; out_put_wrapper_file: fput(wrapper_file); // file_wrapper will be released by fput goto out_put_fd; out_release_wrapper: ksu_release_file_wrapper(file_wrapper_data); out_put_fd: put_unused_fd(out_fd); done: fput(orig_file); return ret; } void ksu_file_wrapper_init(void) { #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 16, 0) static const struct file_operations tmp = { .owner = THIS_MODULE }; struct file *dummy = anon_inode_getfile("dummy", &tmp, NULL, 0); if (IS_ERR(dummy)) { pr_err( "file_wrapper: initialize anon_inode_mnt failed, can't get file: %ld\n", PTR_ERR(dummy)); return; } anon_inode_mnt = dummy->f_path.mnt; if (unlikely(!anon_inode_mnt)) { pr_err("file_wrapper: initialize anon_inode_mnt failed, got NULL\n"); } fput(dummy); #endif } ================================================ FILE: kernel/file_wrapper.h ================================================ #ifndef KSU_FILE_WRAPPER_H #define KSU_FILE_WRAPPER_H #include #include int ksu_install_file_wrapper(int fd); void ksu_file_wrapper_init(void); #endif // KSU_FILE_WRAPPER_H ================================================ FILE: kernel/kernel_umount.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include "kernel_umount.h" #include "klog.h" // IWYU pragma: keep #include "allowlist.h" #include "selinux/selinux.h" #include "feature.h" #include "ksud.h" #include "ksu.h" static bool ksu_kernel_umount_enabled = true; static int kernel_umount_feature_get(u64 *value) { *value = ksu_kernel_umount_enabled ? 1 : 0; return 0; } static int kernel_umount_feature_set(u64 value) { bool enable = value != 0; ksu_kernel_umount_enabled = enable; pr_info("kernel_umount: set to %d\n", enable); return 0; } static const struct ksu_feature_handler kernel_umount_handler = { .feature_id = KSU_FEATURE_KERNEL_UMOUNT, .name = "kernel_umount", .get_handler = kernel_umount_feature_get, .set_handler = kernel_umount_feature_set, }; extern int path_umount(struct path *path, int flags); static void ksu_umount_mnt(struct path *path, int flags) { int err = path_umount(path, flags); if (err) { pr_info("umount %s failed: %d\n", path->dentry->d_iname, err); } } static void try_umount(const char *mnt, int flags) { struct path path; int err = kern_path(mnt, 0, &path); if (err) { return; } if (path.dentry != path.mnt->mnt_root) { // it is not root mountpoint, maybe umounted by others already. path_put(&path); return; } ksu_umount_mnt(&path, flags); } struct umount_tw { struct callback_head cb; }; static void umount_tw_func(struct callback_head *cb) { struct umount_tw *tw = container_of(cb, struct umount_tw, cb); const struct cred *saved = override_creds(ksu_cred); struct mount_entry *entry; down_read(&mount_list_lock); list_for_each_entry (entry, &mount_list, list) { pr_info("%s: unmounting: %s flags 0x%x\n", __func__, entry->umountable, entry->flags); try_umount(entry->umountable, entry->flags); } up_read(&mount_list_lock); revert_creds(saved); kfree(tw); } int ksu_handle_umount(uid_t old_uid, uid_t new_uid) { struct umount_tw *tw; // if there isn't any module mounted, just ignore it! if (!ksu_module_mounted) { return 0; } if (!ksu_kernel_umount_enabled) { return 0; } if (!ksu_cred) { return 0; } // There are 5 scenarios: // 1. Normal app: zygote -> appuid // 2. Isolated process forked from zygote: zygote -> isolated_process // 3. App zygote forked from zygote: zygote -> appuid // 4. Isolated process froked from app zygote: appuid -> isolated_process (already handled by 3) // 5. Isolated process froked from webview zygote (no need to handle, app cannot run custom code) if (!is_appuid(new_uid) && !is_isolated_process(new_uid)) { return 0; } if (!ksu_uid_should_umount(new_uid) && !is_isolated_process(new_uid)) { return 0; } // check old process's selinux context, if it is not zygote, ignore it! // because some su apps may setuid to untrusted_app but they are in global mount namespace // when we umount for such process, that is a disaster! // also handle case 4 and 5 bool is_zygote_child = is_zygote(get_current_cred()); if (!is_zygote_child) { pr_info("handle umount ignore non zygote child: %d\n", current->pid); return 0; } // umount the target mnt pr_info("handle umount for uid: %d, pid: %d\n", new_uid, current->pid); tw = kzalloc(sizeof(*tw), GFP_ATOMIC); if (!tw) return 0; tw->cb.func = umount_tw_func; int err = task_work_add(current, &tw->cb, TWA_RESUME); if (err) { kfree(tw); pr_warn("unmount add task_work failed\n"); } return 0; } void ksu_kernel_umount_init(void) { if (ksu_register_feature_handler(&kernel_umount_handler)) { pr_err("Failed to register kernel_umount feature handler\n"); } } void ksu_kernel_umount_exit(void) { ksu_unregister_feature_handler(KSU_FEATURE_KERNEL_UMOUNT); } ================================================ FILE: kernel/kernel_umount.h ================================================ #ifndef __KSU_H_KERNEL_UMOUNT #define __KSU_H_KERNEL_UMOUNT #include #include #include void ksu_kernel_umount_init(void); void ksu_kernel_umount_exit(void); // Handler function to be called from setresuid hook int ksu_handle_umount(uid_t old_uid, uid_t new_uid); // for the umount list struct mount_entry { char *umountable; unsigned int flags; struct list_head list; }; extern struct list_head mount_list; extern struct rw_semaphore mount_list_lock; #endif ================================================ FILE: kernel/klog.h ================================================ #ifndef __KSU_H_KLOG #define __KSU_H_KLOG #include #ifdef pr_fmt #undef pr_fmt #define pr_fmt(fmt) "KernelSU: " fmt #endif #endif ================================================ FILE: kernel/ksu.c ================================================ #include #include #include #include #include #include #include #include "allowlist.h" #include "app_profile.h" #include "feature.h" #include "klog.h" // IWYU pragma: keep #include "manager.h" #include "throne_tracker.h" #include "syscall_hook_manager.h" #include "ksud.h" #include "supercalls.h" #include "ksu.h" #include "file_wrapper.h" #include "selinux/selinux.h" // workaround for A12-5.10 kernel // Some third-party kernel (e.g. linegaeOS) uses wrong toolchain, which supports // CC_HAVE_STACKPROTECTOR_SYSREG while gki's toolchain doesn't. // Therefore, ksu lkm, which uses gki toolchain, requires this __stack_chk_guard, // while those third-party kernel can't provide. // Thus, we manually provide it instead of using kernel's #if defined(CONFIG_STACKPROTECTOR) && \ (defined(CONFIG_ARM64) && defined(MODULE) && \ !defined(CONFIG_STACKPROTECTOR_PER_TASK)) #include #include unsigned long __stack_chk_guard __ro_after_init __attribute__((visibility("hidden"))); __attribute__((no_stack_protector)) void ksu_setup_stack_chk_guard() { unsigned long canary; /* Try to get a semi random initial value. */ get_random_bytes(&canary, sizeof(canary)); canary ^= LINUX_VERSION_CODE; canary &= CANARY_MASK; __stack_chk_guard = canary; } __attribute__((naked)) int __init kernelsu_init_early(void) { asm("mov x19, x30;\n" "bl ksu_setup_stack_chk_guard;\n" "mov x30, x19;\n" "b kernelsu_init;\n"); } #define NEED_OWN_STACKPROTECTOR 1 #else #define NEED_OWN_STACKPROTECTOR 0 #endif struct cred *ksu_cred; bool ksu_late_loaded; #ifdef CONFIG_KSU_DEBUG bool allow_shell = true; #else bool allow_shell = false; #endif module_param(allow_shell, bool, 0); int __init kernelsu_init(void) { #ifdef MODULE ksu_late_loaded = (current->pid != 1); #else ksu_late_loaded = false; #endif #ifdef CONFIG_KSU_DEBUG pr_alert("*************************************************************"); pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); pr_alert("** **"); pr_alert("** You are running KernelSU in DEBUG mode **"); pr_alert("** **"); pr_alert("** NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE NOTICE **"); pr_alert("*************************************************************"); #endif if (allow_shell) { pr_alert("shell is allowed at init!"); } ksu_cred = prepare_creds(); if (!ksu_cred) { pr_err("prepare cred failed!\n"); } ksu_feature_init(); ksu_supercalls_init(); if (ksu_late_loaded) { pr_info("late load mode, skipping kprobe hooks\n"); apply_kernelsu_rules(); cache_sid(); setup_ksu_cred(); // Grant current process (ksud late-load) root // with KSU SELinux domain before enforcing SELinux, so it // can continue to access /data/app etc. after enforcement. escape_to_root_for_init(); ksu_allowlist_init(); ksu_load_allow_list(); ksu_syscall_hook_manager_init(); ksu_throne_tracker_init(); ksu_observer_init(); ksu_file_wrapper_init(); ksu_boot_completed = true; track_throne(false); if (!getenforce()) { pr_info("Permissive SELinux, enforcing\n"); setenforce(true); } } else { ksu_syscall_hook_manager_init(); ksu_allowlist_init(); ksu_throne_tracker_init(); ksu_ksud_init(); ksu_file_wrapper_init(); } #ifdef MODULE #ifndef CONFIG_KSU_DEBUG kobject_del(&THIS_MODULE->mkobj.kobj); #endif #endif return 0; } extern void ksu_observer_exit(void); void kernelsu_exit(void) { ksu_allowlist_exit(); ksu_throne_tracker_exit(); ksu_observer_exit(); if (!ksu_late_loaded) ksu_ksud_exit(); ksu_syscall_hook_manager_exit(); ksu_supercalls_exit(); ksu_feature_exit(); if (ksu_cred) { put_cred(ksu_cred); } } #if NEED_OWN_STACKPROTECTOR module_init(kernelsu_init_early); #else module_init(kernelsu_init); #endif module_exit(kernelsu_exit); MODULE_LICENSE("GPL"); MODULE_AUTHOR("weishu"); MODULE_DESCRIPTION("Android KernelSU"); #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 13, 0) MODULE_IMPORT_NS("VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver"); #else MODULE_IMPORT_NS(VFS_internal_I_am_really_a_filesystem_and_am_NOT_a_driver); #endif ================================================ FILE: kernel/ksu.h ================================================ #ifndef __KSU_H_KSU #define __KSU_H_KSU #include #include #include #define KERNEL_SU_VERSION KSU_VERSION #define EVENT_POST_FS_DATA 1 #define EVENT_BOOT_COMPLETED 2 #define EVENT_MODULE_MOUNTED 3 static inline int startswith(char *s, char *prefix) { return strncmp(s, prefix, strlen(prefix)); } static inline int endswith(const char *s, const char *t) { size_t slen = strlen(s); size_t tlen = strlen(t); if (tlen > slen) return 1; return strcmp(s + slen - tlen, t); } extern struct cred *ksu_cred; extern bool ksu_late_loaded; #endif ================================================ FILE: kernel/ksud.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "manager.h" #include "allowlist.h" #include "arch.h" #include "klog.h" // IWYU pragma: keep #include "ksu.h" #include "ksud.h" #include "util.h" #include "selinux/selinux.h" #include "throne_tracker.h" bool ksu_module_mounted __read_mostly = false; bool ksu_boot_completed __read_mostly = false; static const char KERNEL_SU_RC[] = "\n" "on post-fs-data\n" " start logd\n" // We should wait for the post-fs-data finish " exec u:r:" KERNEL_SU_DOMAIN ":s0 root -- " KSUD_PATH " post-fs-data\n" "\n" "on nonencrypted\n" " exec u:r:" KERNEL_SU_DOMAIN ":s0 root -- " KSUD_PATH " services\n" "\n" "on property:vold.decrypt=trigger_restart_framework\n" " exec u:r:" KERNEL_SU_DOMAIN ":s0 root -- " KSUD_PATH " services\n" "\n" "on property:sys.boot_completed=1\n" " exec u:r:" KERNEL_SU_DOMAIN ":s0 root -- " KSUD_PATH " boot-completed\n" "\n" "\n"; static void stop_init_rc_hook(); static void stop_execve_hook(); static void stop_input_hook(); static struct work_struct stop_init_rc_hook_work; static struct work_struct stop_execve_hook_work; static struct work_struct stop_input_hook_work; void on_post_fs_data(void) { static bool done = false; if (done) { pr_info("on_post_fs_data already done\n"); return; } done = true; pr_info("on_post_fs_data!\n"); ksu_load_allow_list(); ksu_observer_init(); // sanity check, this may influence the performance stop_input_hook(); } extern void ext4_unregister_sysfs(struct super_block *sb); int nuke_ext4_sysfs(const char *mnt) { struct path path; int err = kern_path(mnt, 0, &path); if (err) { pr_err("nuke path err: %d\n", err); return err; } struct super_block *sb = path.dentry->d_inode->i_sb; const char *name = sb->s_type->name; if (strcmp(name, "ext4") != 0) { pr_info("nuke but module aren't mounted\n"); path_put(&path); return -EINVAL; } ext4_unregister_sysfs(sb); path_put(&path); return 0; } void on_module_mounted(void) { pr_info("on_module_mounted!\n"); ksu_module_mounted = true; } void on_boot_completed(void) { ksu_boot_completed = true; pr_info("on_boot_completed!\n"); track_throne(true); } #define MAX_ARG_STRINGS 0x7FFFFFFF struct user_arg_ptr { #ifdef CONFIG_COMPAT bool is_compat; #endif union { const char __user *const __user *native; #ifdef CONFIG_COMPAT const compat_uptr_t __user *compat; #endif } ptr; }; static const char __user *get_user_arg_ptr(struct user_arg_ptr argv, int nr) { const char __user *native; #ifdef CONFIG_COMPAT if (unlikely(argv.is_compat)) { compat_uptr_t compat; if (get_user(compat, argv.ptr.compat + nr)) return ERR_PTR(-EFAULT); return compat_ptr(compat); } #endif if (get_user(native, argv.ptr.native + nr)) return ERR_PTR(-EFAULT); return native; } /* * count() counts the number of strings in array ARGV. */ /* * Make sure old GCC compiler can use __maybe_unused, * Test passed in 4.4.x ~ 4.9.x when use GCC. */ static int __maybe_unused count(struct user_arg_ptr argv, int max) { int i = 0; if (argv.ptr.native != NULL) { for (;;) { const char __user *p = get_user_arg_ptr(argv, i); if (!p) break; if (IS_ERR(p)) return -EFAULT; if (i >= max) return -E2BIG; ++i; if (fatal_signal_pending(current)) return -ERESTARTNOHAND; } } return i; } static void on_post_fs_data_cbfun(struct callback_head *cb) { on_post_fs_data(); } static struct callback_head on_post_fs_data_cb = { .func = on_post_fs_data_cbfun }; static bool check_argv(struct user_arg_ptr argv, int index, const char *expected, char *buf, size_t buf_len) { const char __user *p; int argc; argc = count(argv, MAX_ARG_STRINGS); if (argc <= index) return false; p = get_user_arg_ptr(argv, index); if (!p || IS_ERR(p)) goto fail; if (strncpy_from_user_nofault(buf, p, buf_len) <= 0) goto fail; buf[buf_len - 1] = '\0'; return !strcmp(buf, expected); fail: pr_err("check_argv failed\n"); return false; } static void ksu_initialize_selinux_tw_func(struct callback_head *cb) { apply_kernelsu_rules(); cache_sid(); setup_ksu_cred(); kfree(cb); } // IMPORTANT NOTE: the call from execve_handler_pre WON'T provided correct value for envp and flags in GKI version int ksu_handle_execveat_ksud(int *fd, struct filename **filename_ptr, struct user_arg_ptr *argv, struct user_arg_ptr *envp, int *flags) { struct filename *filename; static const char app_process[] = "/system/bin/app_process"; static bool first_zygote = true; /* This applies to versions Android 10+ */ static const char system_bin_init[] = "/system/bin/init"; static bool init_second_stage_executed = false; if (!filename_ptr) return 0; filename = *filename_ptr; if (IS_ERR(filename)) { return 0; } // https://cs.android.com/android/platform/superproject/+/android-16.0.0_r2:system/core/init/main.cpp;l=77 if (unlikely(!memcmp(filename->name, system_bin_init, sizeof(system_bin_init) - 1) && argv)) { char buf[16]; if (!init_second_stage_executed && check_argv(*argv, 1, "second_stage", buf, sizeof(buf))) { pr_info("/system/bin/init second_stage executed\n"); struct callback_head *cb = kzalloc(sizeof(*cb), GFP_ATOMIC); if (cb) { cb->func = ksu_initialize_selinux_tw_func; if (task_work_add(current, cb, TWA_RESUME)) { kfree(cb); pr_warn("ksu_initialize_selinux failed to add task work\n"); } } else { pr_warn( "ksu_initialize_selinux failed to allocate task work\n"); } init_second_stage_executed = true; } } if (unlikely( first_zygote && !memcmp(filename->name, app_process, sizeof(app_process) - 1) && argv)) { char buf[16]; if (check_argv(*argv, 1, "-Xzygote", buf, sizeof(buf))) { pr_info("exec zygote, /data prepared, second_stage: %d\n", init_second_stage_executed); rcu_read_lock(); struct task_struct *init_task = rcu_dereference(current->real_parent); if (init_task) task_work_add(init_task, &on_post_fs_data_cb, TWA_RESUME); rcu_read_unlock(); first_zygote = false; stop_execve_hook(); } } return 0; } static ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *); static ssize_t (*orig_read_iter)(struct kiocb *, struct iov_iter *); static struct file_operations fops_proxy; static ssize_t ksu_rc_pos = 0; const size_t ksu_rc_len = sizeof(KERNEL_SU_RC) - 1; // https://cs.android.com/android/platform/superproject/main/+/main:system/core/init/parser.cpp;l=144;drc=61197364367c9e404c7da6900658f1b16c42d0da // https://cs.android.com/android/platform/superproject/main/+/main:system/libbase/file.cpp;l=241-243;drc=61197364367c9e404c7da6900658f1b16c42d0da // The system will read init.rc file until EOF, whenever read() returns 0, // so we begin append ksu rc when we meet EOF. static ssize_t read_proxy(struct file *file, char __user *buf, size_t count, loff_t *pos) { ssize_t ret = 0; size_t append_count; if (ksu_rc_pos && ksu_rc_pos < ksu_rc_len) goto append_ksu_rc; ret = orig_read(file, buf, count, pos); if (ret != 0 || ksu_rc_pos >= ksu_rc_len) { return ret; } else { pr_info("read_proxy: orig read finished, start append rc\n"); } append_ksu_rc: append_count = ksu_rc_len - ksu_rc_pos; if (append_count > count - ret) append_count = count - ret; // copy_to_user returns the number of not copied if (copy_to_user(buf + ret, KERNEL_SU_RC + ksu_rc_pos, append_count)) { pr_info("read_proxy: append error, totally appended %ld\n", ksu_rc_pos); } else { pr_info("read_proxy: append %ld\n", append_count); ksu_rc_pos += append_count; if (ksu_rc_pos == ksu_rc_len) { pr_info("read_proxy: append done\n"); } ret += append_count; } return ret; } static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to) { ssize_t ret = 0; size_t append_count; if (ksu_rc_pos && ksu_rc_pos < ksu_rc_len) goto append_ksu_rc; ret = orig_read_iter(iocb, to); if (ret != 0 || ksu_rc_pos >= ksu_rc_len) { return ret; } else { pr_info("read_iter_proxy: orig read finished, start append rc\n"); } append_ksu_rc: // copy_to_iter returns the number of copied bytes append_count = copy_to_iter(KERNEL_SU_RC + ksu_rc_pos, ksu_rc_len - ksu_rc_pos, to); if (!append_count) { pr_info("read_iter_proxy: append error, totally appended %ld\n", ksu_rc_pos); } else { pr_info("read_iter_proxy: append %ld\n", append_count); ksu_rc_pos += append_count; if (ksu_rc_pos == ksu_rc_len) { pr_info("read_iter_proxy: append done\n"); } ret += append_count; } return ret; } static bool is_init_rc(struct file *fp) { if (strcmp(current->comm, "init")) { // we are only interest in `init` process return false; } if (!d_is_reg(fp->f_path.dentry)) { return false; } const char *short_name = fp->f_path.dentry->d_name.name; if (strcmp(short_name, "init.rc")) { // we are only interest `init.rc` file name file return false; } char path[256]; char *dpath = d_path(&fp->f_path, path, sizeof(path)); if (IS_ERR(dpath)) { return false; } if (strcmp(dpath, "/system/etc/init/hw/init.rc")) { return false; } return true; } static void ksu_handle_sys_read(unsigned int fd) { struct file *file = fget(fd); if (!file) { return; } if (!is_init_rc(file)) { goto skip; } // we only process the first read static bool rc_hooked = false; if (rc_hooked) { // we don't need these kprobe, unregister it! stop_init_rc_hook(); goto skip; } rc_hooked = true; // now we can sure that the init process is reading // `/system/etc/init/init.rc` pr_info("read init.rc, comm: %s, rc_count: %zu\n", current->comm, ksu_rc_len); // Now we need to proxy the read and modify the result! // But, we can not modify the file_operations directly, because it's in read-only memory. // We just replace the whole file_operations with a proxy one. memcpy(&fops_proxy, file->f_op, sizeof(struct file_operations)); orig_read = file->f_op->read; if (orig_read) { fops_proxy.read = read_proxy; } orig_read_iter = file->f_op->read_iter; if (orig_read_iter) { fops_proxy.read_iter = read_iter_proxy; } // replace the file_operations file->f_op = &fops_proxy; skip: fput(file); } static unsigned int volumedown_pressed_count = 0; static bool is_volumedown_enough(unsigned int count) { return count >= 3; } int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value) { if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) { int val = *value; pr_info("KEY_VOLUMEDOWN val: %d\n", val); if (val) { // key pressed, count it volumedown_pressed_count += 1; if (is_volumedown_enough(volumedown_pressed_count)) { stop_input_hook(); } } } return 0; } bool ksu_is_safe_mode() { static bool safe_mode = false; if (safe_mode) { // don't need to check again, userspace may call multiple times return true; } if (ksu_late_loaded) { return false; } // stop hook first! stop_input_hook(); pr_info("volumedown_pressed_count: %d\n", volumedown_pressed_count); if (is_volumedown_enough(volumedown_pressed_count)) { // pressed over 3 times pr_info("KEY_VOLUMEDOWN pressed max times, safe mode detected!\n"); safe_mode = true; return true; } return false; } static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs) { struct pt_regs *real_regs = PT_REAL_REGS(regs); const char __user **filename_user = (const char **)&PT_REGS_PARM1(real_regs); const char __user *const __user *__argv = (const char __user *const __user *)PT_REGS_PARM2(real_regs); struct user_arg_ptr argv = { .ptr.native = __argv }; struct filename filename_in, *filename_p; char path[32]; long ret; unsigned long addr; const char __user *fn; if (!filename_user) return 0; addr = untagged_addr((unsigned long)*filename_user); fn = (const char __user *)addr; memset(path, 0, sizeof(path)); ret = strncpy_from_user_nofault(path, fn, 32); if (ret < 0 && try_set_access_flag(addr)) { ret = strncpy_from_user_nofault(path, fn, 32); } if (ret < 0) { pr_err("Access filename failed for execve_handler_pre\n"); return 0; } filename_in.name = path; filename_p = &filename_in; return ksu_handle_execveat_ksud(AT_FDCWD, &filename_p, &argv, NULL, NULL); } static int sys_read_handler_pre(struct kprobe *p, struct pt_regs *regs) { struct pt_regs *real_regs = PT_REAL_REGS(regs); unsigned int fd = PT_REGS_PARM1(real_regs); ksu_handle_sys_read(fd); return 0; } static int sys_fstat_handler_pre(struct kretprobe_instance *p, struct pt_regs *regs) { struct pt_regs *real_regs = PT_REAL_REGS(regs); unsigned int fd = PT_REGS_PARM1(real_regs); void *statbuf = PT_REGS_PARM2(real_regs); *(void **)&p->data = NULL; struct file *file = fget(fd); if (!file) return 1; if (is_init_rc(file)) { pr_info("stat init.rc"); fput(file); *(void **)&p->data = statbuf; return 0; } fput(file); return 1; } static int sys_fstat_handler_post(struct kretprobe_instance *p, struct pt_regs *regs) { void __user *statbuf = *(void **)&p->data; if (statbuf) { void __user *st_size_ptr = statbuf + offsetof(struct stat, st_size); long size, new_size; if (!copy_from_user_nofault(&size, st_size_ptr, sizeof(long))) { new_size = size + ksu_rc_len; pr_info("adding ksu_rc_len: %ld -> %ld", size, new_size); if (!copy_to_user_nofault(st_size_ptr, &new_size, sizeof(long))) { pr_info("added ksu_rc_len"); } else { pr_err("add ksu_rc_len failed: statbuf 0x%lx", (unsigned long)st_size_ptr); } } else { pr_err("read statbuf 0x%lx failed", (unsigned long)st_size_ptr); } } return 0; } static int input_handle_event_handler_pre(struct kprobe *p, struct pt_regs *regs) { unsigned int *type = (unsigned int *)&PT_REGS_PARM2(regs); unsigned int *code = (unsigned int *)&PT_REGS_PARM3(regs); int *value = (int *)&PT_REGS_CCALL_PARM4(regs); return ksu_handle_input_handle_event(type, code, value); } static struct kprobe execve_kp = { .symbol_name = SYS_EXECVE_SYMBOL, .pre_handler = sys_execve_handler_pre, }; static struct kprobe sys_read_kp = { .symbol_name = SYS_READ_SYMBOL, .pre_handler = sys_read_handler_pre, }; static struct kretprobe sys_fstat_kp = { .kp.symbol_name = SYS_FSTAT_SYMBOL, .entry_handler = sys_fstat_handler_pre, .handler = sys_fstat_handler_post, .data_size = sizeof(void *), }; static struct kprobe input_event_kp = { .symbol_name = "input_event", .pre_handler = input_handle_event_handler_pre, }; static void do_stop_init_rc_hook(struct work_struct *work) { unregister_kprobe(&sys_read_kp); unregister_kretprobe(&sys_fstat_kp); } static void do_stop_execve_hook(struct work_struct *work) { unregister_kprobe(&execve_kp); } static void do_stop_input_hook(struct work_struct *work) { unregister_kprobe(&input_event_kp); } static void stop_init_rc_hook() { bool ret = schedule_work(&stop_init_rc_hook_work); pr_info("unregister init_rc_hook kprobe: %d!\n", ret); } static void stop_execve_hook() { bool ret = schedule_work(&stop_execve_hook_work); pr_info("unregister execve kprobe: %d!\n", ret); } static void stop_input_hook() { static bool input_hook_stopped = false; if (input_hook_stopped) { return; } input_hook_stopped = true; bool ret = schedule_work(&stop_input_hook_work); pr_info("unregister input kprobe: %d!\n", ret); } // ksud: module support void ksu_ksud_init() { int ret; ret = register_kprobe(&execve_kp); pr_info("ksud: execve_kp: %d\n", ret); ret = register_kprobe(&sys_read_kp); pr_info("ksud: sys_read_kp: %d\n", ret); ret = register_kretprobe(&sys_fstat_kp); pr_info("ksud: sys_fstat_kp: %d\n", ret); ret = register_kprobe(&input_event_kp); pr_info("ksud: input_event_kp: %d\n", ret); INIT_WORK(&stop_init_rc_hook_work, do_stop_init_rc_hook); INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook); INIT_WORK(&stop_input_hook_work, do_stop_input_hook); } void ksu_ksud_exit() { unregister_kprobe(&execve_kp); // this should be done before unregister sys_read_kp // unregister_kprobe(&sys_read_kp); unregister_kprobe(&input_event_kp); } ================================================ FILE: kernel/ksud.h ================================================ #ifndef __KSU_H_KSUD #define __KSU_H_KSUD #include #define KSUD_PATH "/data/adb/ksud" void ksu_ksud_init(); void ksu_ksud_exit(); void on_post_fs_data(void); void on_module_mounted(void); void on_boot_completed(void); bool ksu_is_safe_mode(void); int nuke_ext4_sysfs(const char *mnt); extern u32 ksu_file_sid; extern bool ksu_module_mounted; extern bool ksu_boot_completed; #endif ================================================ FILE: kernel/manager.h ================================================ #ifndef __KSU_H_KSU_MANAGER #define __KSU_H_KSU_MANAGER #include #include #include "allowlist.h" #define KSU_INVALID_APPID -1 extern uid_t ksu_manager_appid; // DO NOT DIRECT USE static inline bool ksu_is_manager_appid_valid() { return ksu_manager_appid != KSU_INVALID_APPID; } static inline bool is_manager() { return unlikely(ksu_manager_appid == current_uid().val % PER_USER_RANGE); } static inline bool is_uid_manager(uid_t uid) { return unlikely(ksu_manager_appid == uid % PER_USER_RANGE); } static inline uid_t ksu_get_manager_appid() { return ksu_manager_appid; } static inline void ksu_set_manager_appid(uid_t appid) { ksu_manager_appid = appid; } static inline void ksu_invalidate_manager_uid() { ksu_manager_appid = KSU_INVALID_APPID; } int ksu_observer_init(void); #endif ================================================ FILE: kernel/pkg_observer.c ================================================ // SPDX-License-Identifier: GPL-2.0 #include #include #include #include #include #include #include #include "klog.h" // IWYU pragma: keep #include "throne_tracker.h" #define MASK_SYSTEM (FS_CREATE | FS_MOVE | FS_EVENT_ON_CHILD) struct watch_dir { const char *path; u32 mask; struct path kpath; struct inode *inode; struct fsnotify_mark *mark; }; static struct fsnotify_group *g; static int ksu_handle_inode_event(struct fsnotify_mark *mark, u32 mask, struct inode *inode, struct inode *dir, const struct qstr *file_name, u32 cookie) { if (!file_name) return 0; if (mask & FS_ISDIR) return 0; if (file_name->len == 13 && !memcmp(file_name->name, "packages.list", 13)) { pr_info("packages.list detected: %d\n", mask); track_throne(false); } return 0; } static const struct fsnotify_ops ksu_ops = { .handle_inode_event = ksu_handle_inode_event, }; static int add_mark_on_inode(struct inode *inode, u32 mask, struct fsnotify_mark **out) { struct fsnotify_mark *m; m = kzalloc(sizeof(*m), GFP_KERNEL); if (!m) return -ENOMEM; fsnotify_init_mark(m, g); m->mask = mask; if (fsnotify_add_inode_mark(m, inode, 0)) { fsnotify_put_mark(m); return -EINVAL; } *out = m; return 0; } static int watch_one_dir(struct watch_dir *wd) { int ret = kern_path(wd->path, LOOKUP_FOLLOW, &wd->kpath); if (ret) { pr_info("path not ready: %s (%d)\n", wd->path, ret); return ret; } wd->inode = d_inode(wd->kpath.dentry); ihold(wd->inode); ret = add_mark_on_inode(wd->inode, wd->mask, &wd->mark); if (ret) { pr_err("Add mark failed for %s (%d)\n", wd->path, ret); path_put(&wd->kpath); iput(wd->inode); wd->inode = NULL; return ret; } pr_info("watching %s\n", wd->path); return 0; } static void unwatch_one_dir(struct watch_dir *wd) { if (wd->mark) { fsnotify_destroy_mark(wd->mark, g); fsnotify_put_mark(wd->mark); wd->mark = NULL; } if (wd->inode) { iput(wd->inode); wd->inode = NULL; } if (wd->kpath.dentry) { path_put(&wd->kpath); memset(&wd->kpath, 0, sizeof(wd->kpath)); } } static struct watch_dir g_watch = { .path = "/data/system", .mask = MASK_SYSTEM }; int ksu_observer_init(void) { int ret = 0; #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 0, 0) g = fsnotify_alloc_group(&ksu_ops, 0); #else g = fsnotify_alloc_group(&ksu_ops); #endif if (IS_ERR(g)) return PTR_ERR(g); ret = watch_one_dir(&g_watch); pr_info("observer init done\n"); return 0; } void ksu_observer_exit(void) { unwatch_one_dir(&g_watch); fsnotify_put_group(g); pr_info("observer exit done\n"); } ================================================ FILE: kernel/seccomp_cache.c ================================================ #include #include #include #include #include #include #include #include "klog.h" // IWYU pragma: keep #include "seccomp_cache.h" struct action_cache { DECLARE_BITMAP(allow_native, SECCOMP_ARCH_NATIVE_NR); #ifdef SECCOMP_ARCH_COMPAT DECLARE_BITMAP(allow_compat, SECCOMP_ARCH_COMPAT_NR); #endif }; struct seccomp_filter { refcount_t refs; refcount_t users; bool log; #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) bool wait_killable_recv; #endif struct action_cache cache; struct seccomp_filter *prev; struct bpf_prog *prog; struct notification *notif; struct mutex notify_lock; wait_queue_head_t wqh; }; void ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr) { if (!filter) { return; } if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) { clear_bit(nr, filter->cache.allow_native); } #ifdef SECCOMP_ARCH_COMPAT if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) { clear_bit(nr, filter->cache.allow_compat); } #endif } void ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr) { if (!filter) { return; } if (nr >= 0 && nr < SECCOMP_ARCH_NATIVE_NR) { set_bit(nr, filter->cache.allow_native); } #ifdef SECCOMP_ARCH_COMPAT if (nr >= 0 && nr < SECCOMP_ARCH_COMPAT_NR) { set_bit(nr, filter->cache.allow_compat); } #endif } ================================================ FILE: kernel/seccomp_cache.h ================================================ #ifndef __KSU_H_KERNEL_COMPAT #define __KSU_H_KERNEL_COMPAT #include #include extern void ksu_seccomp_clear_cache(struct seccomp_filter *filter, int nr); extern void ksu_seccomp_allow_cache(struct seccomp_filter *filter, int nr); #endif ================================================ FILE: kernel/selinux/rules.c ================================================ #include "linux/rcupdate.h" #include "security.h" #include #include #include #include #include #include #include "../klog.h" // IWYU pragma: keep #include "selinux.h" #include "sepolicy.h" #include "ss/services.h" #include "linux/lsm_audit.h" // IWYU pragma: keep #include "xfrm.h" #define SELINUX_POLICY_INSTEAD_SELINUX_SS #define ALL NULL #if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) extern int avc_ss_reset(u32 seqno); #else extern int avc_ss_reset(struct selinux_avc *avc, u32 seqno); #endif // reset avc cache table, otherwise the new rules will not take effect if already denied static void reset_avc_cache() { #if (LINUX_VERSION_CODE >= KERNEL_VERSION(6, 4, 0)) avc_ss_reset(0); selnl_notify_policyload(0); selinux_status_update_policyload(0); #else struct selinux_avc *avc = selinux_state.avc; avc_ss_reset(avc, 0); selnl_notify_policyload(0); selinux_status_update_policyload(&selinux_state, 0); #endif selinux_xfrm_notify_policyload(); } void apply_kernelsu_rules() { struct selinux_policy *pol, *old_pol = selinux_state.policy; struct policydb *db; if (!getenforce()) { pr_info("SELinux permissive or disabled, apply rules!\n"); } mutex_lock(&selinux_state.policy_mutex); pol = ksu_dup_sepolicy(rcu_dereference_protected( old_pol, lockdep_is_held(&selinux_state.policy_mutex))); if (!pol) { pr_err("failed to dup selinux_policy\n"); goto out_unlock; } db = &pol->policydb; ksu_permissive(db, KERNEL_SU_DOMAIN); ksu_typeattribute(db, KERNEL_SU_DOMAIN, "mlstrustedsubject"); ksu_typeattribute(db, KERNEL_SU_DOMAIN, "netdomain"); ksu_typeattribute(db, KERNEL_SU_DOMAIN, "bluetoothdomain"); // Create unconstrained file type ksu_type(db, KERNEL_SU_FILE, "file_type"); ksu_typeattribute(db, KERNEL_SU_FILE, "mlstrustedobject"); ksu_allow(db, ALL, KERNEL_SU_FILE, ALL, ALL); // allow all! ksu_allow(db, KERNEL_SU_DOMAIN, ALL, ALL, ALL); // allow us do any ioctl if (db->policyvers >= POLICYDB_VERSION_XPERMS_IOCTL) { ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "blk_file", ALL); ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "fifo_file", ALL); ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "chr_file", ALL); ksu_allowxperm(db, KERNEL_SU_DOMAIN, ALL, "file", ALL); } // our ksud triggered by init ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL); // copied from Magisk rules // suRights ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "search"); ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "dir", "read"); ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "open"); ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "file", "read"); ksu_allow(db, "servicemanager", KERNEL_SU_DOMAIN, "process", "getattr"); ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "process", "sigchld"); // allowLog ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "dir", "search"); ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "read"); ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "open"); ksu_allow(db, "logd", KERNEL_SU_DOMAIN, "file", "getattr"); // dumpsys ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fd", "use"); ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "write"); ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "read"); ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "open"); ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "fifo_file", "getattr"); // bootctl ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "dir", "search"); ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "read"); ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "file", "open"); ksu_allow(db, "hwservicemanager", KERNEL_SU_DOMAIN, "process", "getattr"); // Allow all binder transactions ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL); // Allow system server kill su process ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "getpgid"); ksu_allow(db, "system_server", KERNEL_SU_DOMAIN, "process", "sigkill"); rcu_assign_pointer(selinux_state.policy, pol); synchronize_rcu(); ksu_destroy_sepolicy(old_pol); reset_avc_cache(); out_unlock: mutex_unlock(&selinux_state.policy_mutex); } #define KSU_SEPOLICY_MAX_BATCH_SIZE (8U * 1024U * 1024U) #define KSU_SEPOLICY_MAX_ARGS 5 #define CMD_NORMAL_PERM 1 #define CMD_XPERM 2 #define CMD_TYPE_STATE 3 #define CMD_TYPE 4 #define CMD_TYPE_ATTR 5 #define CMD_ATTR 6 #define CMD_TYPE_TRANSITION 7 #define CMD_TYPE_CHANGE 8 #define CMD_GENFSCON 9 #define SUBCMD_NORMAL_PERM_ALLOW 1 #define SUBCMD_NORMAL_PERM_DENY 2 #define SUBCMD_NORMAL_PERM_AUDITALLOW 3 #define SUBCMD_NORMAL_PERM_DONTAUDIT 4 #define SUBCMD_XPERM_ALLOW 1 #define SUBCMD_XPERM_AUDITALLOW 2 #define SUBCMD_XPERM_DONTAUDIT 3 #define SUBCMD_TYPE_STATE_PERMISSIVE 1 #define SUBCMD_TYPE_STATE_ENFORCE 2 #define SUBCMD_TYPE_CHANGE_CHANGE 1 #define SUBCMD_TYPE_CHANGE_MEMBER 2 struct sepol_data { u32 cmd; u32 subcmd; }; struct sepol_batch_cursor { const u8 *cur; const u8 *end; }; static size_t sepol_remaining(const struct sepol_batch_cursor *cursor) { return (size_t)(cursor->end - cursor->cur); } static int sepol_read_cmd_header(struct sepol_batch_cursor *cursor, struct sepol_data *header) { if (sepol_remaining(cursor) < sizeof(*header)) { return -EINVAL; } memcpy(header, cursor->cur, sizeof(*header)); cursor->cur += sizeof(*header); return 0; } static int sepol_read_string(struct sepol_batch_cursor *cursor, const char **out) { u32 len; const char *str; if (sepol_remaining(cursor) < sizeof(len)) { return -EINVAL; } memcpy(&len, cursor->cur, sizeof(len)); cursor->cur += sizeof(len); if (len >= sepol_remaining(cursor)) { return -EINVAL; } str = (const char *)cursor->cur; if (memchr(str, '\0', len) != NULL || str[len] != '\0') { return -EINVAL; } cursor->cur += len + 1; if (len == 0) { *out = ALL; return 0; } *out = str; return 0; } static int sepol_require_not_all(const char *value, const char *name) { if (value != ALL) { return 0; } pr_err("sepol: %s cannot be ALL.\n", name); return -EINVAL; } static int sepol_expected_argc(u32 cmd) { switch (cmd) { case CMD_NORMAL_PERM: return 4; case CMD_XPERM: return 5; case CMD_TYPE_STATE: return 1; case CMD_TYPE: case CMD_TYPE_ATTR: return 2; case CMD_ATTR: return 1; case CMD_TYPE_TRANSITION: return 5; case CMD_TYPE_CHANGE: return 4; case CMD_GENFSCON: return 3; default: return -EINVAL; } } static int apply_one_sepolicy_cmd(struct policydb *db, const struct sepol_data *header, const char **args) { bool success = false; int ret; switch (header->cmd) { case CMD_NORMAL_PERM: if (header->subcmd == SUBCMD_NORMAL_PERM_ALLOW) { success = ksu_allow(db, args[0], args[1], args[2], args[3]); } else if (header->subcmd == SUBCMD_NORMAL_PERM_DENY) { success = ksu_deny(db, args[0], args[1], args[2], args[3]); } else if (header->subcmd == SUBCMD_NORMAL_PERM_AUDITALLOW) { success = ksu_auditallow(db, args[0], args[1], args[2], args[3]); } else if (header->subcmd == SUBCMD_NORMAL_PERM_DONTAUDIT) { success = ksu_dontaudit(db, args[0], args[1], args[2], args[3]); } else { pr_err("sepol: unknown subcmd: %d\n", header->subcmd); } return success ? 0 : -EINVAL; case CMD_XPERM: ret = sepol_require_not_all(args[3], "operation"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[4], "perm_set"); if (ret < 0) { return ret; } if (header->subcmd == SUBCMD_XPERM_ALLOW) { success = ksu_allowxperm(db, args[0], args[1], args[2], args[4]); } else if (header->subcmd == SUBCMD_XPERM_AUDITALLOW) { success = ksu_auditallowxperm(db, args[0], args[1], args[2], args[4]); } else if (header->subcmd == SUBCMD_XPERM_DONTAUDIT) { success = ksu_dontauditxperm(db, args[0], args[1], args[2], args[4]); } else { pr_err("sepol: unknown subcmd: %d\n", header->subcmd); } return success ? 0 : -EINVAL; case CMD_TYPE_STATE: ret = sepol_require_not_all(args[0], "type"); if (ret < 0) { return ret; } if (header->subcmd == SUBCMD_TYPE_STATE_PERMISSIVE) { success = ksu_permissive(db, args[0]); } else if (header->subcmd == SUBCMD_TYPE_STATE_ENFORCE) { success = ksu_enforce(db, args[0]); } else { pr_err("sepol: unknown subcmd: %d\n", header->subcmd); } return success ? 0 : -EINVAL; case CMD_TYPE: case CMD_TYPE_ATTR: ret = sepol_require_not_all(args[0], "type"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[1], "attribute"); if (ret < 0) { return ret; } if (header->cmd == CMD_TYPE) { success = ksu_type(db, args[0], args[1]); } else { success = ksu_typeattribute(db, args[0], args[1]); } if (!success) { pr_err("sepol: %d failed.\n", header->cmd); return -EINVAL; } return 0; case CMD_ATTR: ret = sepol_require_not_all(args[0], "attribute"); if (ret < 0) { return ret; } if (!ksu_attribute(db, args[0])) { pr_err("sepol: %d failed.\n", header->cmd); return -EINVAL; } return 0; case CMD_TYPE_TRANSITION: { const char *object = ALL; ret = sepol_require_not_all(args[0], "src"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[1], "tgt"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[2], "cls"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[3], "default_type"); if (ret < 0) { return ret; } object = args[4]; success = ksu_type_transition(db, args[0], args[1], args[2], args[3], object); return success ? 0 : -EINVAL; } case CMD_TYPE_CHANGE: ret = sepol_require_not_all(args[0], "src"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[1], "tgt"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[2], "cls"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[3], "default_type"); if (ret < 0) { return ret; } if (header->subcmd == SUBCMD_TYPE_CHANGE_CHANGE) { success = ksu_type_change(db, args[0], args[1], args[2], args[3]); } else if (header->subcmd == SUBCMD_TYPE_CHANGE_MEMBER) { success = ksu_type_member(db, args[0], args[1], args[2], args[3]); } else { pr_err("sepol: unknown subcmd: %d\n", header->subcmd); } return success ? 0 : -EINVAL; case CMD_GENFSCON: ret = sepol_require_not_all(args[0], "name"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[1], "path"); if (ret < 0) { return ret; } ret = sepol_require_not_all(args[2], "context"); if (ret < 0) { return ret; } if (!ksu_genfscon(db, args[0], args[1], args[2])) { pr_err("sepol: %d failed.\n", header->cmd); return -EINVAL; } return 0; default: pr_err("sepol: unknown cmd: %d\n", header->cmd); return -EINVAL; } } int handle_sepolicy(void __user *user_data, u64 data_len) { struct selinux_policy *pol, *old_pol; struct policydb *db; struct sepol_batch_cursor cursor; u8 *payload; int ret; int success_cmd_count; u32 cmd_index; if (!user_data || !data_len) { return -EINVAL; } if (data_len > KSU_SEPOLICY_MAX_BATCH_SIZE) { return -E2BIG; } payload = kvmalloc((size_t)data_len, GFP_KERNEL); if (!payload) { return -ENOMEM; } if (copy_from_user(payload, user_data, (size_t)data_len)) { ret = -EFAULT; goto out_free; } if (!getenforce()) { pr_info("SELinux permissive or disabled when handle policy!\n"); } mutex_lock(&selinux_state.policy_mutex); old_pol = selinux_state.policy; pol = ksu_dup_sepolicy(rcu_dereference_protected( old_pol, lockdep_is_held(&selinux_state.policy_mutex))); if (!pol) { ret = -ENOMEM; goto out_unlock; } db = &pol->policydb; cursor.cur = payload; cursor.end = payload + (size_t)data_len; ret = 0; success_cmd_count = 0; cmd_index = 0; while (cursor.cur < cursor.end) { struct sepol_data header; const char *args[KSU_SEPOLICY_MAX_ARGS] = { 0 }; int expected_argc; u32 arg_index; ret = sepol_read_cmd_header(&cursor, &header); if (ret < 0) { pr_err("sepol: failed to read cmd header #%u.\n", cmd_index); goto out_drop_new_policy; } expected_argc = sepol_expected_argc(header.cmd); if (expected_argc < 0 || expected_argc > KSU_SEPOLICY_MAX_ARGS) { ret = -EINVAL; pr_err("sepol: invalid cmd header #%u.\n", cmd_index); goto out_drop_new_policy; } for (arg_index = 0; arg_index < (u32)expected_argc; arg_index++) { ret = sepol_read_string(&cursor, &args[arg_index]); if (ret < 0) { pr_err("sepol: failed to read cmd #%u arg #%u.\n", cmd_index, arg_index); goto out_drop_new_policy; } } ret = apply_one_sepolicy_cmd(db, &header, args); if (ret < 0) { pr_err("sepol: cmd #%u failed, cmd=%u subcmd=%u.\n", cmd_index, header.cmd, header.subcmd); } else { success_cmd_count++; } cmd_index++; } rcu_assign_pointer(selinux_state.policy, pol); synchronize_rcu(); ksu_destroy_sepolicy(old_pol); reset_avc_cache(); ret = success_cmd_count; goto out_unlock; out_drop_new_policy: ksu_destroy_sepolicy(pol); out_unlock: mutex_unlock(&selinux_state.policy_mutex); out_free: kvfree(payload); return ret; } ================================================ FILE: kernel/selinux/selinux.c ================================================ #include "selinux.h" #include "linux/cred.h" #include "linux/sched.h" #include "objsec.h" #include "linux/version.h" #include "../klog.h" // IWYU pragma: keep #include "../ksu.h" /* * Cached SID values for frequently checked contexts. * These are resolved once at init and used for fast u32 comparison * instead of expensive string operations on every check. * * A value of 0 means "no cached SID is available" for that context. * This covers both the initial "not yet cached" state and any case * where resolving the SID (e.g. via security_secctx_to_secid) failed. * In all such cases we intentionally fall back to the slower * string-based comparison path; this degrades performance only and * does not cause a functional failure. */ static u32 cached_su_sid __read_mostly = 0; static u32 cached_zygote_sid __read_mostly = 0; static u32 cached_init_sid __read_mostly = 0; u32 ksu_file_sid __read_mostly = 0; static int transive_to_domain(const char *domain, struct cred *cred) { u32 sid; int error; #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 18, 0) struct task_security_struct *tsec; #else struct cred_security_struct *tsec; #endif tsec = selinux_cred(cred); if (!tsec) { pr_err("tsec == NULL!\n"); return -1; } error = security_secctx_to_secid(domain, strlen(domain), &sid); if (error) { pr_info("security_secctx_to_secid %s -> sid: %d, error: %d\n", domain, sid, error); } if (!error) { tsec->sid = sid; tsec->create_sid = 0; tsec->keycreate_sid = 0; tsec->sockcreate_sid = 0; } return error; } void setup_selinux(const char *domain, struct cred *cred) { if (transive_to_domain(domain, cred)) { pr_err("transive domain failed.\n"); return; } } void setup_ksu_cred(void) { if (ksu_cred && transive_to_domain(KERNEL_SU_CONTEXT, ksu_cred)) { pr_err("setup ksu cred failed.\n"); } } void setenforce(bool enforce) { #ifdef CONFIG_SECURITY_SELINUX_DEVELOP selinux_state.enforcing = enforce; #endif } bool getenforce(void) { #ifdef CONFIG_SECURITY_SELINUX_DISABLE if (selinux_state.disabled) { return false; } #endif #ifdef CONFIG_SECURITY_SELINUX_DEVELOP return selinux_state.enforcing; #else return true; #endif } #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 14, 0) struct lsm_context { char *context; u32 len; }; static int __security_secid_to_secctx(u32 secid, struct lsm_context *cp) { return security_secid_to_secctx(secid, &cp->context, &cp->len); } static void __security_release_secctx(struct lsm_context *cp) { security_release_secctx(cp->context, cp->len); } #else #define __security_secid_to_secctx security_secid_to_secctx #define __security_release_secctx security_release_secctx #endif /* * Initialize cached SID values for frequently checked SELinux contexts. * Called once after SELinux policy is loaded (post-fs-data). * This eliminates expensive string comparisons in hot paths. */ void cache_sid(void) { int err; err = security_secctx_to_secid(KERNEL_SU_CONTEXT, strlen(KERNEL_SU_CONTEXT), &cached_su_sid); if (err) { pr_warn("Failed to cache kernel su domain SID: %d\n", err); cached_su_sid = 0; } else { pr_info("Cached su SID: %u\n", cached_su_sid); } err = security_secctx_to_secid(ZYGOTE_CONTEXT, strlen(ZYGOTE_CONTEXT), &cached_zygote_sid); if (err) { pr_warn("Failed to cache zygote SID: %d\n", err); cached_zygote_sid = 0; } else { pr_info("Cached zygote SID: %u\n", cached_zygote_sid); } err = security_secctx_to_secid(INIT_CONTEXT, strlen(INIT_CONTEXT), &cached_init_sid); if (err) { pr_warn("Failed to cache init SID: %d\n", err); cached_init_sid = 0; } else { pr_info("Cached init SID: %u\n", cached_init_sid); } err = security_secctx_to_secid(KSU_FILE_CONTEXT, strlen(KSU_FILE_CONTEXT), &ksu_file_sid); if (err) { pr_warn("Failed to cache ksu_file SID: %d\n", err); ksu_file_sid = 0; } else { pr_info("Cached ksu_file SID: %u\n", ksu_file_sid); } } /* * Fast path: compare task's SID directly against cached value. * Falls back to string comparison if cache is not initialized. */ static bool is_sid_match(const struct cred *cred, u32 cached_sid, const char *fallback_context) { if (!cred) { return false; } #if LINUX_VERSION_CODE < KERNEL_VERSION(6, 18, 0) const struct task_security_struct *tsec = selinux_cred(cred); #else const struct cred_security_struct *tsec = selinux_cred(cred); #endif if (!tsec) { return false; } // Fast path: use cached SID if available if (likely(cached_sid != 0)) { return tsec->sid == cached_sid; } // Slow path fallback: string comparison (only before cache is initialized) struct lsm_context ctx; bool result; if (__security_secid_to_secctx(tsec->sid, &ctx)) { return false; } result = strncmp(fallback_context, ctx.context, ctx.len) == 0; __security_release_secctx(&ctx); return result; } bool is_task_ksu_domain(const struct cred *cred) { return is_sid_match(cred, cached_su_sid, KERNEL_SU_CONTEXT); } bool is_ksu_domain(void) { return is_task_ksu_domain(current_cred()); } bool is_zygote(const struct cred *cred) { return is_sid_match(cred, cached_zygote_sid, ZYGOTE_CONTEXT); } bool is_init(const struct cred *cred) { return is_sid_match(cred, cached_init_sid, INIT_CONTEXT); } ================================================ FILE: kernel/selinux/selinux.h ================================================ #ifndef __KSU_H_SELINUX #define __KSU_H_SELINUX #include "linux/types.h" #include "linux/version.h" #include "linux/cred.h" // TODO: rename to "ksu" #define KERNEL_SU_DOMAIN "su" #define KERNEL_SU_FILE "ksu_file" #define KERNEL_SU_CONTEXT "u:r:" KERNEL_SU_DOMAIN ":s0" #define KSU_FILE_CONTEXT "u:object_r:" KERNEL_SU_FILE ":s0" #define ZYGOTE_CONTEXT "u:r:zygote:s0" #define INIT_CONTEXT "u:r:init:s0" void setup_selinux(const char *, struct cred *); void setenforce(bool); bool getenforce(); void cache_sid(void); bool is_task_ksu_domain(const struct cred *cred); bool is_ksu_domain(); bool is_zygote(const struct cred *cred); bool is_init(const struct cred *cred); void apply_kernelsu_rules(); int handle_sepolicy(void __user *user_data, u64 data_len); void setup_ksu_cred(); #endif ================================================ FILE: kernel/selinux/sepolicy.c ================================================ #include "ss/avtab.h" #include "ss/constraint.h" #include "ss/ebitmap.h" #include "ss/hashtab.h" #include "ss/policydb.h" #include "ss/services.h" #include #include #include #include #include "sepolicy.h" #include "../klog.h" // IWYU pragma: keep #include "ss/symtab.h" #define KSU_SUPPORT_ADD_TYPE ////////////////////////////////////////////////////// // Declaration ////////////////////////////////////////////////////// static struct avtab_node *get_avtab_node(struct policydb *db, struct avtab_key *key, struct avtab_extended_perms *xperms); static bool add_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *p, int effect, bool invert); static void add_rule_raw(struct policydb *db, struct type_datum *src, struct type_datum *tgt, struct class_datum *cls, struct perm_datum *perm, int effect, bool invert); static void add_xperm_rule_raw(struct policydb *db, struct type_datum *src, struct type_datum *tgt, struct class_datum *cls, uint16_t low, uint16_t high, int effect, bool invert); static bool add_xperm_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *range, int effect, bool invert); static bool add_type_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *d, int effect); static bool add_filename_trans(struct policydb *db, const char *s, const char *t, const char *c, const char *d, const char *o); static bool add_genfscon(struct policydb *db, const char *fs_name, const char *path, const char *context); static bool add_type(struct policydb *db, const char *type_name, bool attr); static bool set_type_state(struct policydb *db, const char *type_name, bool permissive); static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, struct type_datum *attr); static bool add_typeattribute(struct policydb *db, const char *type, const char *attr); ////////////////////////////////////////////////////// // Implementation ////////////////////////////////////////////////////// // Invert is adding rules for auditdeny; in other cases, invert is removing // rules #define strip_av(effect, invert) ((effect == AVTAB_AUDITDENY) == !invert) #define ksu_hash_for_each(node_ptr, n_slot, cur) \ int i; \ for (i = 0; i < n_slot; ++i) \ for (cur = node_ptr[i]; cur; cur = cur->next) // htable is a struct instead of pointer above 5.8.0: // https://elixir.bootlin.com/linux/v5.8-rc1/source/security/selinux/ss/symtab.h #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 8, 0) #define ksu_hashtab_for_each(htab, cur) \ ksu_hash_for_each(htab.htable, htab.size, cur) #else #define ksu_hashtab_for_each(htab, cur) \ ksu_hash_for_each(htab->htable, htab->size, cur) #endif // symtab_search is introduced on 5.9.0: // https://elixir.bootlin.com/linux/v5.9-rc1/source/security/selinux/ss/symtab.h #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 9, 0) #define symtab_search(s, name) hashtab_search((s)->table, name) #define symtab_insert(s, name, datum) hashtab_insert((s)->table, name, datum) #endif #define avtab_for_each(avtab, cur) \ ksu_hash_for_each(avtab.htable, avtab.nslot, cur); static struct avtab_node *get_avtab_node(struct policydb *db, struct avtab_key *key, struct avtab_extended_perms *xperms) { struct avtab_node *node; /* AVTAB_XPERMS entries are not necessarily unique */ if (key->specified & AVTAB_XPERMS) { bool match = false; node = avtab_search_node(&db->te_avtab, key); while (node) { if ((node->datum.u.xperms->specified == xperms->specified) && (node->datum.u.xperms->driver == xperms->driver)) { match = true; break; } node = avtab_search_node_next(node, key->specified); } if (!match) node = NULL; } else { node = avtab_search_node(&db->te_avtab, key); } if (!node) { struct avtab_datum avdatum = {}; /* * AUDITDENY, aka DONTAUDIT, are &= assigned, versus |= for * others. Initialize the data accordingly. */ if (key->specified & AVTAB_XPERMS) { avdatum.u.xperms = xperms; } else { avdatum.u.data = key->specified == AVTAB_AUDITDENY ? ~0U : 0U; } /* this is used to get the node - insertion is actually unique */ node = avtab_insert_nonunique(&db->te_avtab, key, &avdatum); int grow_size = sizeof(struct avtab_key); grow_size += sizeof(struct avtab_datum); if (key->specified & AVTAB_XPERMS) { grow_size += sizeof(u8); grow_size += sizeof(u8); grow_size += sizeof(u32) * ARRAY_SIZE(avdatum.u.xperms->perms.p); } db->len += grow_size; } return node; } static bool add_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *p, int effect, bool invert) { struct type_datum *src = NULL, *tgt = NULL; struct class_datum *cls = NULL; struct perm_datum *perm = NULL; if (s) { src = symtab_search(&db->p_types, s); if (src == NULL) { pr_info("source type %s does not exist\n", s); return false; } } if (t) { tgt = symtab_search(&db->p_types, t); if (tgt == NULL) { pr_info("target type %s does not exist\n", t); return false; } } if (c) { cls = symtab_search(&db->p_classes, c); if (cls == NULL) { pr_info("class %s does not exist\n", c); return false; } } if (p) { if (c == NULL) { pr_info("No class is specified, cannot add perm [%s] \n", p); return false; } perm = symtab_search(&cls->permissions, p); if (perm == NULL && cls->comdatum != NULL) { perm = symtab_search(&cls->comdatum->permissions, p); } if (perm == NULL) { pr_info("perm %s does not exist in class %s\n", p, c); return false; } } add_rule_raw(db, src, tgt, cls, perm, effect, invert); return true; } static void add_rule_raw(struct policydb *db, struct type_datum *src, struct type_datum *tgt, struct class_datum *cls, struct perm_datum *perm, int effect, bool invert) { if (src == NULL) { struct hashtab_node *node; if (strip_av(effect, invert)) { ksu_hashtab_for_each(db->p_types.table, node) { add_rule_raw(db, (struct type_datum *)node->datum, tgt, cls, perm, effect, invert); }; } else { ksu_hashtab_for_each(db->p_types.table, node) { struct type_datum *type = (struct type_datum *)(node->datum); if (type->attribute) { add_rule_raw(db, type, tgt, cls, perm, effect, invert); } }; } } else if (tgt == NULL) { struct hashtab_node *node; if (strip_av(effect, invert)) { ksu_hashtab_for_each(db->p_types.table, node) { add_rule_raw(db, src, (struct type_datum *)node->datum, cls, perm, effect, invert); }; } else { ksu_hashtab_for_each(db->p_types.table, node) { struct type_datum *type = (struct type_datum *)(node->datum); if (type->attribute) { add_rule_raw(db, src, type, cls, perm, effect, invert); } }; } } else if (cls == NULL) { struct hashtab_node *node; ksu_hashtab_for_each(db->p_classes.table, node) { add_rule_raw(db, src, tgt, (struct class_datum *)node->datum, perm, effect, invert); } } else { struct avtab_key key; key.source_type = src->value; key.target_type = tgt->value; key.target_class = cls->value; key.specified = effect; struct avtab_node *node = get_avtab_node(db, &key, NULL); if (invert) { if (perm) node->datum.u.data &= ~(1U << (perm->value - 1)); else node->datum.u.data = 0U; } else { if (perm) node->datum.u.data |= 1U << (perm->value - 1); else node->datum.u.data = ~0U; } } } #define ioctl_driver(x) (x >> 8 & 0xFF) #define ioctl_func(x) (x & 0xFF) #define xperm_test(x, p) (1 & (p[x >> 5] >> (x & 0x1f))) #define xperm_set(x, p) (p[x >> 5] |= (1 << (x & 0x1f))) #define xperm_clear(x, p) (p[x >> 5] &= ~(1 << (x & 0x1f))) static void add_xperm_rule_raw(struct policydb *db, struct type_datum *src, struct type_datum *tgt, struct class_datum *cls, uint16_t low, uint16_t high, int effect, bool invert) { if (src == NULL) { struct hashtab_node *node; ksu_hashtab_for_each(db->p_types.table, node) { struct type_datum *type = (struct type_datum *)(node->datum); if (type->attribute) { add_xperm_rule_raw(db, type, tgt, cls, low, high, effect, invert); } }; } else if (tgt == NULL) { struct hashtab_node *node; ksu_hashtab_for_each(db->p_types.table, node) { struct type_datum *type = (struct type_datum *)(node->datum); if (type->attribute) { add_xperm_rule_raw(db, src, type, cls, low, high, effect, invert); } }; } else if (cls == NULL) { struct hashtab_node *node; ksu_hashtab_for_each(db->p_classes.table, node) { add_xperm_rule_raw(db, src, tgt, (struct class_datum *)(node->datum), low, high, effect, invert); }; } else { struct avtab_key key; key.source_type = src->value; key.target_type = tgt->value; key.target_class = cls->value; key.specified = effect; struct avtab_datum *datum; struct avtab_node *node; struct avtab_extended_perms xperms; memset(&xperms, 0, sizeof(xperms)); if (ioctl_driver(low) != ioctl_driver(high)) { xperms.specified = AVTAB_XPERMS_IOCTLDRIVER; xperms.driver = 0; } else { xperms.specified = AVTAB_XPERMS_IOCTLFUNCTION; xperms.driver = ioctl_driver(low); } int i; if (xperms.specified == AVTAB_XPERMS_IOCTLDRIVER) { for (i = ioctl_driver(low); i <= ioctl_driver(high); ++i) { if (invert) xperm_clear(i, xperms.perms.p); else xperm_set(i, xperms.perms.p); } } else { for (i = ioctl_func(low); i <= ioctl_func(high); ++i) { if (invert) xperm_clear(i, xperms.perms.p); else xperm_set(i, xperms.perms.p); } } node = get_avtab_node(db, &key, &xperms); if (!node) { pr_warn("add_xperm_rule_raw cannot found node!\n"); return; } datum = &node->datum; if (datum->u.xperms == NULL) { datum->u.xperms = (struct avtab_extended_perms *)(kzalloc( sizeof(xperms), GFP_KERNEL)); if (!datum->u.xperms) { pr_err("alloc xperms failed\n"); return; } memcpy(datum->u.xperms, &xperms, sizeof(xperms)); } } } static bool add_xperm_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *range, int effect, bool invert) { struct type_datum *src = NULL, *tgt = NULL; struct class_datum *cls = NULL; if (s) { src = symtab_search(&db->p_types, s); if (src == NULL) { pr_info("source type %s does not exist\n", s); return false; } } if (t) { tgt = symtab_search(&db->p_types, t); if (tgt == NULL) { pr_info("target type %s does not exist\n", t); return false; } } if (c) { cls = symtab_search(&db->p_classes, c); if (cls == NULL) { pr_info("class %s does not exist\n", c); return false; } } u16 low, high; if (range) { if (strchr(range, '-')) { sscanf(range, "%hx-%hx", &low, &high); } else { sscanf(range, "%hx", &low); high = low; } } else { low = 0; high = 0xFFFF; } add_xperm_rule_raw(db, src, tgt, cls, low, high, effect, invert); return true; } static bool add_type_rule(struct policydb *db, const char *s, const char *t, const char *c, const char *d, int effect) { struct type_datum *src, *tgt, *def; struct class_datum *cls; src = symtab_search(&db->p_types, s); if (src == NULL) { pr_info("source type %s does not exist\n", s); return false; } tgt = symtab_search(&db->p_types, t); if (tgt == NULL) { pr_info("target type %s does not exist\n", t); return false; } cls = symtab_search(&db->p_classes, c); if (cls == NULL) { pr_info("class %s does not exist\n", c); return false; } def = symtab_search(&db->p_types, d); if (def == NULL) { pr_info("default type %s does not exist\n", d); return false; } struct avtab_key key; key.source_type = src->value; key.target_type = tgt->value; key.target_class = cls->value; key.specified = effect; struct avtab_node *node = get_avtab_node(db, &key, NULL); node->datum.u.data = def->value; return true; } // 5.9.0 : static inline int hashtab_insert(struct hashtab *h, void *key, void // *datum, struct hashtab_key_params key_params) 5.8.0: int // hashtab_insert(struct hashtab *h, void *k, void *d); #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 9, 0) static u32 filenametr_hash(const void *k) { const struct filename_trans_key *ft = k; unsigned long hash; unsigned int byte_num; unsigned char focus; hash = ft->ttype ^ ft->tclass; byte_num = 0; while ((focus = ft->name[byte_num++])) hash = partial_name_hash(focus, hash); return hash; } static int filenametr_cmp(const void *k1, const void *k2) { const struct filename_trans_key *ft1 = k1; const struct filename_trans_key *ft2 = k2; int v; v = ft1->ttype - ft2->ttype; if (v) return v; v = ft1->tclass - ft2->tclass; if (v) return v; return strcmp(ft1->name, ft2->name); } static const struct hashtab_key_params filenametr_key_params = { .hash = filenametr_hash, .cmp = filenametr_cmp, }; #endif static bool add_filename_trans(struct policydb *db, const char *s, const char *t, const char *c, const char *d, const char *o) { struct type_datum *src, *tgt, *def; struct class_datum *cls; src = symtab_search(&db->p_types, s); if (src == NULL) { pr_warn("source type %s does not exist\n", s); return false; } tgt = symtab_search(&db->p_types, t); if (tgt == NULL) { pr_warn("target type %s does not exist\n", t); return false; } cls = symtab_search(&db->p_classes, c); if (cls == NULL) { pr_warn("class %s does not exist\n", c); return false; } def = symtab_search(&db->p_types, d); if (def == NULL) { pr_warn("default type %s does not exist\n", d); return false; } struct filename_trans_key key; key.ttype = tgt->value; key.tclass = cls->value; key.name = (char *)o; struct filename_trans_datum *last = NULL; struct filename_trans_datum *trans = policydb_filenametr_search(db, &key); while (trans) { if (ebitmap_get_bit(&trans->stypes, src->value - 1)) { // Duplicate, overwrite existing data and return trans->otype = def->value; return true; } if (trans->otype == def->value) break; last = trans; trans = trans->next; } if (trans == NULL) { trans = (struct filename_trans_datum *)kcalloc(1, sizeof(*trans), GFP_KERNEL); struct filename_trans_key *new_key = (struct filename_trans_key *)kzalloc(sizeof(*new_key), GFP_KERNEL); *new_key = key; new_key->name = kstrdup(key.name, GFP_KERNEL); trans->next = last; trans->otype = def->value; hashtab_insert(&db->filename_trans, new_key, trans, filenametr_key_params); } db->compat_filename_trans_count++; return ebitmap_set_bit(&trans->stypes, src->value - 1, 1) == 0; } static bool add_genfscon(struct policydb *db, const char *fs_name, const char *path, const char *context) { return false; } // https://github.com/torvalds/linux/commit/590b9d576caec6b4c46bba49ed36223a399c3fc5#diff-cc9aa90e094e6e0f47bd7300db4f33cf4366b98b55d8753744f31eb69c691016R844-R845 #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) #define ksu_kvrealloc(p, new_size, _old_size) kvrealloc(p, new_size, GFP_KERNEL) // https://github.com/torvalds/linux/commit/de2860f4636256836450c6543be744a50118fc66#diff-fa19cdd9c3369d7f59aa2e8404628109408dbf8e1b568d1157a27328f75b8410R638-R652 #elif LINUX_VERSION_CODE >= KERNEL_VERSION(5, 15, 0) #define ksu_kvrealloc(p, new_size, old_size) \ kvrealloc(p, old_size, new_size, GFP_KERNEL) #else // https://cs.android.com/android/_/android/kernel/common/+/f5f3e54f811679761c33526e695bd296190faade // Some 5.10 kernel don't have this backport, so copy one. void *ksu_kvrealloc_compat(const void *p, size_t oldsize, size_t newsize, gfp_t flags) { void *newp; if (oldsize >= newsize) return (void *)p; newp = kvmalloc(newsize, flags); if (!newp) return NULL; memcpy(newp, p, oldsize); kvfree(p); return newp; } #define ksu_kvrealloc(p, new_size, old_size) \ ksu_kvrealloc_compat(p, old_size, new_size, GFP_KERNEL) #endif static bool add_type(struct policydb *db, const char *type_name, bool attr) { struct type_datum *type = symtab_search(&db->p_types, type_name); if (type) { pr_warn("Type %s already exists\n", type_name); return true; } u32 value = ++db->p_types.nprim; type = (struct type_datum *)kzalloc(sizeof(struct type_datum), GFP_KERNEL); if (!type) { pr_err("add_type: alloc type_datum failed.\n"); return false; } type->primary = 1; type->value = value; type->attribute = attr; char *key = kstrdup(type_name, GFP_KERNEL); if (!key) { pr_err("add_type: alloc key failed.\n"); return false; } if (symtab_insert(&db->p_types, key, type)) { pr_err("add_type: insert symtab failed.\n"); return false; } struct ebitmap *new_type_attr_map_array = ksu_kvrealloc(db->type_attr_map_array, value * sizeof(struct ebitmap), (value - 1) * sizeof(struct ebitmap)); if (!new_type_attr_map_array) { pr_err("add_type: alloc type_attr_map_array failed\n"); return false; } struct type_datum **new_type_val_to_struct = ksu_kvrealloc(db->type_val_to_struct, sizeof(*db->type_val_to_struct) * value, sizeof(*db->type_val_to_struct) * (value - 1)); if (!new_type_val_to_struct) { pr_err("add_type: alloc type_val_to_struct failed\n"); return false; } char **new_val_to_name_types = ksu_kvrealloc(db->sym_val_to_name[SYM_TYPES], sizeof(char *) * value, sizeof(char *) * (value - 1)); if (!new_val_to_name_types) { pr_err("add_type: alloc val_to_name failed\n"); return false; } db->type_attr_map_array = new_type_attr_map_array; ebitmap_init(&db->type_attr_map_array[value - 1]); ebitmap_set_bit(&db->type_attr_map_array[value - 1], value - 1, 1); db->type_val_to_struct = new_type_val_to_struct; db->type_val_to_struct[value - 1] = type; db->sym_val_to_name[SYM_TYPES] = new_val_to_name_types; db->sym_val_to_name[SYM_TYPES][value - 1] = key; int i; for (i = 0; i < db->p_roles.nprim; ++i) { ebitmap_set_bit(&db->role_val_to_struct[i]->types, value - 1, 1); } return true; } static bool set_type_state(struct policydb *db, const char *type_name, bool permissive) { struct type_datum *type; if (type_name == NULL) { struct hashtab_node *node; ksu_hashtab_for_each(db->p_types.table, node) { type = (struct type_datum *)(node->datum); if (ebitmap_set_bit(&db->permissive_map, type->value, permissive)) pr_info("Could not set bit in permissive map\n"); }; } else { type = (struct type_datum *)symtab_search(&db->p_types, type_name); if (type == NULL) { pr_info("type %s does not exist\n", type_name); return false; } if (ebitmap_set_bit(&db->permissive_map, type->value, permissive)) { pr_info("Could not set bit in permissive map\n"); return false; } } return true; } static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, struct type_datum *attr) { struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1]; ebitmap_set_bit(sattr, attr->value - 1, 1); struct hashtab_node *node; struct constraint_node *n; struct constraint_expr *e; ksu_hashtab_for_each(db->p_classes.table, node) { struct class_datum *cls = (struct class_datum *)(node->datum); for (n = cls->constraints; n; n = n->next) { for (e = n->expr; e; e = e->next) { if (e->expr_type == CEXPR_NAMES && ebitmap_get_bit(&e->type_names->types, attr->value - 1)) { ebitmap_set_bit(&e->names, type->value - 1, 1); } } } }; } static bool add_typeattribute(struct policydb *db, const char *type, const char *attr) { struct type_datum *type_d = symtab_search(&db->p_types, type); if (type_d == NULL) { pr_info("type %s does not exist\n", type); return false; } else if (type_d->attribute) { pr_info("type %s is an attribute\n", attr); return false; } struct type_datum *attr_d = symtab_search(&db->p_types, attr); if (attr_d == NULL) { pr_info("attribute %s does not exist\n", type); return false; } else if (!attr_d->attribute) { pr_info("type %s is not an attribute \n", attr); return false; } add_typeattribute_raw(db, type_d, attr_d); return true; } ////////////////////////////////////////////////////////////////////////// // Operation on types bool ksu_type(struct policydb *db, const char *name, const char *attr) { return add_type(db, name, false) && add_typeattribute(db, name, attr); } bool ksu_attribute(struct policydb *db, const char *name) { return add_type(db, name, true); } bool ksu_permissive(struct policydb *db, const char *type) { return set_type_state(db, type, true); } bool ksu_enforce(struct policydb *db, const char *type) { return set_type_state(db, type, false); } bool ksu_typeattribute(struct policydb *db, const char *type, const char *attr) { return add_typeattribute(db, type, attr); } bool ksu_exists(struct policydb *db, const char *type) { return symtab_search(&db->p_types, type) != NULL; } // Access vector rules bool ksu_allow(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm) { return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, false); } bool ksu_deny(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm) { return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, true); } bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm) { return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITALLOW, false); } bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm) { return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITDENY, true); } // Extended permissions access vector rules bool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range) { return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_ALLOWED, false); } bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range) { return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_AUDITALLOW, false); } bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range) { return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_DONTAUDIT, false); } // Type rules bool ksu_type_transition(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def, const char *obj) { if (obj) { return add_filename_trans(db, src, tgt, cls, def, obj); } else { return add_type_rule(db, src, tgt, cls, def, AVTAB_TRANSITION); } } bool ksu_type_change(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def) { return add_type_rule(db, src, tgt, cls, def, AVTAB_CHANGE); } bool ksu_type_member(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def) { return add_type_rule(db, src, tgt, cls, def, AVTAB_MEMBER); } // File system labeling bool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path, const char *ctx) { return add_genfscon(db, fs_name, path, ctx); } // https://github.com/torvalds/linux/commit/581646c3fb98494009671f6d347ea125bc0e663a #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0) #define CONST_IF_6_10 const #else #define CONST_IF_6_10 #endif // ======== begin copy ======== static int copy_hashtab_node(struct hashtab_node *new_node, CONST_IF_6_10 struct hashtab_node *old_node, void *data) { new_node->datum = old_node->datum; new_node->key = old_node->key; return 0; } static int destroy_hashtab_node(void *key, void *datum, void *data) { // just copied pointer, no need to free return 0; } static int shallow_copy_hashtab(struct hashtab *new_tab, struct hashtab *old_tab) { return hashtab_duplicate(new_tab, old_tab, copy_hashtab_node, destroy_hashtab_node, NULL); } // ======== class_datum ======== static int copy_class_datum_partially_callback(struct hashtab_node *new_node, CONST_IF_6_10 struct hashtab_node *old_node, void *data) { struct policydb *db = data; struct class_datum *cls = old_node->datum, *new_cls; struct constraint_node *oldn, *n, *nprev = NULL; struct constraint_expr *olde, *e, *eprev; new_node->key = old_node->key; new_cls = kmemdup(cls, sizeof(struct class_datum), GFP_KERNEL); if (!new_cls) return -ENOMEM; new_node->datum = new_cls; new_cls->constraints = NULL; for (oldn = cls->constraints; oldn; oldn = oldn->next) { n = kmemdup(oldn, sizeof(struct constraint_node), GFP_KERNEL); if (!n) goto out_nomem; if (nprev) { nprev->next = n; } else { new_cls->constraints = n; } eprev = NULL; n->expr = NULL; for (olde = oldn->expr; olde; olde = olde->next) { e = kmemdup(olde, sizeof(struct constraint_expr), GFP_KERNEL); if (!e) { goto out_nomem; } if (eprev) { eprev->next = e; } else { n->expr = e; } if (olde->expr_type == CEXPR_NAMES) { if (ebitmap_cpy(&e->names, &olde->names) < 0) { goto out_nomem; } } eprev = e; } nprev = n; } db->class_val_to_struct[new_cls->value - 1] = new_cls; return 0; out_nomem: return -ENOMEM; } static int destroy_class_datum_partially_callback(void *key, void *datum, void *data) { struct class_datum *cls = datum; struct constraint_node *n, *nprev; struct constraint_expr *e, *eprev; if (cls) { for (n = cls->constraints; n;) { for (e = n->expr; e;) { if (e->expr_type == CEXPR_NAMES) { ebitmap_destroy(&e->names); } eprev = e; e = e->next; kfree(eprev); } nprev = n; n = n->next; kfree(nprev); } } kfree(cls); return 0; } static void free_class_datum_partially(struct policydb *db) { if (db->class_val_to_struct) { kfree(db->class_val_to_struct); } if (db->p_classes.table.htable) { hashtab_map(&db->p_classes.table, destroy_class_datum_partially_callback, NULL); hashtab_destroy(&db->p_classes.table); } } static int copy_class_datum_partially(struct policydb *new_db, struct policydb *old_db) { int ret; u32 n = new_db->symtab[SYM_CLASSES].nprim; struct class_datum **new_class_val_to_struct; new_db->class_val_to_struct = NULL; memset(&new_db->p_classes.table, 0, sizeof(new_db->p_classes.table)); new_class_val_to_struct = kcalloc(n, sizeof(struct class_datum *), GFP_KERNEL); if (!new_class_val_to_struct) { ret = -ENOMEM; goto exit; } new_db->class_val_to_struct = new_class_val_to_struct; ret = hashtab_duplicate(&new_db->p_classes.table, &old_db->p_classes.table, copy_class_datum_partially_callback, destroy_class_datum_partially_callback, new_db); if (ret) { goto exit; } return 0; exit: free_class_datum_partially(new_db); return ret; } // ======== avtab ======== static int copy_avtab(struct avtab *new_avtab, struct avtab *old_avtab) { int ret, i; struct avtab_node *n, *p; ret = avtab_alloc_dup(new_avtab, old_avtab); if (ret < 0) return ret; for (i = 0; i < old_avtab->nslot; i++) { n = old_avtab->htable[i]; while (n) { p = avtab_insert_nonunique(new_avtab, &n->key, &n->datum); if (!p) { ret = -ENOMEM; goto out_free; } n = n->next; } } return 0; out_free: avtab_destroy(new_avtab); return ret; } // ======== role_datum ======== static int copy_role_datum_partially_callback(struct hashtab_node *new_node, CONST_IF_6_10 struct hashtab_node *old_node, void *data) { int ret = 0; struct policydb *db = data; struct role_datum *role = old_node->datum, *new_role; new_role = kmemdup(role, sizeof(struct role_datum), GFP_KERNEL); if (!new_role) { ret = -ENOMEM; goto out; } new_node->datum = new_role; new_node->key = old_node->key; ret = ebitmap_cpy(&new_role->types, &role->types); if (ret) { goto out; } db->role_val_to_struct[role->value - 1] = new_role; out: return ret; } static int destroy_role_datum_partially_callback(void *key, void *datum, void *data) { struct role_datum *role = datum; if (role) { ebitmap_destroy(&role->types); kfree(role); } return 0; } static void free_role_datum_partially(struct policydb *db) { if (db->role_val_to_struct) { kfree(db->role_val_to_struct); } if (db->p_roles.table.htable) { hashtab_map(&db->p_roles.table, destroy_role_datum_partially_callback, NULL); hashtab_destroy(&db->p_roles.table); } } static int copy_role_datum_partially(struct policydb *new_db, struct policydb *old_db) { int ret; struct role_datum **new_role_val_to_struct; u32 n = old_db->p_roles.nprim; new_db->role_val_to_struct = NULL; memset(&new_db->p_roles.table, 0, sizeof(new_db->p_roles.table)); new_role_val_to_struct = kcalloc(n, sizeof(*new_db->role_val_to_struct), GFP_KERNEL); if (!new_role_val_to_struct) { ret = -ENOMEM; goto out_free; } new_db->role_val_to_struct = new_role_val_to_struct; ret = hashtab_duplicate(&new_db->p_roles.table, &old_db->p_roles.table, copy_role_datum_partially_callback, destroy_role_datum_partially_callback, new_db); if (ret) goto out_free; return 0; out_free: free_role_datum_partially(new_db); return ret; } // ======== type_datum ======== static void free_type_datum_partially(struct policydb *db) { u32 sz = db->p_types.nprim, i; if (db->type_attr_map_array) { for (i = 0; i < sz; i++) { ebitmap_destroy(&db->type_attr_map_array[i]); } kvfree(db->type_attr_map_array); } if (db->type_val_to_struct) { kvfree(db->type_val_to_struct); } if (db->sym_val_to_name[SYM_TYPES]) { kvfree(db->sym_val_to_name[SYM_TYPES]); } hashtab_destroy(&db->p_types.table); } static int copy_type_datum_partially(struct policydb *new_db, struct policydb *old_db) { int ret = -ENOMEM; u32 sz = new_db->p_types.nprim, i; struct ebitmap *new_type_attr_map_array; struct type_datum **new_type_val_to_struct; char **new_sym_val_to_name_types; new_db->type_attr_map_array = NULL; new_db->type_val_to_struct = NULL; new_db->sym_val_to_name[SYM_TYPES] = NULL; memset(&new_db->p_types.table, 0, sizeof(new_db->p_types.table)); // ======== type_attr_map_array ======== new_type_attr_map_array = kvcalloc(sz, sizeof(struct ebitmap), GFP_KERNEL); if (!new_type_attr_map_array) { goto out; } new_db->type_attr_map_array = new_type_attr_map_array; for (i = 0; i < sz; i++) { ret = ebitmap_cpy(&new_db->type_attr_map_array[i], &old_db->type_attr_map_array[i]); if (ret < 0) goto out; } // ======== type_val_to_struct ======== ret = -ENOMEM; new_type_val_to_struct = kvcalloc(sz, sizeof(*new_db->type_val_to_struct), GFP_KERNEL); if (!new_type_val_to_struct) { goto out; } new_db->type_val_to_struct = new_type_val_to_struct; memcpy(new_db->type_val_to_struct, old_db->type_val_to_struct, sz * sizeof(*new_db->type_val_to_struct)); // ======== sym_val_to_name[SYM_TYPES] ======== new_sym_val_to_name_types = kvcalloc(sz, sizeof(*new_db->sym_val_to_name[SYM_TYPES]), GFP_KERNEL); if (!new_sym_val_to_name_types) goto out; new_db->sym_val_to_name[SYM_TYPES] = new_sym_val_to_name_types; memcpy(new_db->sym_val_to_name[SYM_TYPES], old_db->sym_val_to_name[SYM_TYPES], sz * sizeof(*new_db->sym_val_to_name[SYM_TYPES])); // ======== p_types ======== ret = shallow_copy_hashtab(&new_db->p_types.table, &old_db->p_types.table); if (ret < 0) goto out; return 0; out: free_type_datum_partially(new_db); return ret; } // ======== permissive_map ======== static void free_permissive_map(struct policydb *db) { ebitmap_destroy(&db->permissive_map); } static int copy_permissive_map(struct policydb *new_db, struct policydb *old_db) { // On failure, the old ebitmap is cleaned. return ebitmap_cpy(&new_db->permissive_map, &old_db->permissive_map); } // ======== filename_trans ======== static void free_filename_trans(struct policydb *db) { hashtab_destroy(&db->filename_trans); } static int copy_filename_trans(struct policydb *new_db, struct policydb *old_db) { // On failure, the old hashtab is cleaned. return shallow_copy_hashtab(&new_db->filename_trans, &old_db->filename_trans); } // ======== sepolicy ======== void ksu_destroy_sepolicy(struct selinux_policy *pol) { if (!pol) return; struct policydb *db = &pol->policydb; free_class_datum_partially(db); avtab_destroy(&db->te_avtab); free_role_datum_partially(db); free_type_datum_partially(db); free_permissive_map(db); free_filename_trans(db); kfree(pol); } struct selinux_policy *ksu_dup_sepolicy(struct selinux_policy *old_pol) { int ret; struct selinux_policy *new_pol = kmemdup(old_pol, sizeof(*old_pol), GFP_KERNEL); if (!new_pol) { return NULL; } struct policydb *new_db = &new_pol->policydb, *old_db = &old_pol->policydb; ret = copy_class_datum_partially(new_db, old_db); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_class_datum_partially\n"); goto out; } ret = copy_avtab(&new_db->te_avtab, &old_db->te_avtab); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_avtab\n"); goto out; } ret = copy_role_datum_partially(new_db, old_db); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_role_datum_partially\n"); goto out; } ret = copy_type_datum_partially(new_db, old_db); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_type_datum_partially\n"); goto out; } ret = copy_permissive_map(new_db, old_db); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_permissive_map\n"); goto out; } ret = copy_filename_trans(new_db, old_db); if (ret < 0) { pr_err("ksu_dup_sepolicy: copy_filename_trans\n"); goto out; } return new_pol; out: kfree(new_pol); return NULL; } ================================================ FILE: kernel/selinux/sepolicy.h ================================================ #ifndef __KSU_H_SEPOLICY #define __KSU_H_SEPOLICY #include #include "ss/policydb.h" struct selinux_policy *ksu_dup_sepolicy(struct selinux_policy *old_pol); void ksu_destroy_sepolicy(struct selinux_policy *orig); // Operation on types bool ksu_type(struct policydb *db, const char *name, const char *attr); bool ksu_attribute(struct policydb *db, const char *name); bool ksu_permissive(struct policydb *db, const char *type); bool ksu_enforce(struct policydb *db, const char *type); bool ksu_typeattribute(struct policydb *db, const char *type, const char *attr); bool ksu_exists(struct policydb *db, const char *type); // Access vector rules bool ksu_allow(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm); bool ksu_deny(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm); bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm); bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *perm); // Extended permissions access vector rules bool ksu_allowxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range); bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range); bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *range); // Type rules bool ksu_type_transition(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def, const char *obj); bool ksu_type_change(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def); bool ksu_type_member(struct policydb *db, const char *src, const char *tgt, const char *cls, const char *def); // File system labeling bool ksu_genfscon(struct policydb *db, const char *fs_name, const char *path, const char *ctx); #endif ================================================ FILE: kernel/setuid_hook.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "allowlist.h" #include "setuid_hook.h" #include "klog.h" // IWYU pragma: keep #include "manager.h" #include "selinux/selinux.h" #include "seccomp_cache.h" #include "supercalls.h" #include "syscall_hook_manager.h" #include "kernel_umount.h" static void ksu_install_manager_fd_tw_func(struct callback_head *cb) { ksu_install_fd(); kfree(cb); } int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid) { // we rely on the fact that zygote always call setresuid(3) with same uids uid_t new_uid = ruid; uid_t old_uid = current_uid().val; pr_info("handle_setresuid from %d to %d\n", old_uid, new_uid); if (likely(ksu_is_manager_appid_valid()) && unlikely(ksu_get_manager_appid() == new_uid % PER_USER_RANGE)) { spin_lock_irq(¤t->sighand->siglock); ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot); ksu_set_task_tracepoint_flag(current); spin_unlock_irq(¤t->sighand->siglock); pr_info("install fd for manager: %d\n", new_uid); struct callback_head *cb = kzalloc(sizeof(*cb), GFP_ATOMIC); if (!cb) return 0; cb->func = ksu_install_manager_fd_tw_func; if (task_work_add(current, cb, TWA_RESUME)) { kfree(cb); pr_warn("install manager fd add task_work failed\n"); } return 0; } if (ksu_is_allow_uid_for_current(new_uid)) { if (current->seccomp.mode == SECCOMP_MODE_FILTER && current->seccomp.filter) { spin_lock_irq(¤t->sighand->siglock); ksu_seccomp_allow_cache(current->seccomp.filter, __NR_reboot); spin_unlock_irq(¤t->sighand->siglock); } ksu_set_task_tracepoint_flag(current); } else { ksu_clear_task_tracepoint_flag_if_needed(current); } // Handle kernel umount ksu_handle_umount(old_uid, new_uid); return 0; } void ksu_setuid_hook_init(void) { ksu_kernel_umount_init(); } void ksu_setuid_hook_exit(void) { pr_info("ksu_core_exit\n"); ksu_kernel_umount_exit(); } ================================================ FILE: kernel/setuid_hook.h ================================================ #ifndef __KSU_H_KSU_CORE #define __KSU_H_KSU_CORE #include #include void ksu_setuid_hook_init(void); void ksu_setuid_hook_exit(void); // Handler functions for hook_manager int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid); #endif ================================================ FILE: kernel/setup.sh ================================================ #!/bin/sh set -eu GKI_ROOT=$(pwd) display_usage() { echo "Usage: $0 [--cleanup | ]" echo " --cleanup: Cleans up previous modifications made by the script." echo " : Sets up or updates the KernelSU to specified tag or commit." echo " -h, --help: Displays this usage information." echo " (no args): Sets up or updates the KernelSU environment to the latest tagged version." } initialize_variables() { if test -d "$GKI_ROOT/common/drivers"; then DRIVER_DIR="$GKI_ROOT/common/drivers" elif test -d "$GKI_ROOT/drivers"; then DRIVER_DIR="$GKI_ROOT/drivers" else echo '[ERROR] "drivers/" directory not found.' exit 127 fi DRIVER_MAKEFILE=$DRIVER_DIR/Makefile DRIVER_KCONFIG=$DRIVER_DIR/Kconfig } # Reverts modifications made by this script perform_cleanup() { echo "[+] Cleaning up..." [ -L "$DRIVER_DIR/kernelsu" ] && rm "$DRIVER_DIR/kernelsu" && echo "[-] Symlink removed." grep -q "kernelsu" "$DRIVER_MAKEFILE" && sed -i '/kernelsu/d' "$DRIVER_MAKEFILE" && echo "[-] Makefile reverted." grep -q "drivers/kernelsu/Kconfig" "$DRIVER_KCONFIG" && sed -i '/drivers\/kernelsu\/Kconfig/d' "$DRIVER_KCONFIG" && echo "[-] Kconfig reverted." if [ -d "$GKI_ROOT/KernelSU" ]; then rm -rf "$GKI_ROOT/KernelSU" && echo "[-] KernelSU directory deleted." fi } # Sets up or update KernelSU environment setup_kernelsu() { echo "[+] Setting up KernelSU..." test -d "$GKI_ROOT/KernelSU" || git clone https://github.com/tiann/KernelSU && echo "[+] Repository cloned." cd "$GKI_ROOT/KernelSU" git stash && echo "[-] Stashed current changes." if [ "$(git status | grep -Po 'v\d+(\.\d+)*' | head -n1)" ]; then git checkout main && echo "[-] Switched to main branch." fi git pull && echo "[+] Repository updated." if [ -z "${1-}" ]; then git checkout "$(git describe --abbrev=0 --tags)" && echo "[-] Checked out latest tag." else git checkout "$1" && echo "[-] Checked out $1." || echo "[-] Checkout default branch" fi cd "$DRIVER_DIR" ln -sf "$(realpath --relative-to="$DRIVER_DIR" "$GKI_ROOT/KernelSU/kernel")" "kernelsu" && echo "[+] Symlink created." # Add entries in Makefile and Kconfig if not already existing grep -q "kernelsu" "$DRIVER_MAKEFILE" || printf "\nobj-\$(CONFIG_KSU) += kernelsu/\n" >> "$DRIVER_MAKEFILE" && echo "[+] Modified Makefile." grep -q "source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" || sed -i "/endmenu/i\source \"drivers/kernelsu/Kconfig\"" "$DRIVER_KCONFIG" && echo "[+] Modified Kconfig." echo '[+] Done.' } # Process command-line arguments if [ "$#" -eq 0 ]; then initialize_variables setup_kernelsu elif [ "$1" = "-h" ] || [ "$1" = "--help" ]; then display_usage elif [ "$1" = "--cleanup" ]; then initialize_variables perform_cleanup else initialize_variables setup_kernelsu "$@" fi ================================================ FILE: kernel/su_mount_ns.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "arch.h" #include "klog.h" // IWYU pragma: keep #include "ksu.h" #include "su_mount_ns.h" extern int path_mount(const char *dev_name, struct path *path, const char *type_page, unsigned long flags, void *data_page); #if defined(__aarch64__) extern long __arm64_sys_setns(const struct pt_regs *regs); #elif defined(__x86_64__) extern long __x64_sys_setns(const struct pt_regs *regs); #endif static long ksu_sys_setns(int fd, int flags) { struct pt_regs regs; memset(®s, 0, sizeof(regs)); PT_REGS_PARM1(®s) = fd; PT_REGS_PARM2(®s) = flags; #if defined(__aarch64__) return __arm64_sys_setns(®s); #elif defined(__x86_64__) return __x64_sys_setns(®s); #else #error "Unsupported arch" #endif } // global mode , need CAP_SYS_ADMIN and CAP_SYS_CHROOT to perform setns static void ksu_mnt_ns_global(void) { // save current working directory as absolute path before setns char *pwd_path = NULL; char *pwd_buf = kmalloc(PATH_MAX, GFP_KERNEL); if (!pwd_buf) { pr_warn("no mem for pwd buffer, skip restore pwd!!\n"); goto try_setns; } struct path saved_pwd; get_fs_pwd(current->fs, &saved_pwd); pwd_path = d_path(&saved_pwd, pwd_buf, PATH_MAX); path_put(&saved_pwd); if (IS_ERR(pwd_path)) { if (PTR_ERR(pwd_path) == -ENAMETOOLONG) { pr_warn("absolute pwd longer than: %d, skip restore pwd!!\n", PATH_MAX); } else { pr_warn("get absolute pwd failed: %ld\n", PTR_ERR(pwd_path)); } pwd_path = NULL; } try_setns: rcu_read_lock(); // &init_task is not init, but swapper/idle, which forks the init process // so we need find init process struct pid *pid_struct = find_pid_ns(1, &init_pid_ns); if (unlikely(!pid_struct)) { rcu_read_unlock(); pr_warn("failed to find pid_struct for PID 1\n"); goto out; } struct task_struct *pid1_task = get_pid_task(pid_struct, PIDTYPE_PID); rcu_read_unlock(); if (unlikely(!pid1_task)) { pr_warn("failed to get task_struct for PID 1\n"); goto out; } struct path ns_path; long ret = ns_get_path(&ns_path, pid1_task, &mntns_operations); put_task_struct(pid1_task); if (ret) { pr_warn("failed get path for init mount namespace: %ld\n", ret); goto out; } struct file *ns_file = dentry_open(&ns_path, O_RDONLY, ksu_cred); path_put(&ns_path); if (IS_ERR(ns_file)) { pr_warn("failed open file for init mount namespace: %ld\n", PTR_ERR(ns_file)); goto out; } int fd = get_unused_fd_flags(O_CLOEXEC); if (fd < 0) { pr_warn("failed to get an unused fd: %d\n", fd); fput(ns_file); goto out; } fd_install(fd, ns_file); ret = ksu_sys_setns(fd, CLONE_NEWNS); #if LINUX_VERSION_CODE < KERNEL_VERSION(5, 11, 0) ksys_close(fd); #else close_fd(fd); #endif if (ret) { pr_warn("call setns failed: %ld\n", ret); goto out; } // try to restore working directory using absolute path after setns if (pwd_path) { struct path new_pwd; int err = kern_path(pwd_path, 0, &new_pwd); if (!err) { set_fs_pwd(current->fs, &new_pwd); path_put(&new_pwd); } else { pr_warn("restore pwd failed: %d, path: %s\n", err, pwd_path); } } out: kfree(pwd_buf); } // individual mode , need CAP_SYS_ADMIN to perform unshare and remount static void ksu_mnt_ns_individual(void) { long ret = ksys_unshare(CLONE_NEWNS); if (ret) { pr_warn("call ksys_unshare failed: %ld\n", ret); return; } // make root mount private struct path root_path; get_fs_root(current->fs, &root_path); int pm_ret = path_mount(NULL, &root_path, NULL, MS_PRIVATE | MS_REC, NULL); path_put(&root_path); if (pm_ret < 0) { pr_err("failed to make root private, err: %d\n", pm_ret); } } static void ksu_setup_mount_ns_tw_func(struct callback_head *cb) { struct ksu_mns_tw *tw = container_of(cb, struct ksu_mns_tw, cb); const struct cred *old_cred = override_creds(ksu_cred); if (tw->ns_mode == KSU_NS_GLOBAL) { ksu_mnt_ns_global(); } else { ksu_mnt_ns_individual(); } revert_creds(old_cred); kfree(tw); } void setup_mount_ns(int32_t ns_mode) { // inherit mode if (ns_mode == KSU_NS_INHERITED) { // do nothing return; } if (ns_mode != KSU_NS_GLOBAL && ns_mode != KSU_NS_INDIVIDUAL) { pr_warn("pid: %d ,unknown mount namespace mode: %d\n", current->pid, ns_mode); return; } if (!ksu_cred) { pr_err("no ksu cred! skip mnt_ns magic for pid: %d.\n", current->pid); return; } struct ksu_mns_tw *tw = kzalloc(sizeof(*tw), GFP_ATOMIC); if (!tw) { pr_err("no mem for tw! skip mnt_ns magic for pid: %d.\n", current->pid); return; } tw->cb.func = ksu_setup_mount_ns_tw_func; tw->ns_mode = ns_mode; if (task_work_add(current, &tw->cb, TWA_RESUME)) { kfree(tw); pr_err("add task work failed! skip mnt_ns magic for pid: %d.\n", current->pid); } } ================================================ FILE: kernel/su_mount_ns.h ================================================ #ifndef __KSU_SU_MOUNT_NS_H #define __KSU_SU_MOUNT_NS_H #define KSU_NS_INHERITED 0 #define KSU_NS_GLOBAL 1 #define KSU_NS_INDIVIDUAL 2 struct ksu_mns_tw { struct callback_head cb; int32_t ns_mode; }; void setup_mount_ns(int32_t ns_mode); #endif ================================================ FILE: kernel/sucompat.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "allowlist.h" #include "feature.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" #include "sucompat.h" #include "app_profile.h" #include "util.h" #define SU_PATH "/system/bin/su" #define SH_PATH "/system/bin/sh" bool ksu_su_compat_enabled __read_mostly = true; static int su_compat_feature_get(u64 *value) { *value = ksu_su_compat_enabled ? 1 : 0; return 0; } static int su_compat_feature_set(u64 value) { bool enable = value != 0; ksu_su_compat_enabled = enable; pr_info("su_compat: set to %d\n", enable); return 0; } static const struct ksu_feature_handler su_compat_handler = { .feature_id = KSU_FEATURE_SU_COMPAT, .name = "su_compat", .get_handler = su_compat_feature_get, .set_handler = su_compat_feature_set, }; static void __user *userspace_stack_buffer(const void *d, size_t len) { // To avoid having to mmap a page in userspace, just write below the stack // pointer. char __user *p = (void __user *)current_user_stack_pointer() - len; return copy_to_user(p, d, len) ? NULL : p; } static char __user *sh_user_path(void) { static const char sh_path[] = "/system/bin/sh"; return userspace_stack_buffer(sh_path, sizeof(sh_path)); } static char __user *ksud_user_path(void) { static const char ksud_path[] = KSUD_PATH; return userspace_stack_buffer(ksud_path, sizeof(ksud_path)); } int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *__unused_flags) { const char su[] = SU_PATH; if (!ksu_is_allow_uid_for_current(current_uid().val)) { return 0; } char path[sizeof(su) + 1]; memset(path, 0, sizeof(path)); strncpy_from_user_nofault(path, *filename_user, sizeof(path)); if (unlikely(!memcmp(path, su, sizeof(su)))) { pr_info("faccessat su->sh!\n"); *filename_user = sh_user_path(); } return 0; } int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags) { // const char sh[] = SH_PATH; const char su[] = SU_PATH; if (!ksu_is_allow_uid_for_current(current_uid().val)) { return 0; } if (unlikely(!filename_user)) { return 0; } char path[sizeof(su) + 1]; memset(path, 0, sizeof(path)); strncpy_from_user_nofault(path, *filename_user, sizeof(path)); if (unlikely(!memcmp(path, su, sizeof(su)))) { pr_info("newfstatat su->sh!\n"); *filename_user = sh_user_path(); } return 0; } int ksu_handle_execve_sucompat(const char __user **filename_user, void *__never_use_argv, void *__never_use_envp, int *__never_use_flags) { const char su[] = SU_PATH; const char __user *fn; char path[sizeof(su) + 1]; long ret; unsigned long addr; if (unlikely(!filename_user)) return 0; if (!ksu_is_allow_uid_for_current(current_uid().val)) return 0; addr = untagged_addr((unsigned long)*filename_user); fn = (const char __user *)addr; memset(path, 0, sizeof(path)); ret = strncpy_from_user_nofault(path, fn, sizeof(path)); if (ret < 0 && try_set_access_flag(addr)) { ret = strncpy_from_user_nofault(path, fn, sizeof(path)); } if (ret < 0 && preempt_count()) { /* This is crazy, but we know what we are doing: * Temporarily exit atomic context to handle page faults, then restore it */ pr_info("Access filename failed, try rescue..\n"); preempt_enable_no_resched_notrace(); ret = strncpy_from_user(path, fn, sizeof(path)); preempt_disable_notrace(); } if (ret < 0) { pr_warn("Access filename when execve failed: %ld", ret); return 0; } if (likely(memcmp(path, su, sizeof(su)))) return 0; pr_info("sys_execve su found\n"); *filename_user = ksud_user_path(); escape_with_root_profile(); return 0; } // sucompat: permitted process can execute 'su' to gain root access. void ksu_sucompat_init() { if (ksu_register_feature_handler(&su_compat_handler)) { pr_err("Failed to register su_compat feature handler\n"); } } void ksu_sucompat_exit() { ksu_unregister_feature_handler(KSU_FEATURE_SU_COMPAT); } ================================================ FILE: kernel/sucompat.h ================================================ #ifndef __KSU_H_SUCOMPAT #define __KSU_H_SUCOMPAT #include extern bool ksu_su_compat_enabled; void ksu_sucompat_init(void); void ksu_sucompat_exit(void); // Handler functions exported for hook_manager int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *__unused_flags); int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); int ksu_handle_execve_sucompat(const char __user **filename_user, void *__never_use_argv, void *__never_use_envp, int *__never_use_flags); #endif ================================================ FILE: kernel/supercalls.c ================================================ #include #include #include #include #include #include #include #include #include #include #include #include #include #include "supercalls.h" #include "arch.h" #include "allowlist.h" #include "feature.h" #include "klog.h" // IWYU pragma: keep #include "ksu.h" #include "ksud.h" #include "kernel_umount.h" #include "manager.h" #include "selinux/selinux.h" #include "file_wrapper.h" #include "syscall_hook_manager.h" // Permission check functions bool only_manager(void) { return is_manager(); } bool only_root(void) { return current_uid().val == 0; } bool manager_or_root(void) { return current_uid().val == 0 || is_manager(); } bool always_allow(void) { return true; // No permission check } bool allowed_for_su(void) { bool is_allowed = is_manager() || ksu_is_allow_uid_for_current(current_uid().val); return is_allowed; } static int do_grant_root(void __user *arg) { // we already check uid above on allowed_for_su() pr_info("allow root for: %d\n", current_uid().val); escape_with_root_profile(); return 0; } static int do_get_info(void __user *arg) { struct ksu_get_info_cmd cmd = { .version = KERNEL_SU_VERSION, .flags = 0 }; #ifdef MODULE cmd.flags |= KSU_GET_INFO_FLAG_LKM; #endif if (is_manager()) { cmd.flags |= KSU_GET_INFO_FLAG_MANAGER; } if (ksu_late_loaded) { cmd.flags |= KSU_GET_INFO_FLAG_LATE_LOAD; } #ifdef EXPECTED_SIZE2 cmd.flags |= KSU_GET_INFO_FLAG_PR_BUILD; #endif cmd.features = KSU_FEATURE_MAX; if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("get_version: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_report_event(void __user *arg) { struct ksu_report_event_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { return -EFAULT; } switch (cmd.event) { case EVENT_POST_FS_DATA: { static bool post_fs_data_lock = false; if (!post_fs_data_lock) { post_fs_data_lock = true; if (ksu_late_loaded) { pr_info("post-fs-data skipped (late load)\n"); } else { pr_info("post-fs-data triggered\n"); on_post_fs_data(); } } break; } case EVENT_BOOT_COMPLETED: { static bool boot_complete_lock = false; if (!boot_complete_lock) { boot_complete_lock = true; if (ksu_late_loaded) { pr_info("boot_complete skipped (late load)\n"); } else { pr_info("boot_complete triggered\n"); on_boot_completed(); } } break; } case EVENT_MODULE_MOUNTED: { pr_info("module mounted!\n"); on_module_mounted(); break; } default: break; } return 0; } static int do_set_sepolicy(void __user *arg) { struct ksu_set_sepolicy_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { return -EFAULT; } return handle_sepolicy((void __user *)cmd.data, cmd.data_len); } static int do_check_safemode(void __user *arg) { struct ksu_check_safemode_cmd cmd; cmd.in_safe_mode = ksu_is_safe_mode(); if (cmd.in_safe_mode) { pr_warn("safemode enabled!\n"); } if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("check_safemode: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_new_get_allow_list_common(void __user *arg, bool allow) { struct ksu_new_get_allow_list_cmd cmd; int *arr = NULL; int err = 0; if (copy_from_user(&cmd, arg, sizeof(cmd))) { return -EFAULT; } if (cmd.count) { arr = kmalloc(sizeof(int) * cmd.count, GFP_KERNEL); if (!arr) { return -ENOMEM; } } bool success = ksu_get_allow_list(arr, cmd.count, &cmd.count, &cmd.total_count, allow); if (!success) { err = -EFAULT; goto out; } if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("new_get_allow_list: copy_to_user count failed\n"); err = -EFAULT; goto out; } if (cmd.count && copy_to_user(&((struct ksu_new_get_allow_list_cmd *)arg)->uids, arr, sizeof(int) * cmd.count)) { pr_err("new_get_allow_list: copy_to_user uids failed\n"); err = -EFAULT; } out: if (arr) { kfree(arr); } return err; } static int do_new_get_deny_list(void __user *arg) { return do_new_get_allow_list_common(arg, false); } static int do_new_get_allow_list(void __user *arg) { return do_new_get_allow_list_common(arg, true); } static int do_get_allow_list_common(void __user *arg, bool allow) { int *arr = NULL; int err = 0; u16 count; u32 out_count; static const u16 kSize = 128; arr = kmalloc(sizeof(int) * kSize, GFP_KERNEL); if (!arr) { return -ENOMEM; } bool success = ksu_get_allow_list(arr, kSize, &count, NULL, allow); if (!success) { err = -EFAULT; goto out; } out_count = count; if (copy_to_user(arg + offsetof(struct ksu_get_allow_list_cmd, count), &out_count, sizeof(u32))) { pr_err("get_allow_list: copy_to_user count failed\n"); err = -EFAULT; goto out; } if (copy_to_user(arg, arr, sizeof(u32) * count)) { pr_err("get_allow_list: copy_to_user uids failed\n"); err = -EFAULT; } out: if (arr) { kfree(arr); } return err; } static int do_get_deny_list(void __user *arg) { return do_get_allow_list_common(arg, false); } static int do_get_allow_list(void __user *arg) { return do_get_allow_list_common(arg, true); } static int do_uid_granted_root(void __user *arg) { struct ksu_uid_granted_root_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { return -EFAULT; } cmd.granted = ksu_is_allow_uid_for_current(cmd.uid); if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("uid_granted_root: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_uid_should_umount(void __user *arg) { struct ksu_uid_should_umount_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { return -EFAULT; } cmd.should_umount = ksu_uid_should_umount(cmd.uid); if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("uid_should_umount: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_get_manager_appid(void __user *arg) { struct ksu_get_manager_appid_cmd cmd; cmd.appid = ksu_get_manager_appid(); if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("get_manager_appid: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_get_app_profile(void __user *arg) { struct ksu_get_app_profile_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("get_app_profile: copy_from_user failed\n"); return -EFAULT; } if (!ksu_get_app_profile(&cmd.profile)) { return -ENOENT; } if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("get_app_profile: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_set_app_profile(void __user *arg) { struct ksu_set_app_profile_cmd cmd; int ret; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("set_app_profile: copy_from_user failed\n"); return -EFAULT; } ret = ksu_set_app_profile(&cmd.profile); if (!ret) { ksu_persistent_allow_list(); ksu_mark_running_process(); } return ret; } static int do_get_feature(void __user *arg) { struct ksu_get_feature_cmd cmd; bool supported; int ret; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("get_feature: copy_from_user failed\n"); return -EFAULT; } ret = ksu_get_feature(cmd.feature_id, &cmd.value, &supported); cmd.supported = supported ? 1 : 0; if (ret && supported) { pr_err("get_feature: failed for feature %u: %d\n", cmd.feature_id, ret); return ret; } if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("get_feature: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_set_feature(void __user *arg) { struct ksu_set_feature_cmd cmd; int ret; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("set_feature: copy_from_user failed\n"); return -EFAULT; } ret = ksu_set_feature(cmd.feature_id, cmd.value); if (ret) { pr_err("set_feature: failed for feature %u: %d\n", cmd.feature_id, ret); return ret; } return 0; } static int do_get_wrapper_fd(void __user *arg) { if (!ksu_file_sid) { return -EINVAL; } struct ksu_get_wrapper_fd_cmd cmd; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("get_wrapper_fd: copy_from_user failed\n"); return -EFAULT; } return ksu_install_file_wrapper(cmd.fd); } static int do_manage_mark(void __user *arg) { struct ksu_manage_mark_cmd cmd; int ret = 0; if (copy_from_user(&cmd, arg, sizeof(cmd))) { pr_err("manage_mark: copy_from_user failed\n"); return -EFAULT; } switch (cmd.operation) { case KSU_MARK_GET: { // Get task mark status ret = ksu_get_task_mark(cmd.pid); if (ret < 0) { pr_err("manage_mark: get failed for pid %d: %d\n", cmd.pid, ret); return ret; } cmd.result = (u32)ret; break; } case KSU_MARK_MARK: { if (cmd.pid == 0) { ksu_mark_all_process(); } else { ret = ksu_set_task_mark(cmd.pid, true); if (ret < 0) { pr_err("manage_mark: set_mark failed for pid %d: %d\n", cmd.pid, ret); return ret; } } break; } case KSU_MARK_UNMARK: { if (cmd.pid == 0) { ksu_unmark_all_process(); } else { ret = ksu_set_task_mark(cmd.pid, false); if (ret < 0) { pr_err("manage_mark: set_unmark failed for pid %d: %d\n", cmd.pid, ret); return ret; } } break; } case KSU_MARK_REFRESH: { ksu_mark_running_process(); pr_info("manage_mark: refreshed running processes\n"); break; } default: { pr_err("manage_mark: invalid operation %u\n", cmd.operation); return -EINVAL; } } if (copy_to_user(arg, &cmd, sizeof(cmd))) { pr_err("manage_mark: copy_to_user failed\n"); return -EFAULT; } return 0; } static int do_nuke_ext4_sysfs(void __user *arg) { struct ksu_nuke_ext4_sysfs_cmd cmd; char mnt[256]; long ret; if (copy_from_user(&cmd, arg, sizeof(cmd))) return -EFAULT; if (!cmd.arg) return -EINVAL; memset(mnt, 0, sizeof(mnt)); ret = strncpy_from_user(mnt, cmd.arg, sizeof(mnt)); if (ret < 0) { pr_err("nuke ext4 copy mnt failed: %ld\\n", ret); return -EFAULT; // 或者 return ret; } if (ret == sizeof(mnt)) { pr_err("nuke ext4 mnt path too long\\n"); return -ENAMETOOLONG; } pr_info("do_nuke_ext4_sysfs: %s\n", mnt); return nuke_ext4_sysfs(mnt); } struct list_head mount_list = LIST_HEAD_INIT(mount_list); DECLARE_RWSEM(mount_list_lock); static int add_try_umount(void __user *arg) { struct mount_entry *new_entry, *entry, *tmp; struct ksu_add_try_umount_cmd cmd; char buf[256] = { 0 }; if (copy_from_user(&cmd, arg, sizeof cmd)) return -EFAULT; switch (cmd.mode) { case KSU_UMOUNT_WIPE: { struct mount_entry *entry, *tmp; down_write(&mount_list_lock); list_for_each_entry_safe (entry, tmp, &mount_list, list) { pr_info("wipe_umount_list: removing entry: %s\n", entry->umountable); list_del(&entry->list); kfree(entry->umountable); kfree(entry); } up_write(&mount_list_lock); return 0; } case KSU_UMOUNT_ADD: { long len = strncpy_from_user(buf, (const char __user *)cmd.arg, 256); if (len <= 0) return -EFAULT; buf[sizeof(buf) - 1] = '\0'; new_entry = kzalloc(sizeof(*new_entry), GFP_KERNEL); if (!new_entry) return -ENOMEM; new_entry->umountable = kstrdup(buf, GFP_KERNEL); if (!new_entry->umountable) { kfree(new_entry); return -ENOMEM; } down_write(&mount_list_lock); // disallow dupes // if this gets too many, we can consider moving this whole task to a kthread list_for_each_entry (entry, &mount_list, list) { if (!strcmp(entry->umountable, buf)) { pr_info("cmd_add_try_umount: %s is already here!\n", buf); up_write(&mount_list_lock); kfree(new_entry->umountable); kfree(new_entry); return -EEXIST; } } // now check flags and add // this also serves as a null check if (cmd.flags) new_entry->flags = cmd.flags; else new_entry->flags = 0; // debug list_add(&new_entry->list, &mount_list); up_write(&mount_list_lock); pr_info("cmd_add_try_umount: %s added!\n", buf); return 0; } // this is just strcmp'd wipe anyway case KSU_UMOUNT_DEL: { long len = strncpy_from_user(buf, (const char __user *)cmd.arg, sizeof(buf) - 1); if (len <= 0) return -EFAULT; buf[sizeof(buf) - 1] = '\0'; down_write(&mount_list_lock); list_for_each_entry_safe (entry, tmp, &mount_list, list) { if (!strcmp(entry->umountable, buf)) { pr_info("cmd_add_try_umount: entry removed: %s\n", entry->umountable); list_del(&entry->list); kfree(entry->umountable); kfree(entry); } } up_write(&mount_list_lock); return 0; } default: { pr_err("cmd_add_try_umount: invalid operation %u\n", cmd.mode); return -EINVAL; } } // switch(cmd.mode) return 0; } // IOCTL handlers mapping table static const struct ksu_ioctl_cmd_map ksu_ioctl_handlers[] = { { .cmd = KSU_IOCTL_GRANT_ROOT, .name = "GRANT_ROOT", .handler = do_grant_root, .perm_check = allowed_for_su }, { .cmd = KSU_IOCTL_GET_INFO, .name = "GET_INFO", .handler = do_get_info, .perm_check = always_allow }, { .cmd = KSU_IOCTL_REPORT_EVENT, .name = "REPORT_EVENT", .handler = do_report_event, .perm_check = only_root }, { .cmd = KSU_IOCTL_SET_SEPOLICY, .name = "SET_SEPOLICY", .handler = do_set_sepolicy, .perm_check = only_root }, { .cmd = KSU_IOCTL_CHECK_SAFEMODE, .name = "CHECK_SAFEMODE", .handler = do_check_safemode, .perm_check = always_allow }, { .cmd = KSU_IOCTL_GET_ALLOW_LIST, .name = "GET_ALLOW_LIST", .handler = do_get_allow_list, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_DENY_LIST, .name = "GET_DENY_LIST", .handler = do_get_deny_list, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_NEW_GET_ALLOW_LIST, .name = "NEW_GET_ALLOW_LIST", .handler = do_new_get_allow_list, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_NEW_GET_DENY_LIST, .name = "NEW_GET_DENY_LIST", .handler = do_new_get_deny_list, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_UID_GRANTED_ROOT, .name = "UID_GRANTED_ROOT", .handler = do_uid_granted_root, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_UID_SHOULD_UMOUNT, .name = "UID_SHOULD_UMOUNT", .handler = do_uid_should_umount, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_MANAGER_APPID, .name = "GET_MANAGER_APPID", .handler = do_get_manager_appid, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_APP_PROFILE, .name = "GET_APP_PROFILE", .handler = do_get_app_profile, .perm_check = only_manager }, { .cmd = KSU_IOCTL_SET_APP_PROFILE, .name = "SET_APP_PROFILE", .handler = do_set_app_profile, .perm_check = only_manager }, { .cmd = KSU_IOCTL_GET_FEATURE, .name = "GET_FEATURE", .handler = do_get_feature, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_SET_FEATURE, .name = "SET_FEATURE", .handler = do_set_feature, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_GET_WRAPPER_FD, .name = "GET_WRAPPER_FD", .handler = do_get_wrapper_fd, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_MANAGE_MARK, .name = "MANAGE_MARK", .handler = do_manage_mark, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_NUKE_EXT4_SYSFS, .name = "NUKE_EXT4_SYSFS", .handler = do_nuke_ext4_sysfs, .perm_check = manager_or_root }, { .cmd = KSU_IOCTL_ADD_TRY_UMOUNT, .name = "ADD_TRY_UMOUNT", .handler = add_try_umount, .perm_check = manager_or_root }, { .cmd = 0, .name = NULL, .handler = NULL, .perm_check = NULL } // Sentinel }; struct ksu_install_fd_tw { struct callback_head cb; int __user *outp; }; static void ksu_install_fd_tw_func(struct callback_head *cb) { struct ksu_install_fd_tw *tw = container_of(cb, struct ksu_install_fd_tw, cb); int fd = ksu_install_fd(); pr_info("[%d] install ksu fd: %d\n", current->pid, fd); if (copy_to_user(tw->outp, &fd, sizeof(fd))) { pr_err("install ksu fd reply err\n"); #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) close_fd(fd); #else ksys_close(fd); #endif } kfree(tw); } static int reboot_handler_pre(struct kprobe *p, struct pt_regs *regs) { struct pt_regs *real_regs = PT_REAL_REGS(regs); int magic1 = (int)PT_REGS_PARM1(real_regs); int magic2 = (int)PT_REGS_PARM2(real_regs); unsigned long arg4; // Check if this is a request to install KSU fd if (magic1 == KSU_INSTALL_MAGIC1 && magic2 == KSU_INSTALL_MAGIC2) { struct ksu_install_fd_tw *tw; arg4 = (unsigned long)PT_REGS_SYSCALL_PARM4(real_regs); tw = kzalloc(sizeof(*tw), GFP_ATOMIC); if (!tw) return 0; tw->outp = (int __user *)arg4; tw->cb.func = ksu_install_fd_tw_func; if (task_work_add(current, &tw->cb, TWA_RESUME)) { kfree(tw); pr_warn("install fd add task_work failed\n"); } } return 0; } static struct kprobe reboot_kp = { .symbol_name = REBOOT_SYMBOL, .pre_handler = reboot_handler_pre, }; void ksu_supercalls_init(void) { int i; pr_info("KernelSU IOCTL Commands:\n"); for (i = 0; ksu_ioctl_handlers[i].handler; i++) { pr_info(" %-18s = 0x%08x\n", ksu_ioctl_handlers[i].name, ksu_ioctl_handlers[i].cmd); } int rc = register_kprobe(&reboot_kp); if (rc) { pr_err("reboot kprobe failed: %d\n", rc); } else { pr_info("reboot kprobe registered successfully\n"); } } void ksu_supercalls_exit(void) { unregister_kprobe(&reboot_kp); } // IOCTL dispatcher static long anon_ksu_ioctl(struct file *filp, unsigned int cmd, unsigned long arg) { void __user *argp = (void __user *)arg; int i; #ifdef CONFIG_KSU_DEBUG pr_info("ksu ioctl: cmd=0x%x from uid=%d\n", cmd, current_uid().val); #endif for (i = 0; ksu_ioctl_handlers[i].handler; i++) { if (cmd == ksu_ioctl_handlers[i].cmd) { // Check permission first if (ksu_ioctl_handlers[i].perm_check && !ksu_ioctl_handlers[i].perm_check()) { pr_warn("ksu ioctl: permission denied for cmd=0x%x uid=%d\n", cmd, current_uid().val); return -EPERM; } // Execute handler return ksu_ioctl_handlers[i].handler(argp); } } pr_warn("ksu ioctl: unsupported command 0x%x\n", cmd); return -ENOTTY; } // File release handler static int anon_ksu_release(struct inode *inode, struct file *filp) { pr_info("ksu fd released\n"); return 0; } // File operations structure static const struct file_operations anon_ksu_fops = { .owner = THIS_MODULE, .unlocked_ioctl = anon_ksu_ioctl, .compat_ioctl = anon_ksu_ioctl, .release = anon_ksu_release, }; // Install KSU fd to current process int ksu_install_fd(void) { struct file *filp; int fd; // Get unused fd fd = get_unused_fd_flags(O_CLOEXEC); if (fd < 0) { pr_err("ksu_install_fd: failed to get unused fd\n"); return fd; } // Create anonymous inode file filp = anon_inode_getfile("[ksu_driver]", &anon_ksu_fops, NULL, O_RDWR | O_CLOEXEC); if (IS_ERR(filp)) { pr_err("ksu_install_fd: failed to create anon inode file\n"); put_unused_fd(fd); return PTR_ERR(filp); } // Install fd fd_install(fd, filp); pr_info("ksu fd installed: %d for pid %d\n", fd, current->pid); return fd; } ================================================ FILE: kernel/supercalls.h ================================================ #ifndef __KSU_H_SUPERCALLS #define __KSU_H_SUPERCALLS #include #include #include "app_profile.h" // Magic numbers for reboot hook to install fd #define KSU_INSTALL_MAGIC1 0xDEADBEEF #define KSU_INSTALL_MAGIC2 0xCAFEBABE // Command structures for ioctl struct ksu_become_daemon_cmd { __u8 token[65]; // Input: daemon token (null-terminated) }; #define KSU_GET_INFO_FLAG_LKM (1U << 0) #define KSU_GET_INFO_FLAG_MANAGER (1U << 1) #define KSU_GET_INFO_FLAG_LATE_LOAD (1U << 2) #define KSU_GET_INFO_FLAG_PR_BUILD (1U << 3) struct ksu_get_info_cmd { __u32 version; // Output: KERNEL_SU_VERSION __u32 flags; // Output: KSU_GET_INFO_FLAG_* bits __u32 features; // Output: max feature ID supported }; struct ksu_report_event_cmd { __u32 event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc. }; struct ksu_set_sepolicy_cmd { __u64 data_len; // Input: bytes of serialized command payload __aligned_u64 data; // Input: pointer to serialized payload }; struct ksu_sepolicy_cmd_hdr { __u32 cmd; // Input: command type, CMD_* __u32 subcmd; // Input: command subtype }; // After each ksu_sepolicy_cmd_hdr, command arguments are encoded sequentially as: // [u32 len][len bytes][\0], where len excludes the trailing '\0'. // len == 0 represents ALL. // Argument count is derived from cmd: // CMD_NORMAL_PERM=4, CMD_XPERM=5, CMD_TYPE_STATE=1, CMD_TYPE=2, // CMD_TYPE_ATTR=2, CMD_ATTR=1, CMD_TYPE_TRANSITION=5, // CMD_TYPE_CHANGE=4, CMD_GENFSCON=3. struct ksu_check_safemode_cmd { __u8 in_safe_mode; // Output: true if in safe mode, false otherwise }; // deprecated struct ksu_get_allow_list_cmd { __u32 uids[128]; // Output: array of allowed/denied UIDs __u32 count; // Output: number of UIDs in array __u8 allow; // Input: true for allow list, false for deny list }; struct ksu_new_get_allow_list_cmd { __u16 count; // Input / Output: number of UIDs in array __u16 total_count; // Output: total number of UIDs in requested list __u32 uids[0]; // Output: array of allowed/denied UIDs }; struct ksu_uid_granted_root_cmd { __u32 uid; // Input: target UID to check __u8 granted; // Output: true if granted, false otherwise }; struct ksu_uid_should_umount_cmd { __u32 uid; // Input: target UID to check __u8 should_umount; // Output: true if should umount, false otherwise }; struct ksu_get_manager_appid_cmd { __u32 appid; // Output: manager app id }; struct ksu_get_app_profile_cmd { struct app_profile profile; // Input/Output: app profile structure }; struct ksu_set_app_profile_cmd { struct app_profile profile; // Input: app profile structure }; struct ksu_get_feature_cmd { __u32 feature_id; // Input: feature ID (enum ksu_feature_id) __u64 value; // Output: feature value/state __u8 supported; // Output: true if feature is supported, false otherwise }; struct ksu_set_feature_cmd { __u32 feature_id; // Input: feature ID (enum ksu_feature_id) __u64 value; // Input: feature value/state to set }; struct ksu_get_wrapper_fd_cmd { __u32 fd; // Input: userspace fd __u32 flags; // Input: flags of userspace fd }; struct ksu_manage_mark_cmd { __u32 operation; // Input: KSU_MARK_* __s32 pid; // Input: target pid (0 for all processes) __u32 result; // Output: for get operation - mark status or reg_count }; #define KSU_MARK_GET 1 #define KSU_MARK_MARK 2 #define KSU_MARK_UNMARK 3 #define KSU_MARK_REFRESH 4 struct ksu_nuke_ext4_sysfs_cmd { __aligned_u64 arg; // Input: mnt pointer }; struct ksu_add_try_umount_cmd { __aligned_u64 arg; // char ptr, this is the mountpoint __u32 flags; // this is the flag we use for it __u8 mode; // denotes what to do with it 0:wipe_list 1:add_to_list 2:delete_entry }; #define KSU_UMOUNT_WIPE 0 // ignore everything and wipe list #define KSU_UMOUNT_ADD 1 // add entry (path + flags) #define KSU_UMOUNT_DEL 2 // delete entry, strcmp // IOCTL command definitions #define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0) #define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0) #define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0) #define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ | _IOC_WRITE, 'K', 4, 0) #define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0) // deprecated #define KSU_IOCTL_GET_ALLOW_LIST _IOC(_IOC_READ | _IOC_WRITE, 'K', 6, 0) // deprecated #define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ | _IOC_WRITE, 'K', 7, 0) #define KSU_IOCTL_NEW_GET_ALLOW_LIST \ _IOWR('K', 6, struct ksu_new_get_allow_list_cmd) #define KSU_IOCTL_NEW_GET_DENY_LIST \ _IOWR('K', 7, struct ksu_new_get_allow_list_cmd) #define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ | _IOC_WRITE, 'K', 8, 0) #define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ | _IOC_WRITE, 'K', 9, 0) #define KSU_IOCTL_GET_MANAGER_APPID _IOC(_IOC_READ, 'K', 10, 0) #define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ | _IOC_WRITE, 'K', 11, 0) #define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0) #define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ | _IOC_WRITE, 'K', 13, 0) #define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0) #define KSU_IOCTL_GET_WRAPPER_FD _IOC(_IOC_WRITE, 'K', 15, 0) #define KSU_IOCTL_MANAGE_MARK _IOC(_IOC_READ | _IOC_WRITE, 'K', 16, 0) #define KSU_IOCTL_NUKE_EXT4_SYSFS _IOC(_IOC_WRITE, 'K', 17, 0) #define KSU_IOCTL_ADD_TRY_UMOUNT _IOC(_IOC_WRITE, 'K', 18, 0) // IOCTL handler types typedef int (*ksu_ioctl_handler_t)(void __user *arg); typedef bool (*ksu_perm_check_t)(void); // IOCTL command mapping struct ksu_ioctl_cmd_map { unsigned int cmd; const char *name; ksu_ioctl_handler_t handler; ksu_perm_check_t perm_check; // Permission check function }; // Install KSU fd to current process int ksu_install_fd(void); void ksu_supercalls_init(void); void ksu_supercalls_exit(void); #endif // __KSU_H_SUPERCALLS ================================================ FILE: kernel/syscall_hook_manager.c ================================================ #include "linux/compiler.h" #include "linux/cred.h" #include "linux/printk.h" #include "selinux/selinux.h" #include #include #include #include #include #include #include #include "allowlist.h" #include "arch.h" #include "klog.h" // IWYU pragma: keep #include "syscall_hook_manager.h" #include "sucompat.h" #include "setuid_hook.h" #include "selinux/selinux.h" #include "util.h" #include "ksud.h" // Tracepoint registration count management // == 1: just us // > 1: someone else is also using syscall tracepoint e.g. ftrace static int tracepoint_reg_count = 0; static DEFINE_SPINLOCK(tracepoint_reg_lock); void ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t) { unsigned long flags; spin_lock_irqsave(&tracepoint_reg_lock, flags); if (tracepoint_reg_count <= 1) { ksu_clear_task_tracepoint_flag(t); } spin_unlock_irqrestore(&tracepoint_reg_lock, flags); } // Process marking management static void handle_process_mark(bool mark) { struct task_struct *p, *t; read_lock(&tasklist_lock); for_each_process_thread (p, t) { if (mark) ksu_set_task_tracepoint_flag(t); else ksu_clear_task_tracepoint_flag(t); } read_unlock(&tasklist_lock); } void ksu_mark_all_process(void) { handle_process_mark(true); pr_info("hook_manager: mark all user process done!\n"); } void ksu_unmark_all_process(void) { handle_process_mark(false); pr_info("hook_manager: unmark all user process done!\n"); } static void ksu_mark_running_process_locked() { struct task_struct *p, *t; read_lock(&tasklist_lock); for_each_process_thread (p, t) { if (!t->mm) { // only user processes continue; } int uid = task_uid(t).val; const struct cred *cred = get_task_cred(t); bool ksu_root_process = uid == 0 && is_task_ksu_domain(cred); bool is_zygote_process = is_zygote(cred); bool is_shell = uid == 2000; // before boot completed, we shall mark init for marking zygote bool is_init = t->pid == 1; if (ksu_root_process || is_zygote_process || is_shell || is_init || ksu_is_allow_uid(uid)) { ksu_set_task_tracepoint_flag(t); pr_info("hook_manager: mark process: pid:%d, uid: %d, comm:%s\n", t->pid, uid, t->comm); } else { ksu_clear_task_tracepoint_flag(t); pr_info("hook_manager: unmark process: pid:%d, uid: %d, comm:%s\n", t->pid, uid, t->comm); } put_cred(cred); } read_unlock(&tasklist_lock); } void ksu_mark_running_process() { unsigned long flags; spin_lock_irqsave(&tracepoint_reg_lock, flags); if (tracepoint_reg_count <= 1) { ksu_mark_running_process_locked(); } else { pr_info( "hook_manager: not mark running process since syscall tracepoint is in use\n"); } spin_unlock_irqrestore(&tracepoint_reg_lock, flags); } // Get task mark status // Returns: 1 if marked, 0 if not marked, -ESRCH if task not found int ksu_get_task_mark(pid_t pid) { struct task_struct *task; int marked = -ESRCH; rcu_read_lock(); task = find_task_by_vpid(pid); if (task) { get_task_struct(task); rcu_read_unlock(); #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) marked = test_task_syscall_work(task, SYSCALL_TRACEPOINT) ? 1 : 0; #else marked = test_tsk_thread_flag(task, TIF_SYSCALL_TRACEPOINT) ? 1 : 0; #endif put_task_struct(task); } else { rcu_read_unlock(); } return marked; } // Set task mark status // Returns: 0 on success, -ESRCH if task not found int ksu_set_task_mark(pid_t pid, bool mark) { struct task_struct *task; int ret = -ESRCH; rcu_read_lock(); task = find_task_by_vpid(pid); if (task) { get_task_struct(task); rcu_read_unlock(); if (mark) { ksu_set_task_tracepoint_flag(task); pr_info("hook_manager: marked task pid=%d comm=%s\n", pid, task->comm); } else { ksu_clear_task_tracepoint_flag(task); pr_info("hook_manager: unmarked task pid=%d comm=%s\n", pid, task->comm); } put_task_struct(task); ret = 0; } else { rcu_read_unlock(); } return ret; } #ifdef CONFIG_KRETPROBES static struct kretprobe *init_kretprobe(const char *name, kretprobe_handler_t handler) { struct kretprobe *rp = kzalloc(sizeof(struct kretprobe), GFP_KERNEL); if (!rp) return NULL; rp->kp.symbol_name = name; rp->handler = handler; rp->data_size = 0; rp->maxactive = 0; int ret = register_kretprobe(rp); pr_info("hook_manager: register_%s kretprobe: %d\n", name, ret); if (ret) { kfree(rp); return NULL; } return rp; } static void destroy_kretprobe(struct kretprobe **rp_ptr) { struct kretprobe *rp = *rp_ptr; if (!rp) return; unregister_kretprobe(rp); synchronize_rcu(); kfree(rp); *rp_ptr = NULL; } static int syscall_regfunc_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { unsigned long flags; spin_lock_irqsave(&tracepoint_reg_lock, flags); if (tracepoint_reg_count < 1) { // while install our tracepoint, mark our processes ksu_mark_running_process_locked(); } else if (tracepoint_reg_count == 1) { // while other tracepoint first added, mark all processes ksu_mark_all_process(); } tracepoint_reg_count++; spin_unlock_irqrestore(&tracepoint_reg_lock, flags); return 0; } static int syscall_unregfunc_handler(struct kretprobe_instance *ri, struct pt_regs *regs) { unsigned long flags; spin_lock_irqsave(&tracepoint_reg_lock, flags); tracepoint_reg_count--; if (tracepoint_reg_count <= 0) { // while no tracepoint left, unmark all processes ksu_unmark_all_process(); } else if (tracepoint_reg_count == 1) { // while just our tracepoint left, unmark disallowed processes ksu_mark_running_process_locked(); } spin_unlock_irqrestore(&tracepoint_reg_lock, flags); return 0; } static struct kretprobe *syscall_regfunc_rp = NULL; static struct kretprobe *syscall_unregfunc_rp = NULL; #endif static inline bool check_syscall_fastpath(int nr) { switch (nr) { case __NR_newfstatat: case __NR_faccessat: case __NR_execve: case __NR_setresuid: return true; default: return false; } } // Unmark init's child that are not zygote, adbd or ksud int ksu_handle_init_mark_tracker(const char __user **filename_user) { char path[64]; unsigned long addr; const char __user *fn; long ret; if (unlikely(!filename_user)) return 0; addr = untagged_addr((unsigned long)*filename_user); fn = (const char __user *)addr; memset(path, 0, sizeof(path)); ret = strncpy_from_user_nofault(path, fn, sizeof(path)); if (ret < 0 && try_set_access_flag(addr)) { ret = strncpy_from_user_nofault(path, fn, sizeof(path)); pr_info("ksu_handle_init_mark_tracker: %ld\n", ret); } if (unlikely(strcmp(path, KSUD_PATH) == 0)) { pr_info("hook_manager: escape to root for init executing ksud: %d\n", current->pid); escape_to_root_for_init(); } else if (likely(strstr(path, "/app_process") == NULL && strstr(path, "/adbd") == NULL)) { pr_info("hook_manager: unmark %d exec %s\n", current->pid, path); ksu_clear_task_tracepoint_flag_if_needed(current); } return 0; } #ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS // Generic sys_enter handler that dispatches to specific handlers static void ksu_sys_enter_handler(void *data, struct pt_regs *regs, long id) { if (unlikely(check_syscall_fastpath(id))) { if (ksu_su_compat_enabled) { // Handle newfstatat if (id == __NR_newfstatat) { int *dfd = (int *)&PT_REGS_PARM1(regs); const char __user **filename_user = (const char __user **)&PT_REGS_PARM2(regs); int *flags = (int *)&PT_REGS_SYSCALL_PARM4(regs); ksu_handle_stat(dfd, filename_user, flags); return; } // Handle faccessat if (id == __NR_faccessat) { int *dfd = (int *)&PT_REGS_PARM1(regs); const char __user **filename_user = (const char __user **)&PT_REGS_PARM2(regs); int *mode = (int *)&PT_REGS_PARM3(regs); ksu_handle_faccessat(dfd, filename_user, mode, NULL); return; } // Handle execve if (id == __NR_execve) { const char __user **filename_user = (const char __user **)&PT_REGS_PARM1(regs); if (current->pid != 1 && is_init(get_current_cred())) { ksu_handle_init_mark_tracker(filename_user); } else { ksu_handle_execve_sucompat(filename_user, NULL, NULL, NULL); } return; } } // Handle setresuid if (id == __NR_setresuid) { uid_t ruid = (uid_t)PT_REGS_PARM1(regs); uid_t euid = (uid_t)PT_REGS_PARM2(regs); uid_t suid = (uid_t)PT_REGS_PARM3(regs); ksu_handle_setresuid(ruid, euid, suid); return; } } } #endif void ksu_syscall_hook_manager_init(void) { int ret; pr_info("hook_manager: ksu_hook_manager_init called\n"); #ifdef CONFIG_KRETPROBES // Register kretprobe for syscall_regfunc syscall_regfunc_rp = init_kretprobe("syscall_regfunc", syscall_regfunc_handler); // Register kretprobe for syscall_unregfunc syscall_unregfunc_rp = init_kretprobe("syscall_unregfunc", syscall_unregfunc_handler); #endif #ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS ret = register_trace_sys_enter(ksu_sys_enter_handler, NULL); #ifndef CONFIG_KRETPROBES ksu_mark_running_process_locked(); #endif if (ret) { pr_err("hook_manager: failed to register sys_enter tracepoint: %d\n", ret); } else { pr_info("hook_manager: sys_enter tracepoint registered\n"); } #endif ksu_setuid_hook_init(); ksu_sucompat_init(); } void ksu_syscall_hook_manager_exit(void) { pr_info("hook_manager: ksu_hook_manager_exit called\n"); #ifdef CONFIG_HAVE_SYSCALL_TRACEPOINTS unregister_trace_sys_enter(ksu_sys_enter_handler, NULL); tracepoint_synchronize_unregister(); pr_info("hook_manager: sys_enter tracepoint unregistered\n"); #endif #ifdef CONFIG_KRETPROBES destroy_kretprobe(&syscall_regfunc_rp); destroy_kretprobe(&syscall_unregfunc_rp); #endif ksu_sucompat_exit(); ksu_setuid_hook_exit(); } ================================================ FILE: kernel/syscall_hook_manager.h ================================================ #ifndef __KSU_H_HOOK_MANAGER #define __KSU_H_HOOK_MANAGER #include #include #include // Hook manager initialization and cleanup void ksu_syscall_hook_manager_init(void); void ksu_syscall_hook_manager_exit(void); // Process marking for tracepoint void ksu_mark_all_process(void); void ksu_unmark_all_process(void); void ksu_mark_running_process(void); // Per-task mark operations int ksu_get_task_mark(pid_t pid); int ksu_set_task_mark(pid_t pid, bool mark); static inline void ksu_set_task_tracepoint_flag(struct task_struct *t) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) set_task_syscall_work(t, SYSCALL_TRACEPOINT); #else set_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT); #endif } static inline void ksu_clear_task_tracepoint_flag(struct task_struct *t) { #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) clear_task_syscall_work(t, SYSCALL_TRACEPOINT); #else clear_tsk_thread_flag(t, TIF_SYSCALL_TRACEPOINT); #endif } void ksu_clear_task_tracepoint_flag_if_needed(struct task_struct *t); #endif ================================================ FILE: kernel/throne_tracker.c ================================================ #include #include #include #include #include #include #include #include "allowlist.h" #include "apk_sign.h" #include "klog.h" // IWYU pragma: keep #include "manager.h" #include "throne_tracker.h" uid_t ksu_manager_appid = KSU_INVALID_APPID; #define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list" struct uid_data { struct list_head list; u32 uid; char package[KSU_MAX_PACKAGE_NAME]; }; static void crown_manager(const char *apk, struct list_head *uid_data) { char pkg[KSU_MAX_PACKAGE_NAME]; if (get_pkg_from_apk_path(pkg, apk) < 0) { pr_err("Failed to get package name from apk path: %s\n", apk); return; } pr_info("manager pkg: %s\n", pkg); struct list_head *list = (struct list_head *)uid_data; struct uid_data *np; list_for_each_entry (np, list, list) { if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) { pr_info("Crowning manager: %s(uid=%d)\n", pkg, np->uid); ksu_set_manager_appid(np->uid); break; } } } #define DATA_PATH_LEN 384 // 384 is enough for /data/app//base.apk struct data_path { char dirpath[DATA_PATH_LEN]; int depth; struct list_head list; }; struct apk_path_hash { unsigned int hash; bool exists; struct list_head list; }; static struct list_head apk_path_hash_list = LIST_HEAD_INIT(apk_path_hash_list); struct my_dir_context { struct dir_context ctx; struct list_head *data_path_list; char *parent_dir; void *private_data; int depth; int *stop; }; // https://docs.kernel.org/filesystems/porting.html // filldir_t (readdir callbacks) calling conventions have changed. Instead of returning 0 or -E... it returns bool now. false means "no more" (as -E... used to) and true - "keep going" (as 0 in old calling conventions). Rationale: callers never looked at specific -E... values anyway. -> iterate_shared() instances require no changes at all, all filldir_t ones in the tree converted. #if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 1, 0) #define FILLDIR_RETURN_TYPE bool #define FILLDIR_ACTOR_CONTINUE true #define FILLDIR_ACTOR_STOP false #else #define FILLDIR_RETURN_TYPE int #define FILLDIR_ACTOR_CONTINUE 0 #define FILLDIR_ACTOR_STOP -EINVAL #endif extern bool is_manager_apk(char *path); FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, int namelen, loff_t off, u64 ino, unsigned int d_type) { struct my_dir_context *my_ctx = container_of(ctx, struct my_dir_context, ctx); char dirpath[DATA_PATH_LEN]; if (!my_ctx) { pr_err("Invalid context\n"); return FILLDIR_ACTOR_STOP; } if (my_ctx->stop && *my_ctx->stop) { pr_info("Stop searching\n"); return FILLDIR_ACTOR_STOP; } if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) return FILLDIR_ACTOR_CONTINUE; // Skip "." and ".." if (d_type == DT_DIR && namelen >= 8 && !strncmp(name, "vmdl", 4) && !strncmp(name + namelen - 4, ".tmp", 4)) { pr_info("Skipping directory: %.*s\n", namelen, name); return FILLDIR_ACTOR_CONTINUE; // Skip staging package } if (snprintf(dirpath, DATA_PATH_LEN, "%s/%.*s", my_ctx->parent_dir, namelen, name) >= DATA_PATH_LEN) { pr_err("Path too long: %s/%.*s\n", my_ctx->parent_dir, namelen, name); return FILLDIR_ACTOR_CONTINUE; } if (d_type == DT_DIR && my_ctx->depth > 0 && (my_ctx->stop && !*my_ctx->stop)) { struct data_path *data = kzalloc(sizeof(struct data_path), GFP_ATOMIC); if (!data) { pr_err("Failed to allocate memory for %s\n", dirpath); return FILLDIR_ACTOR_CONTINUE; } strscpy(data->dirpath, dirpath, DATA_PATH_LEN); data->depth = my_ctx->depth - 1; list_add_tail(&data->list, my_ctx->data_path_list); } else { if ((namelen == 8) && (strncmp(name, "base.apk", namelen) == 0)) { struct apk_path_hash *pos, *n; unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath)); list_for_each_entry (pos, &apk_path_hash_list, list) { if (hash == pos->hash) { pos->exists = true; return FILLDIR_ACTOR_CONTINUE; } } bool is_manager = is_manager_apk(dirpath); pr_info("Found new base.apk at path: %s, is_manager: %d\n", dirpath, is_manager); if (is_manager) { crown_manager(dirpath, my_ctx->private_data); *my_ctx->stop = 1; // Manager found, clear APK cache list list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) { list_del(&pos->list); kfree(pos); } } else { struct apk_path_hash *apk_data = kzalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); apk_data->hash = hash; apk_data->exists = true; list_add_tail(&apk_data->list, &apk_path_hash_list); } } } return FILLDIR_ACTOR_CONTINUE; } void search_manager(const char *path, int depth, struct list_head *uid_data) { int i, stop = 0; struct list_head data_path_list; INIT_LIST_HEAD(&data_path_list); unsigned long data_app_magic = 0; // Initialize APK cache list struct apk_path_hash *pos, *n; list_for_each_entry (pos, &apk_path_hash_list, list) { pos->exists = false; } // First depth struct data_path data; strscpy(data.dirpath, path, DATA_PATH_LEN); data.depth = depth; list_add_tail(&data.list, &data_path_list); for (i = depth; i >= 0; i--) { struct data_path *pos, *n; list_for_each_entry_safe (pos, n, &data_path_list, list) { struct my_dir_context ctx = { .ctx.actor = my_actor, .data_path_list = &data_path_list, .parent_dir = pos->dirpath, .private_data = uid_data, .depth = pos->depth, .stop = &stop }; struct file *file; if (!stop) { file = filp_open(pos->dirpath, O_RDONLY | O_NOFOLLOW, 0); if (IS_ERR(file)) { pr_err("Failed to open directory: %s, err: %ld\n", pos->dirpath, PTR_ERR(file)); goto skip_iterate; } // grab magic on first folder, which is /data/app if (!data_app_magic) { if (file->f_inode->i_sb->s_magic) { data_app_magic = file->f_inode->i_sb->s_magic; pr_info("%s: dir: %s got magic! 0x%lx\n", __func__, pos->dirpath, data_app_magic); } else { filp_close(file, NULL); goto skip_iterate; } } if (file->f_inode->i_sb->s_magic != data_app_magic) { pr_info("%s: skip: %s magic: 0x%lx expected: 0x%lx\n", __func__, pos->dirpath, file->f_inode->i_sb->s_magic, data_app_magic); filp_close(file, NULL); goto skip_iterate; } iterate_dir(file, &ctx.ctx); filp_close(file, NULL); } skip_iterate: list_del(&pos->list); if (pos != &data) kfree(pos); } } // Remove stale cached APK entries list_for_each_entry_safe (pos, n, &apk_path_hash_list, list) { if (!pos->exists) { list_del(&pos->list); kfree(pos); } } } static bool is_uid_exist(uid_t uid, char *package, void *data) { struct list_head *list = (struct list_head *)data; struct uid_data *np; bool exist = false; list_for_each_entry (np, list, list) { if (np->uid == uid % PER_USER_RANGE && strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { exist = true; break; } } return exist; } void track_throne(bool prune_only) { struct file *fp = filp_open(SYSTEM_PACKAGES_LIST_PATH, O_RDONLY, 0); if (IS_ERR(fp)) { pr_err("%s: open " SYSTEM_PACKAGES_LIST_PATH " failed: %ld\n", __func__, PTR_ERR(fp)); return; } struct list_head uid_list; INIT_LIST_HEAD(&uid_list); char chr = 0; loff_t pos = 0; loff_t line_start = 0; char buf[KSU_MAX_PACKAGE_NAME]; for (;;) { ssize_t count = kernel_read(fp, &chr, sizeof(chr), &pos); if (count != sizeof(chr)) break; if (chr != '\n') continue; count = kernel_read(fp, buf, sizeof(buf), &line_start); struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC); if (!data) { filp_close(fp, 0); goto out; } char *tmp = buf; const char *delim = " "; char *package = strsep(&tmp, delim); char *uid = strsep(&tmp, delim); if (!uid || !package) { kfree(data); pr_err("update_uid: package or uid is NULL!\n"); break; } u32 res; if (kstrtou32(uid, 10, &res)) { kfree(data); pr_err("update_uid: uid parse err\n"); break; } data->uid = res; strncpy(data->package, package, KSU_MAX_PACKAGE_NAME); list_add_tail(&data->list, &uid_list); // reset line start line_start = pos; } filp_close(fp, 0); // now update uid list struct uid_data *np; struct uid_data *n; if (prune_only) goto prune; // first, check if manager_uid exist! bool manager_exist = false; list_for_each_entry (np, &uid_list, list) { if (np->uid == ksu_get_manager_appid()) { manager_exist = true; break; } } if (!manager_exist) { if (ksu_is_manager_appid_valid()) { pr_info("manager is uninstalled, invalidate it!\n"); ksu_invalidate_manager_uid(); goto prune; } pr_info("Searching manager...\n"); search_manager("/data/app", 2, &uid_list); pr_info("Search manager finished\n"); } prune: // then prune the allowlist ksu_prune_allowlist(is_uid_exist, &uid_list); out: // free uid_list list_for_each_entry_safe (np, n, &uid_list, list) { list_del(&np->list); kfree(np); } } void ksu_throne_tracker_init() { // nothing to do } void ksu_throne_tracker_exit() { // nothing to do } ================================================ FILE: kernel/throne_tracker.h ================================================ #ifndef __KSU_H_UID_OBSERVER #define __KSU_H_UID_OBSERVER void ksu_throne_tracker_init(); void ksu_throne_tracker_exit(); void track_throne(bool prune_only); #endif ================================================ FILE: kernel/tools/check_symbol.c ================================================ #include #include #include #include #include #include #include #include typedef struct { void *data; size_t size; Elf64_Ehdr *ehdr; Elf64_Shdr *shdr; char *shstrtab; } ElfFile; int open_elf(const char *path, ElfFile *elf) { int fd = open(path, O_RDONLY); if (fd < 0) { fprintf(stderr, "Error: Cannot open file %s\n", path); return -1; } struct stat st; if (fstat(fd, &st) < 0) { fprintf(stderr, "Error: Cannot stat file %s\n", path); close(fd); return -1; } elf->size = st.st_size; elf->data = mmap(NULL, elf->size, PROT_READ, MAP_PRIVATE, fd, 0); close(fd); if (elf->data == MAP_FAILED) { fprintf(stderr, "Error: Cannot mmap file %s\n", path); return -1; } elf->ehdr = (Elf64_Ehdr *)elf->data; if (memcmp(elf->ehdr->e_ident, ELFMAG, SELFMAG) != 0) { fprintf(stderr, "Error: %s is not a valid ELF file\n", path); munmap(elf->data, elf->size); return -1; } if (elf->ehdr->e_ident[EI_CLASS] != ELFCLASS64) { fprintf(stderr, "Error: %s is not a 64-bit ELF file\n", path); munmap(elf->data, elf->size); return -1; } elf->shdr = (Elf64_Shdr *)((char *)elf->data + elf->ehdr->e_shoff); elf->shstrtab = (char *)elf->data + elf->shdr[elf->ehdr->e_shstrndx].sh_offset; return 0; } void close_elf(ElfFile *elf) { munmap(elf->data, elf->size); } Elf64_Shdr *find_symtab(ElfFile *elf) { for (int i = 0; i < elf->ehdr->e_shnum; i++) { if (elf->shdr[i].sh_type == SHT_SYMTAB) { return &elf->shdr[i]; } } return NULL; } Elf64_Shdr *find_section(ElfFile *elf, const char *name) { for (int i = 0; i < elf->ehdr->e_shnum; i++) { const char *section_name = elf->shstrtab + elf->shdr[i].sh_name; if (strcmp(section_name, name) == 0) { return &elf->shdr[i]; } } return NULL; } Elf64_Sym *find_symbol(ElfFile *elf, const char *name, Elf64_Shdr *symtab, char *strtab) { Elf64_Sym *syms = (Elf64_Sym *)((char *)elf->data + symtab->sh_offset); int sym_count = symtab->sh_size / sizeof(Elf64_Sym); for (int i = 0; i < sym_count; i++) { const char *sym_name = strtab + syms[i].st_name; if (strcmp(sym_name, name) == 0) { return &syms[i]; } } return NULL; } int main(int argc, char *argv[]) { if (argc != 3) { fprintf(stderr, "Usage: %s \n", argv[0]); return 1; } const char *ko_path = argv[1]; const char *vmlinux_path = argv[2]; ElfFile ko_elf, vmlinux; if (open_elf(ko_path, &ko_elf) < 0) { return 1; } if (open_elf(vmlinux_path, &vmlinux) < 0) { close_elf(&ko_elf); return 1; } Elf64_Shdr *ko_symtab = find_symtab(&ko_elf); Elf64_Shdr *vmlinux_symtab = find_symtab(&vmlinux); Elf64_Shdr *ko_version_sec = find_section(&ko_elf, "__versions"); if (!ko_symtab) { fprintf(stderr, "Error: No symbol table found in %s\n", ko_path); close_elf(&ko_elf); close_elf(&vmlinux); return 1; } if (!vmlinux_symtab) { fprintf(stderr, "Error: No symbol table found in %s\n", vmlinux_path); close_elf(&ko_elf); close_elf(&vmlinux); return 1; } if (!ko_version_sec) { fprintf(stderr, "Error: No __versions section found in %s\n", ko_path); close_elf(&ko_elf); close_elf(&vmlinux); return 1; } if (ko_version_sec->sh_size != 0) { fprintf( stderr, "Error: __versions section in %s must have size 0 (actual=%llu)\n", ko_path, (unsigned long long)ko_version_sec->sh_size); close_elf(&ko_elf); close_elf(&vmlinux); return 1; } char *ko_strtab = (char *)ko_elf.data + ko_elf.shdr[ko_symtab->sh_link].sh_offset; char *vmlinux_strtab = (char *)vmlinux.data + vmlinux.shdr[vmlinux_symtab->sh_link].sh_offset; Elf64_Sym *ko_syms = (Elf64_Sym *)((char *)ko_elf.data + ko_symtab->sh_offset); int ko_sym_count = ko_symtab->sh_size / sizeof(Elf64_Sym); int has_error = 0; for (int i = 0; i < ko_sym_count; i++) { if (ko_syms[i].st_shndx == SHN_UNDEF && ko_syms[i].st_name != 0) { const char *sym_name = ko_strtab + ko_syms[i].st_name; Elf64_Sym *vmlinux_sym = find_symbol(&vmlinux, sym_name, vmlinux_symtab, vmlinux_strtab); if (!vmlinux_sym || vmlinux_sym->st_shndx == SHN_UNDEF) { fprintf(stderr, "Error: Symbol '%s' not found or undefined in %s\n", sym_name, vmlinux_path); has_error = 1; } else { int binding = ELF64_ST_BIND(vmlinux_sym->st_info); if (binding != STB_GLOBAL && binding != STB_WEAK) { fprintf( stderr, "Warning: Symbol '%s' is defined in %s but not global (binding=%d)\n", sym_name, vmlinux_path, binding); } } } } close_elf(&ko_elf); close_elf(&vmlinux); return has_error ? 1 : 0; } ================================================ FILE: kernel/util.c ================================================ #include #include #include #include #include "util.h" bool try_set_access_flag(unsigned long addr) { #ifdef CONFIG_ARM64 struct mm_struct *mm = current->mm; struct vm_area_struct *vma; pgd_t *pgd; p4d_t *p4d; pud_t *pud; pmd_t *pmd; pte_t *ptep, pte; spinlock_t *ptl; bool ret = false; if (!mm) return false; if (!mmap_read_trylock(mm)) return false; vma = find_vma(mm, addr); if (!vma || addr < vma->vm_start) goto out_unlock; pgd = pgd_offset(mm, addr); if (!pgd_present(*pgd)) goto out_unlock; p4d = p4d_offset(pgd, addr); if (!p4d_present(*p4d)) goto out_unlock; pud = pud_offset(p4d, addr); if (!pud_present(*pud)) goto out_unlock; pmd = pmd_offset(pud, addr); if (!pmd_present(*pmd)) goto out_unlock; if (pmd_trans_huge(*pmd)) goto out_unlock; ptep = pte_offset_map_lock(mm, pmd, addr, &ptl); if (!ptep) goto out_unlock; pte = *ptep; if (!pte_present(pte)) goto out_pte_unlock; if (pte_young(pte)) { ret = true; goto out_pte_unlock; } ptep_set_access_flags(vma, addr, ptep, pte_mkyoung(pte), 0); pr_info("set AF for addr %lx\n", addr); ret = true; out_pte_unlock: pte_unmap_unlock(ptep, ptl); out_unlock: mmap_read_unlock(mm); return ret; #else return false; #endif } ================================================ FILE: kernel/util.h ================================================ #ifndef __KSU_UTIL_H #define __KSU_UTIL_H #include #ifndef preempt_enable_no_resched_notrace #define preempt_enable_no_resched_notrace() \ do { \ barrier(); \ __preempt_count_dec(); \ } while (0) #endif #ifndef preempt_disable_notrace #define preempt_disable_notrace() \ do { \ __preempt_count_inc(); \ barrier(); \ } while (0) #endif bool try_set_access_flag(unsigned long addr); #endif ================================================ FILE: manager/.gitignore ================================================ *.iml .gradle .idea .kotlin .DS_Store build captures .cxx local.properties key.jks ================================================ FILE: manager/app/.gitignore ================================================ /build /release/ ================================================ FILE: manager/app/build.gradle.kts ================================================ @file:Suppress("UnstableApiUsage") plugins { alias(libs.plugins.agp.app) alias(libs.plugins.compose.compiler) alias(libs.plugins.kotlin.serialization) alias(libs.plugins.lsplugin.apksign) id("kotlin-parcelize") } val androidCompileSdkVersion: Int by rootProject.extra val androidCompileNdkVersion: String by rootProject.extra val androidBuildToolsVersion: String by rootProject.extra val androidMinSdkVersion: Int by rootProject.extra val androidTargetSdkVersion: Int by rootProject.extra val androidSourceCompatibility: JavaVersion by rootProject.extra val androidTargetCompatibility: JavaVersion by rootProject.extra val managerVersionCode: Int by rootProject.extra val managerVersionName: String by rootProject.extra apksign { storeFileProperty = "KEYSTORE_FILE" storePasswordProperty = "KEYSTORE_PASSWORD" keyAliasProperty = "KEY_ALIAS" keyPasswordProperty = "KEY_PASSWORD" } val baseCFlags = listOf( "-Wall", "-Qunused-arguments", "-fvisibility=hidden", "-fvisibility-inlines-hidden", "-fno-exceptions", "-fno-stack-protector", "-fomit-frame-pointer", "-Wno-builtin-macro-redefined", "-Wno-unused-value", "-D__FILE__=__FILE_NAME__" ) val baseCppFlags = baseCFlags + "-fno-rtti" android { namespace = "me.weishu.kernelsu" buildTypes { debug { externalNativeBuild { cmake { arguments += listOf("-DCMAKE_CXX_FLAGS_DEBUG=-Og", "-DCMAKE_C_FLAGS_DEBUG=-Og") } } } release { isMinifyEnabled = true isShrinkResources = true vcsInfo.include = false proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") externalNativeBuild { cmake { arguments += "-DDEBUG_SYMBOLS_PATH=${layout.buildDirectory.get().asFile.absolutePath}/symbols" arguments += "-DCMAKE_BUILD_TYPE=Release" val releaseFlags = listOf( "-flto", "-ffunction-sections", "-fdata-sections", "-Wl,--gc-sections", "-fno-unwind-tables", "-fno-asynchronous-unwind-tables", "-Wl,--exclude-libs,ALL" ) val configFlags = listOf("-Oz", "-DNDEBUG").joinToString(" ") cppFlags += releaseFlags cFlags += releaseFlags arguments += listOf( "-DCMAKE_CXX_FLAGS_RELEASE=$configFlags", "-DCMAKE_C_FLAGS_RELEASE=$configFlags", "-DCMAKE_SHARED_LINKER_FLAGS=-Wl,--gc-sections -Wl,--exclude-libs,ALL -Wl,--icf=all -s -Wl,--hash-style=sysv -Wl,-z,norelro" ) } } } } buildFeatures { aidl = true buildConfig = true compose = true prefab = true } packaging { dex { useLegacyPackaging = true } jniLibs { useLegacyPackaging = true } } externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") } } dependenciesInfo { includeInApk = false includeInBundle = false } androidResources { generateLocaleConfig = true } compileSdk = androidCompileSdkVersion ndkVersion = androidCompileNdkVersion buildToolsVersion = androidBuildToolsVersion defaultConfig { minSdk = androidMinSdkVersion targetSdk = androidTargetSdkVersion versionCode = managerVersionCode versionName = managerVersionName val isPrBuild = project.findProperty("IS_PR_BUILD")?.toString()?.toBoolean() ?: false buildConfigField("boolean", "IS_PR_BUILD", isPrBuild.toString()) externalNativeBuild { cmake { arguments += "-DANDROID_STL=none" cFlags += baseCFlags + "-std=c2x" cppFlags += baseCppFlags + "-std=c++2b" } } ndk { abiFilters += listOf("arm64-v8a", "x86_64") } } lint { abortOnError = true checkReleaseBuilds = false } compileOptions { sourceCompatibility = androidSourceCompatibility targetCompatibility = androidTargetCompatibility } } androidComponents { onVariants(selector().withBuildType("release")) { it.packaging.resources.excludes.addAll(listOf("META-INF/**", "kotlin/**", "org/**", "**.bin")) } } base { archivesName.set( "KernelSU_${managerVersionName}_${managerVersionCode}" ) } dependencies { implementation(libs.androidx.activity.compose) implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.compose.material.icons.extended) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.ui) implementation(libs.androidx.compose.ui.tooling.preview) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.runtime.ktx) implementation(libs.androidx.lifecycle.viewmodel.compose) implementation(libs.androidx.lifecycle.viewmodel.navigation3) implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigationevent.compose) implementation(libs.com.github.topjohnwu.libsu.core) implementation(libs.com.github.topjohnwu.libsu.service) implementation(libs.com.github.topjohnwu.libsu.io) implementation(libs.dev.rikka.rikkax.parcelablelist) implementation(libs.kotlinx.coroutines.core) implementation(libs.markwon) implementation(libs.androidx.webkit) implementation(libs.lsposed.cxx) implementation(libs.hiddenapibypass) implementation(libs.miuix) implementation(libs.miuix.icons) implementation(libs.miuix.navigation3.ui) implementation(platform(libs.okhttp.bom)) implementation(libs.okhttp) implementation(libs.backdrop) implementation(libs.capsule) implementation(libs.haze) implementation(libs.material.kolor) } ================================================ FILE: manager/app/proguard-rules.pro ================================================ ================================================ FILE: manager/app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: manager/app/src/main/aidl/me/weishu/kernelsu/IKsuInterface.aidl ================================================ // IKsuInterface.aidl package me.weishu.kernelsu; import android.content.pm.PackageInfo; import rikka.parcelablelist.ParcelableListSlice; interface IKsuInterface { ParcelableListSlice getPackages(int flags); int[] getUserIds(); } ================================================ FILE: manager/app/src/main/assets/github-markdown.css ================================================ /* https://raw.githubusercontent.com/sindresorhus/github-markdown-css/gh-pages/github-markdown.css */ .markdown-body { --base-size-4: 0.25rem; --base-size-8: 0.5rem; --base-size-16: 1rem; --base-size-24: 1.5rem; --base-size-40: 2.5rem; --base-text-weight-normal: 400; --base-text-weight-medium: 500; --base-text-weight-semibold: 600; --fontStack-monospace: ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace; --fgColor-accent: Highlight; } [data-theme="dark"] { color-scheme: dark; --focus-outlineColor: #1f6feb; --fgColor-default: #f0f6fc; --fgColor-muted: #9198a1; --fgColor-accent: #4493f8; --fgColor-success: #3fb950; --fgColor-attention: #d29922; --fgColor-danger: #f85149; --fgColor-done: #ab7df8; --bgColor-default: #0d1117; --bgColor-muted: #151b23; --bgColor-neutral-muted: #656c7633; --bgColor-attention-muted: #bb800926; --borderColor-default: #3d444d; --borderColor-muted: #3d444db3; --borderColor-neutral-muted: #3d444db3; --borderColor-accent-emphasis: #1f6feb; --borderColor-success-emphasis: #238636; --borderColor-attention-emphasis: #9e6a03; --borderColor-danger-emphasis: #da3633; --borderColor-done-emphasis: #8957e5; --color-prettylights-syntax-comment: #9198a1; --color-prettylights-syntax-constant: #79c0ff; --color-prettylights-syntax-constant-other-reference-link: #a5d6ff; --color-prettylights-syntax-entity: #d2a8ff; --color-prettylights-syntax-storage-modifier-import: #f0f6fc; --color-prettylights-syntax-entity-tag: #7ee787; --color-prettylights-syntax-keyword: #ff7b72; --color-prettylights-syntax-string: #a5d6ff; --color-prettylights-syntax-variable: #ffa657; --color-prettylights-syntax-brackethighlighter-unmatched: #f85149; --color-prettylights-syntax-brackethighlighter-angle: #9198a1; --color-prettylights-syntax-invalid-illegal-text: #f0f6fc; --color-prettylights-syntax-invalid-illegal-bg: #8e1519; --color-prettylights-syntax-carriage-return-text: #f0f6fc; --color-prettylights-syntax-carriage-return-bg: #b62324; --color-prettylights-syntax-string-regexp: #7ee787; --color-prettylights-syntax-markup-list: #f2cc60; --color-prettylights-syntax-markup-heading: #1f6feb; --color-prettylights-syntax-markup-italic: #f0f6fc; --color-prettylights-syntax-markup-bold: #f0f6fc; --color-prettylights-syntax-markup-deleted-text: #ffdcd7; --color-prettylights-syntax-markup-deleted-bg: #67060c; --color-prettylights-syntax-markup-inserted-text: #aff5b4; --color-prettylights-syntax-markup-inserted-bg: #033a16; --color-prettylights-syntax-markup-changed-text: #ffdfb6; --color-prettylights-syntax-markup-changed-bg: #5a1e02; --color-prettylights-syntax-markup-ignored-text: #f0f6fc; --color-prettylights-syntax-markup-ignored-bg: #1158c7; --color-prettylights-syntax-meta-diff-range: #d2a8ff; --color-prettylights-syntax-sublimelinter-gutter-mark: #3d444d; } [data-theme="light"] { color-scheme: light; --focus-outlineColor: #0969da; --fgColor-default: #1f2328; --fgColor-muted: #59636e; --fgColor-accent: #0969da; --fgColor-success: #1a7f37; --fgColor-attention: #9a6700; --fgColor-danger: #d1242f; --fgColor-done: #8250df; --bgColor-default: #ffffff; --bgColor-muted: #f6f8fa; --bgColor-neutral-muted: #818b981f; --bgColor-attention-muted: #fff8c5; --borderColor-default: #d1d9e0; --borderColor-muted: #d1d9e0b3; --borderColor-neutral-muted: #d1d9e0b3; --borderColor-accent-emphasis: #0969da; --borderColor-success-emphasis: #1a7f37; --borderColor-attention-emphasis: #9a6700; --borderColor-danger-emphasis: #cf222e; --borderColor-done-emphasis: #8250df; --color-prettylights-syntax-comment: #59636e; --color-prettylights-syntax-constant: #0550ae; --color-prettylights-syntax-constant-other-reference-link: #0a3069; --color-prettylights-syntax-entity: #6639ba; --color-prettylights-syntax-storage-modifier-import: #1f2328; --color-prettylights-syntax-entity-tag: #0550ae; --color-prettylights-syntax-keyword: #cf222e; --color-prettylights-syntax-string: #0a3069; --color-prettylights-syntax-variable: #953800; --color-prettylights-syntax-brackethighlighter-unmatched: #82071e; --color-prettylights-syntax-brackethighlighter-angle: #59636e; --color-prettylights-syntax-invalid-illegal-text: #f6f8fa; --color-prettylights-syntax-invalid-illegal-bg: #82071e; --color-prettylights-syntax-carriage-return-text: #f6f8fa; --color-prettylights-syntax-carriage-return-bg: #cf222e; --color-prettylights-syntax-string-regexp: #116329; --color-prettylights-syntax-markup-list: #3b2300; --color-prettylights-syntax-markup-heading: #0550ae; --color-prettylights-syntax-markup-italic: #1f2328; --color-prettylights-syntax-markup-bold: #1f2328; --color-prettylights-syntax-markup-deleted-text: #82071e; --color-prettylights-syntax-markup-deleted-bg: #ffebe9; --color-prettylights-syntax-markup-inserted-text: #116329; --color-prettylights-syntax-markup-inserted-bg: #dafbe1; --color-prettylights-syntax-markup-changed-text: #953800; --color-prettylights-syntax-markup-changed-bg: #ffd8b5; --color-prettylights-syntax-markup-ignored-text: #d1d9e0; --color-prettylights-syntax-markup-ignored-bg: #0550ae; --color-prettylights-syntax-meta-diff-range: #8250df; --color-prettylights-syntax-sublimelinter-gutter-mark: #818b98; } .markdown-body { -ms-text-size-adjust: 100%; -webkit-text-size-adjust: 100%; margin: 0; color: var(--fgColor-default); background-color: var(--bgColor-default); font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji"; font-size: 16px; line-height: 1.5; word-wrap: break-word; } .markdown-body .octicon { display: inline-block; fill: currentColor; vertical-align: text-bottom; } .markdown-body h1:hover .anchor .octicon-link:before, .markdown-body h2:hover .anchor .octicon-link:before, .markdown-body h3:hover .anchor .octicon-link:before, .markdown-body h4:hover .anchor .octicon-link:before, .markdown-body h5:hover .anchor .octicon-link:before, .markdown-body h6:hover .anchor .octicon-link:before { width: 16px; height: 16px; content: ' '; display: inline-block; background-color: currentColor; -webkit-mask-image: url("data:image/svg+xml,"); mask-image: url("data:image/svg+xml,"); } .markdown-body details, .markdown-body figcaption, .markdown-body figure { display: block; } .markdown-body summary { display: list-item; } .markdown-body [hidden] { display: none !important; } .markdown-body a { background-color: transparent; color: var(--fgColor-accent); text-decoration: none; } .markdown-body abbr[title] { border-bottom: none; -webkit-text-decoration: underline dotted; text-decoration: underline dotted; } .markdown-body b, .markdown-body strong { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dfn { font-style: italic; } .markdown-body h1 { margin: .67em 0; font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 2em; border-bottom: 1px solid var(--borderColor-muted); } .markdown-body mark { background-color: var(--bgColor-attention-muted); color: var(--fgColor-default); } .markdown-body small { font-size: 90%; } .markdown-body sub, .markdown-body sup { font-size: 75%; line-height: 0; position: relative; vertical-align: baseline; } .markdown-body sub { bottom: -0.25em; } .markdown-body sup { top: -0.5em; } .markdown-body img { border-style: none; max-width: 100%; box-sizing: content-box; } .markdown-body code, .markdown-body kbd, .markdown-body pre, .markdown-body samp { font-family: monospace; font-size: 1em; } .markdown-body figure { margin: 1em var(--base-size-40); } .markdown-body hr { box-sizing: content-box; overflow: hidden; background: transparent; border-bottom: 1px solid var(--borderColor-muted); height: .25em; padding: 0; margin: var(--base-size-24) 0; background-color: var(--borderColor-default); border: 0; } .markdown-body input { font: inherit; margin: 0; overflow: visible; font-family: inherit; font-size: inherit; line-height: inherit; } .markdown-body [type=button], .markdown-body [type=reset], .markdown-body [type=submit] { -webkit-appearance: button; appearance: button; } .markdown-body [type=checkbox], .markdown-body [type=radio] { box-sizing: border-box; padding: 0; } .markdown-body [type=number]::-webkit-inner-spin-button, .markdown-body [type=number]::-webkit-outer-spin-button { height: auto; } .markdown-body [type=search]::-webkit-search-cancel-button, .markdown-body [type=search]::-webkit-search-decoration { -webkit-appearance: none; appearance: none; } .markdown-body ::-webkit-input-placeholder { color: inherit; opacity: .54; } .markdown-body ::-webkit-file-upload-button { -webkit-appearance: button; appearance: button; font: inherit; } .markdown-body a:hover { text-decoration: underline; } .markdown-body ::placeholder { color: var(--fgColor-muted); opacity: 1; } .markdown-body hr::before { display: table; content: ""; } .markdown-body hr::after { display: table; clear: both; content: ""; } .markdown-body table { border-spacing: 0; border-collapse: collapse; display: block; width: max-content; max-width: 100%; overflow: auto; font-variant: tabular-nums; } .markdown-body td, .markdown-body th { padding: 0; } .markdown-body details summary { cursor: pointer; } .markdown-body a:focus, .markdown-body [role=button]:focus, .markdown-body input[type=radio]:focus, .markdown-body input[type=checkbox]:focus { outline: 2px solid var(--focus-outlineColor); outline-offset: -2px; box-shadow: none; } .markdown-body a:focus:not(:focus-visible), .markdown-body [role=button]:focus:not(:focus-visible), .markdown-body input[type=radio]:focus:not(:focus-visible), .markdown-body input[type=checkbox]:focus:not(:focus-visible) { outline: solid 1px transparent; } .markdown-body a:focus-visible, .markdown-body [role=button]:focus-visible, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus-visible { outline: 2px solid var(--focus-outlineColor); outline-offset: -2px; box-shadow: none; } .markdown-body a:not([class]):focus, .markdown-body a:not([class]):focus-visible, .markdown-body input[type=radio]:focus, .markdown-body input[type=radio]:focus-visible, .markdown-body input[type=checkbox]:focus, .markdown-body input[type=checkbox]:focus-visible { outline-offset: 0; } .markdown-body kbd { display: inline-block; padding: var(--base-size-4); font: 11px var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); line-height: 10px; color: var(--fgColor-default); vertical-align: middle; background-color: var(--bgColor-muted); border: solid 1px var(--borderColor-neutral-muted); border-bottom-color: var(--borderColor-neutral-muted); border-radius: 6px; box-shadow: inset 0 -1px 0 var(--borderColor-neutral-muted); } .markdown-body h1, .markdown-body h2, .markdown-body h3, .markdown-body h4, .markdown-body h5, .markdown-body h6 { margin-top: var(--base-size-24); margin-bottom: var(--base-size-16); font-weight: var(--base-text-weight-semibold, 600); line-height: 1.25; } .markdown-body h2 { font-weight: var(--base-text-weight-semibold, 600); padding-bottom: .3em; font-size: 1.5em; border-bottom: 1px solid var(--borderColor-muted); } .markdown-body h3 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1.25em; } .markdown-body h4 { font-weight: var(--base-text-weight-semibold, 600); font-size: 1em; } .markdown-body h5 { font-weight: var(--base-text-weight-semibold, 600); font-size: .875em; } .markdown-body h6 { font-weight: var(--base-text-weight-semibold, 600); font-size: .85em; color: var(--fgColor-muted); } .markdown-body p { margin-top: 0; margin-bottom: 10px; } .markdown-body blockquote { margin: 0; padding: 0 1em; color: var(--fgColor-muted); border-left: .25em solid var(--borderColor-default); } .markdown-body ul, .markdown-body ol { margin-top: 0; margin-bottom: 0; padding-left: 2em; } .markdown-body ol ol, .markdown-body ul ol { list-style-type: lower-roman; } .markdown-body ul ul ol, .markdown-body ul ol ol, .markdown-body ol ul ol, .markdown-body ol ol ol { list-style-type: lower-alpha; } .markdown-body dd { margin-left: 0; } .markdown-body tt, .markdown-body code, .markdown-body samp { font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); font-size: 12px; } .markdown-body pre { margin-top: 0; margin-bottom: 0; font-family: var(--fontStack-monospace, ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas, Liberation Mono, monospace); font-size: 12px; word-wrap: normal; } .markdown-body .octicon { display: inline-block; overflow: visible !important; vertical-align: text-bottom; fill: currentColor; } .markdown-body input::-webkit-outer-spin-button, .markdown-body input::-webkit-inner-spin-button { margin: 0; appearance: none; } .markdown-body .mr-2 { margin-right: var(--base-size-8, 8px) !important; } .markdown-body::before { display: table; content: ""; } .markdown-body::after { display: table; clear: both; content: ""; } .markdown-body>*:first-child { margin-top: 0 !important; } .markdown-body>*:last-child { margin-bottom: 0 !important; } .markdown-body a:not([href]) { color: inherit; text-decoration: none; } .markdown-body .absent { color: var(--fgColor-danger); } .markdown-body .anchor { float: left; padding-right: var(--base-size-4); margin-left: -20px; line-height: 1; } .markdown-body .anchor:focus { outline: none; } .markdown-body p, .markdown-body blockquote, .markdown-body ul, .markdown-body ol, .markdown-body dl, .markdown-body table, .markdown-body pre, .markdown-body details { margin-top: 0; margin-bottom: var(--base-size-16); } .markdown-body blockquote>:first-child { margin-top: 0; } .markdown-body blockquote>:last-child { margin-bottom: 0; } .markdown-body h1 .octicon-link, .markdown-body h2 .octicon-link, .markdown-body h3 .octicon-link, .markdown-body h4 .octicon-link, .markdown-body h5 .octicon-link, .markdown-body h6 .octicon-link { color: var(--fgColor-default); vertical-align: middle; visibility: hidden; } .markdown-body h1:hover .anchor, .markdown-body h2:hover .anchor, .markdown-body h3:hover .anchor, .markdown-body h4:hover .anchor, .markdown-body h5:hover .anchor, .markdown-body h6:hover .anchor { text-decoration: none; } .markdown-body h1:hover .anchor .octicon-link, .markdown-body h2:hover .anchor .octicon-link, .markdown-body h3:hover .anchor .octicon-link, .markdown-body h4:hover .anchor .octicon-link, .markdown-body h5:hover .anchor .octicon-link, .markdown-body h6:hover .anchor .octicon-link { visibility: visible; } .markdown-body h1 tt, .markdown-body h1 code, .markdown-body h2 tt, .markdown-body h2 code, .markdown-body h3 tt, .markdown-body h3 code, .markdown-body h4 tt, .markdown-body h4 code, .markdown-body h5 tt, .markdown-body h5 code, .markdown-body h6 tt, .markdown-body h6 code { padding: 0 .2em; font-size: inherit; } .markdown-body summary h1, .markdown-body summary h2, .markdown-body summary h3, .markdown-body summary h4, .markdown-body summary h5, .markdown-body summary h6 { display: inline-block; } .markdown-body summary h1 .anchor, .markdown-body summary h2 .anchor, .markdown-body summary h3 .anchor, .markdown-body summary h4 .anchor, .markdown-body summary h5 .anchor, .markdown-body summary h6 .anchor { margin-left: -40px; } .markdown-body summary h1, .markdown-body summary h2 { padding-bottom: 0; border-bottom: 0; } .markdown-body ul.no-list, .markdown-body ol.no-list { padding: 0; list-style-type: none; } .markdown-body ol[type="a s"] { list-style-type: lower-alpha; } .markdown-body ol[type="A s"] { list-style-type: upper-alpha; } .markdown-body ol[type="i s"] { list-style-type: lower-roman; } .markdown-body ol[type="I s"] { list-style-type: upper-roman; } .markdown-body ol[type="1"] { list-style-type: decimal; } .markdown-body div>ol:not([type]) { list-style-type: decimal; } .markdown-body ul ul, .markdown-body ul ol, .markdown-body ol ol, .markdown-body ol ul { margin-top: 0; margin-bottom: 0; } .markdown-body li>p { margin-top: var(--base-size-16); } .markdown-body li+li { margin-top: .25em; } .markdown-body dl { padding: 0; } .markdown-body dl dt { padding: 0; margin-top: var(--base-size-16); font-size: 1em; font-style: italic; font-weight: var(--base-text-weight-semibold, 600); } .markdown-body dl dd { padding: 0 var(--base-size-16); margin-bottom: var(--base-size-16); } .markdown-body table th { font-weight: var(--base-text-weight-semibold, 600); } .markdown-body table th, .markdown-body table td { padding: 6px 13px; border: 1px solid var(--borderColor-default); } .markdown-body table td>:last-child { margin-bottom: 0; } .markdown-body table tr { background-color: var(--bgColor-default); border-top: 1px solid var(--borderColor-muted); } .markdown-body table tr:nth-child(2n) { background-color: var(--bgColor-muted); } .markdown-body table img { background-color: transparent; } .markdown-body img[align=right] { padding-left: 20px; } .markdown-body img[align=left] { padding-right: 20px; } .markdown-body .emoji { max-width: none; vertical-align: text-top; background-color: transparent; } .markdown-body span.frame { display: block; overflow: hidden; } .markdown-body span.frame>span { display: block; float: left; width: auto; padding: 7px; margin: 13px 0 0; overflow: hidden; border: 1px solid var(--borderColor-default); } .markdown-body span.frame span img { display: block; float: left; } .markdown-body span.frame span span { display: block; padding: 5px 0 0; clear: both; color: var(--fgColor-default); } .markdown-body span.align-center { display: block; overflow: hidden; clear: both; } .markdown-body span.align-center>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: center; } .markdown-body span.align-center span img { margin: 0 auto; text-align: center; } .markdown-body span.align-right { display: block; overflow: hidden; clear: both; } .markdown-body span.align-right>span { display: block; margin: 13px 0 0; overflow: hidden; text-align: right; } .markdown-body span.align-right span img { margin: 0; text-align: right; } .markdown-body span.float-left { display: block; float: left; margin-right: 13px; overflow: hidden; } .markdown-body span.float-left span { margin: 13px 0 0; } .markdown-body span.float-right { display: block; float: right; margin-left: 13px; overflow: hidden; } .markdown-body span.float-right>span { display: block; margin: 13px auto 0; overflow: hidden; text-align: right; } .markdown-body code, .markdown-body tt { padding: .2em .4em; margin: 0; font-size: 85%; white-space: break-spaces; background-color: var(--bgColor-neutral-muted); border-radius: 6px; } .markdown-body code br, .markdown-body tt br { display: none; } .markdown-body del code { text-decoration: inherit; } .markdown-body samp { font-size: 85%; } .markdown-body pre code { font-size: 100%; } .markdown-body pre>code { padding: 0; margin: 0; word-break: normal; white-space: pre; background: transparent; border: 0; } .markdown-body .highlight { margin-bottom: var(--base-size-16); } .markdown-body .highlight pre { margin-bottom: 0; word-break: normal; } .markdown-body .highlight pre, .markdown-body pre { padding: var(--base-size-16); overflow: auto; font-size: 85%; line-height: 1.45; color: var(--fgColor-default); background-color: var(--bgColor-muted); border-radius: 6px; } .markdown-body pre code, .markdown-body pre tt { display: inline; max-width: auto; padding: 0; margin: 0; overflow: visible; line-height: inherit; word-wrap: normal; background-color: transparent; border: 0; } .markdown-body .csv-data td, .markdown-body .csv-data th { padding: 5px; overflow: hidden; font-size: 12px; line-height: 1; text-align: left; white-space: nowrap; } .markdown-body .csv-data .blob-num { padding: 10px var(--base-size-8) 9px; text-align: right; background: var(--bgColor-default); border: 0; } .markdown-body .csv-data tr { border-top: 0; } .markdown-body .csv-data th { font-weight: var(--base-text-weight-semibold, 600); background: var(--bgColor-muted); border-top: 0; } .markdown-body [data-footnote-ref]::before { content: "["; } .markdown-body [data-footnote-ref]::after { content: "]"; } .markdown-body .footnotes { font-size: 12px; color: var(--fgColor-muted); border-top: 1px solid var(--borderColor-default); } .markdown-body .footnotes ol { padding-left: var(--base-size-16); } .markdown-body .footnotes ol ul { display: inline-block; padding-left: var(--base-size-16); margin-top: var(--base-size-16); } .markdown-body .footnotes li { position: relative; } .markdown-body .footnotes li:target::before { position: absolute; top: calc(var(--base-size-8)*-1); right: calc(var(--base-size-8)*-1); bottom: calc(var(--base-size-8)*-1); left: calc(var(--base-size-24)*-1); pointer-events: none; content: ""; border: 2px solid var(--borderColor-accent-emphasis); border-radius: 6px; } .markdown-body .footnotes li:target { color: var(--fgColor-default); } .markdown-body .footnotes .data-footnote-backref g-emoji { font-family: monospace; } .markdown-body body:has(:modal) { padding-right: var(--dialog-scrollgutter) !important; } .markdown-body .pl-c { color: var(--color-prettylights-syntax-comment); } .markdown-body .pl-c1, .markdown-body .pl-s .pl-v { color: var(--color-prettylights-syntax-constant); } .markdown-body .pl-e, .markdown-body .pl-en { color: var(--color-prettylights-syntax-entity); } .markdown-body .pl-smi, .markdown-body .pl-s .pl-s1 { color: var(--color-prettylights-syntax-storage-modifier-import); } .markdown-body .pl-ent { color: var(--color-prettylights-syntax-entity-tag); } .markdown-body .pl-k { color: var(--color-prettylights-syntax-keyword); } .markdown-body .pl-s, .markdown-body .pl-pds, .markdown-body .pl-s .pl-pse .pl-s1, .markdown-body .pl-sr, .markdown-body .pl-sr .pl-cce, .markdown-body .pl-sr .pl-sre, .markdown-body .pl-sr .pl-sra { color: var(--color-prettylights-syntax-string); } .markdown-body .pl-v, .markdown-body .pl-smw { color: var(--color-prettylights-syntax-variable); } .markdown-body .pl-bu { color: var(--color-prettylights-syntax-brackethighlighter-unmatched); } .markdown-body .pl-ii { color: var(--color-prettylights-syntax-invalid-illegal-text); background-color: var(--color-prettylights-syntax-invalid-illegal-bg); } .markdown-body .pl-c2 { color: var(--color-prettylights-syntax-carriage-return-text); background-color: var(--color-prettylights-syntax-carriage-return-bg); } .markdown-body .pl-sr .pl-cce { font-weight: bold; color: var(--color-prettylights-syntax-string-regexp); } .markdown-body .pl-ml { color: var(--color-prettylights-syntax-markup-list); } .markdown-body .pl-mh, .markdown-body .pl-mh .pl-en, .markdown-body .pl-ms { font-weight: bold; color: var(--color-prettylights-syntax-markup-heading); } .markdown-body .pl-mi { font-style: italic; color: var(--color-prettylights-syntax-markup-italic); } .markdown-body .pl-mb { font-weight: bold; color: var(--color-prettylights-syntax-markup-bold); } .markdown-body .pl-md { color: var(--color-prettylights-syntax-markup-deleted-text); background-color: var(--color-prettylights-syntax-markup-deleted-bg); } .markdown-body .pl-mi1 { color: var(--color-prettylights-syntax-markup-inserted-text); background-color: var(--color-prettylights-syntax-markup-inserted-bg); } .markdown-body .pl-mc { color: var(--color-prettylights-syntax-markup-changed-text); background-color: var(--color-prettylights-syntax-markup-changed-bg); } .markdown-body .pl-mi2 { color: var(--color-prettylights-syntax-markup-ignored-text); background-color: var(--color-prettylights-syntax-markup-ignored-bg); } .markdown-body .pl-mdr { font-weight: bold; color: var(--color-prettylights-syntax-meta-diff-range); } .markdown-body .pl-ba { color: var(--color-prettylights-syntax-brackethighlighter-angle); } .markdown-body .pl-sg { color: var(--color-prettylights-syntax-sublimelinter-gutter-mark); } .markdown-body .pl-corl { text-decoration: underline; color: var(--color-prettylights-syntax-constant-other-reference-link); } .markdown-body [role=button]:focus:not(:focus-visible), .markdown-body [role=tabpanel][tabindex="0"]:focus:not(:focus-visible), .markdown-body button:focus:not(:focus-visible), .markdown-body summary:focus:not(:focus-visible), .markdown-body a:focus:not(:focus-visible) { outline: none; box-shadow: none; } .markdown-body [tabindex="0"]:focus:not(:focus-visible), .markdown-body details-dialog:focus:not(:focus-visible) { outline: none; } .markdown-body g-emoji { display: inline-block; min-width: 1ch; font-family: "Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"; font-size: 1em; font-style: normal !important; font-weight: var(--base-text-weight-normal, 400); line-height: 1; vertical-align: -0.075em; } .markdown-body g-emoji img { width: 1em; height: 1em; } .markdown-body .task-list-item { list-style-type: none; } .markdown-body .task-list-item label { font-weight: var(--base-text-weight-normal, 400); } .markdown-body .task-list-item.enabled label { cursor: pointer; } .markdown-body .task-list-item+.task-list-item { margin-top: var(--base-size-4); } .markdown-body .task-list-item .handle { display: none; } .markdown-body .task-list-item-checkbox { margin: 0 .2em .25em -1.4em; vertical-align: middle; } .markdown-body ul:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em .25em .2em; } .markdown-body ol:dir(rtl) .task-list-item-checkbox { margin: 0 -1.6em .25em .2em; } .markdown-body .contains-task-list:hover .task-list-item-convert-container, .markdown-body .contains-task-list:focus-within .task-list-item-convert-container { display: block; width: auto; height: 24px; overflow: visible; clip: auto; } .markdown-body ::-webkit-calendar-picker-indicator { filter: invert(50%); } .markdown-body .markdown-alert { padding: var(--base-size-8) var(--base-size-16); margin-bottom: var(--base-size-16); color: inherit; border-left: .25em solid var(--borderColor-default); } .markdown-body .markdown-alert>:first-child { margin-top: 0; } .markdown-body .markdown-alert>:last-child { margin-bottom: 0; } .markdown-body .markdown-alert .markdown-alert-title { display: flex; font-weight: var(--base-text-weight-medium, 500); align-items: center; line-height: 1; } .markdown-body .markdown-alert.markdown-alert-note { border-left-color: var(--borderColor-accent-emphasis); } .markdown-body .markdown-alert.markdown-alert-note .markdown-alert-title { color: var(--fgColor-accent); } .markdown-body .markdown-alert.markdown-alert-important { border-left-color: var(--borderColor-done-emphasis); } .markdown-body .markdown-alert.markdown-alert-important .markdown-alert-title { color: var(--fgColor-done); } .markdown-body .markdown-alert.markdown-alert-warning { border-left-color: var(--borderColor-attention-emphasis); } .markdown-body .markdown-alert.markdown-alert-warning .markdown-alert-title { color: var(--fgColor-attention); } .markdown-body .markdown-alert.markdown-alert-tip { border-left-color: var(--borderColor-success-emphasis); } .markdown-body .markdown-alert.markdown-alert-tip .markdown-alert-title { color: var(--fgColor-success); } .markdown-body .markdown-alert.markdown-alert-caution { border-left-color: var(--borderColor-danger-emphasis); } .markdown-body .markdown-alert.markdown-alert-caution .markdown-alert-title { color: var(--fgColor-danger); } .markdown-body>*:first-child>.heading-element:first-child { margin-top: 0 !important; } .markdown-body .highlight pre:has(+.zeroclipboard-container) { min-height: 52px; } ================================================ FILE: manager/app/src/main/cpp/CMakeLists.txt ================================================ # For more information about using CMake with Android Studio, read the # documentation: https://d.android.com/studio/projects/add-native-code.html # Sets the minimum version of CMake required to build the native library. cmake_minimum_required(VERSION 3.18.1) project("kernelsu") find_package(cxx REQUIRED CONFIG) link_libraries(cxx::cxx) add_library(kernelsu SHARED jni.cc ksu.cc ) find_library(log-lib log) target_link_libraries(kernelsu ${log-lib}) ================================================ FILE: manager/app/src/main/cpp/jni.cc ================================================ #include #include #include #include #include #include #include #include #include "ksu.h" #include "logging.h" extern "C" JNIEXPORT jint JNICALL Java_me_weishu_kernelsu_Natives_getVersion(JNIEnv *env, jobject) { int version = get_version(); if (version > 0) { return version; } // try legacy method as fallback return legacy_get_info().first; } extern "C" JNIEXPORT jint JNICALL Java_me_weishu_kernelsu_Natives_getSuperuserCount(JNIEnv *env, jobject) { struct ksu_new_get_allow_list_cmd cmd = { .count = 0 }; bool result = get_allow_list(&cmd); return result ? cmd.total_count : 0; } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isSafeMode(JNIEnv *env, jclass clazz) { return is_safe_mode(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isLkmMode(JNIEnv *env, jclass clazz) { return is_lkm_mode(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isLateLoadMode(JNIEnv *env, jclass clazz) { return is_late_load_mode(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isManager(JNIEnv *env, jclass clazz) { return is_manager(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isPrBuild(JNIEnv *env, jclass clazz) { return is_pr_build(); } static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) { auto cls = env->GetObjectClass(list); auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z"); auto integerCls = env->FindClass("java/lang/Integer"); auto constructor = env->GetMethodID(integerCls, "", "(I)V"); for (int i = 0; i < count; ++i) { auto integer = env->NewObject(integerCls, constructor, data[i]); env->CallBooleanMethod(list, add, integer); } } static void addIntToList(JNIEnv *env, jobject list, int ele) { auto cls = env->GetObjectClass(list); auto add = env->GetMethodID(cls, "add", "(Ljava/lang/Object;)Z"); auto integerCls = env->FindClass("java/lang/Integer"); auto constructor = env->GetMethodID(integerCls, "", "(I)V"); auto integer = env->NewObject(integerCls, constructor, ele); env->CallBooleanMethod(list, add, integer); } static uint64_t capListToBits(JNIEnv *env, jobject list) { auto cls = env->GetObjectClass(list); auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;"); auto size = env->GetMethodID(cls, "size", "()I"); auto listSize = env->CallIntMethod(list, size); auto integerCls = env->FindClass("java/lang/Integer"); auto intValue = env->GetMethodID(integerCls, "intValue", "()I"); uint64_t result = 0; for (int i = 0; i < listSize; ++i) { auto integer = env->CallObjectMethod(list, get, i); int data = env->CallIntMethod(integer, intValue); if (cap_valid(data)) { result |= (1ULL << data); } } return result; } static int getListSize(JNIEnv *env, jobject list) { auto cls = env->GetObjectClass(list); auto size = env->GetMethodID(cls, "size", "()I"); return env->CallIntMethod(list, size); } static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) { auto cls = env->GetObjectClass(list); auto get = env->GetMethodID(cls, "get", "(I)Ljava/lang/Object;"); auto integerCls = env->FindClass("java/lang/Integer"); auto intValue = env->GetMethodID(integerCls, "intValue", "()I"); for (int i = 0; i < count; ++i) { auto integer = env->CallObjectMethod(list, get, i); data[i] = env->CallIntMethod(integer, intValue); } } extern "C" JNIEXPORT jobject JNICALL Java_me_weishu_kernelsu_Natives_getAppProfile(JNIEnv *env, jobject, jstring pkg, jint uid) { if (env->GetStringLength(pkg) > KSU_MAX_PACKAGE_NAME) { return nullptr; } p_key_t key = {}; auto cpkg = env->GetStringUTFChars(pkg, nullptr); strcpy(key, cpkg); env->ReleaseStringUTFChars(pkg, cpkg); app_profile profile = {}; profile.version = KSU_APP_PROFILE_VER; strcpy(profile.key, key); profile.current_uid = uid; bool useDefaultProfile = get_app_profile(&profile) != 0; auto cls = env->FindClass("me/weishu/kernelsu/Natives$Profile"); auto constructor = env->GetMethodID(cls, "", "()V"); auto obj = env->NewObject(cls, constructor); auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;"); auto currentUidField = env->GetFieldID(cls, "currentUid", "I"); auto allowSuField = env->GetFieldID(cls, "allowSu", "Z"); auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z"); auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;"); auto uidField = env->GetFieldID(cls, "uid", "I"); auto gidField = env->GetFieldID(cls, "gid", "I"); auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;"); auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;"); auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;"); auto namespacesField = env->GetFieldID(cls, "namespace", "I"); auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z"); auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z"); env->SetObjectField(obj, keyField, env->NewStringUTF(profile.key)); env->SetIntField(obj, currentUidField, profile.current_uid); if (useDefaultProfile) { // no profile found, so just use default profile: // don't allow root and use default profile! LOGD("use default profile for: %s, %d", key, uid); // allow_su = false // non root use default = true env->SetBooleanField(obj, allowSuField, false); env->SetBooleanField(obj, nonRootUseDefaultField, true); return obj; } auto allowSu = profile.allow_su; if (allowSu) { env->SetBooleanField(obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); if (strlen(profile.rp_config.template_name) > 0) { env->SetObjectField(obj, rootTemplateField, env->NewStringUTF(profile.rp_config.template_name)); } env->SetIntField(obj, uidField, profile.rp_config.profile.uid); env->SetIntField(obj, gidField, profile.rp_config.profile.gid); jobject groupList = env->GetObjectField(obj, groupsField); int groupCount = profile.rp_config.profile.groups_count; if (groupCount > KSU_MAX_GROUPS) { LOGD("kernel group count too large: %d???", groupCount); groupCount = KSU_MAX_GROUPS; } fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); jobject capList = env->GetObjectField(obj, capabilitiesField); for (int i = 0; i <= CAP_LAST_CAP; i++) { if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { addIntToList(env, capList, i); } } env->SetObjectField(obj, domainField, env->NewStringUTF(profile.rp_config.profile.selinux_domain)); env->SetIntField(obj, namespacesField, profile.rp_config.profile.namespaces); env->SetBooleanField(obj, allowSuField, profile.allow_su); } else { env->SetBooleanField(obj, nonRootUseDefaultField, (jboolean) profile.nrp_config.use_default); env->SetBooleanField(obj, umountModulesField, profile.nrp_config.profile.umount_modules); } return obj; } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_setAppProfile(JNIEnv *env, jobject clazz, jobject profile) { auto cls = env->FindClass("me/weishu/kernelsu/Natives$Profile"); auto keyField = env->GetFieldID(cls, "name", "Ljava/lang/String;"); auto currentUidField = env->GetFieldID(cls, "currentUid", "I"); auto allowSuField = env->GetFieldID(cls, "allowSu", "Z"); auto rootUseDefaultField = env->GetFieldID(cls, "rootUseDefault", "Z"); auto rootTemplateField = env->GetFieldID(cls, "rootTemplate", "Ljava/lang/String;"); auto uidField = env->GetFieldID(cls, "uid", "I"); auto gidField = env->GetFieldID(cls, "gid", "I"); auto groupsField = env->GetFieldID(cls, "groups", "Ljava/util/List;"); auto capabilitiesField = env->GetFieldID(cls, "capabilities", "Ljava/util/List;"); auto domainField = env->GetFieldID(cls, "context", "Ljava/lang/String;"); auto namespacesField = env->GetFieldID(cls, "namespace", "I"); auto nonRootUseDefaultField = env->GetFieldID(cls, "nonRootUseDefault", "Z"); auto umountModulesField = env->GetFieldID(cls, "umountModules", "Z"); auto key = env->GetObjectField(profile, keyField); if (!key) { return false; } if (env->GetStringLength((jstring) key) > KSU_MAX_PACKAGE_NAME) { return false; } auto cpkg = env->GetStringUTFChars((jstring) key, nullptr); p_key_t p_key = {}; strcpy(p_key, cpkg); env->ReleaseStringUTFChars((jstring) key, cpkg); auto currentUid = env->GetIntField(profile, currentUidField); auto uid = env->GetIntField(profile, uidField); auto gid = env->GetIntField(profile, gidField); auto groups = env->GetObjectField(profile, groupsField); auto capabilities = env->GetObjectField(profile, capabilitiesField); auto domain = env->GetObjectField(profile, domainField); auto allowSu = env->GetBooleanField(profile, allowSuField); auto umountModules = env->GetBooleanField(profile, umountModulesField); app_profile p = {}; p.version = KSU_APP_PROFILE_VER; strcpy(p.key, p_key); p.allow_su = allowSu; p.current_uid = currentUid; if (allowSu) { p.rp_config.use_default = env->GetBooleanField(profile, rootUseDefaultField); auto templateName = env->GetObjectField(profile, rootTemplateField); if (templateName) { auto ctemplateName = env->GetStringUTFChars((jstring) templateName, nullptr); strcpy(p.rp_config.template_name, ctemplateName); env->ReleaseStringUTFChars((jstring) templateName, ctemplateName); } p.rp_config.profile.uid = uid; p.rp_config.profile.gid = gid; int groups_count = getListSize(env, groups); if (groups_count > KSU_MAX_GROUPS) { LOGD("groups count too large: %d", groups_count); return false; } p.rp_config.profile.groups_count = groups_count; fillArrayWithList(env, groups, p.rp_config.profile.groups, groups_count); p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities); auto cdomain = env->GetStringUTFChars((jstring) domain, nullptr); strcpy(p.rp_config.profile.selinux_domain, cdomain); env->ReleaseStringUTFChars((jstring) domain, cdomain); p.rp_config.profile.namespaces = env->GetIntField(profile, namespacesField); } else { p.nrp_config.use_default = env->GetBooleanField(profile, nonRootUseDefaultField); p.nrp_config.profile.umount_modules = umountModules; } return set_app_profile(&p); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_uidShouldUmount(JNIEnv *env, jobject thiz, jint uid) { return uid_should_umount(uid); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isSuEnabled(JNIEnv *env, jobject thiz) { return is_su_enabled(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_setSuEnabled(JNIEnv *env, jobject thiz, jboolean enabled) { return set_su_enabled(enabled); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_isKernelUmountEnabled(JNIEnv *env, jobject thiz) { return is_kernel_umount_enabled(); } extern "C" JNIEXPORT jboolean JNICALL Java_me_weishu_kernelsu_Natives_setKernelUmountEnabled(JNIEnv *env, jobject thiz, jboolean enabled) { return set_kernel_umount_enabled(enabled); } extern "C" JNIEXPORT jstring JNICALL Java_me_weishu_kernelsu_Natives_getUserName(JNIEnv *env, jobject thiz, jint uid) { struct passwd *pw = getpwuid((uid_t) uid); if (pw && pw->pw_name && pw->pw_name[0] != '\0') { return env->NewStringUTF(pw->pw_name); } return nullptr; } int fork_dont_care_and_exec_ksud(const char *path) { int pid = fork(); if (pid < 0) { PLOGE("fork"); return pid; } else if (pid > 0) { int status = 0; if (TEMP_FAILURE_RETRY(waitpid(pid, &status, 0)) < 0) { PLOGE("waitpid"); return -1; } if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) { LOGE("magica bootstrap child failed, status=%d", status); } return pid; } if (setuid(0) != 0) { PLOGE("setuid"); _exit(1); } pid = fork(); if (pid < 0) { PLOGE("fork 2"); _exit(1); } else if (pid > 0) { _exit(0); } execl(path, "ksud", "late-load", "--magica", "5555", nullptr); PLOGE("exec magica"); _exit(1); } extern "C" JNIEXPORT void JNICALL Java_me_weishu_kernelsu_magica_AppZygotePreload_forkDontCareAndExecKsud(JNIEnv *env, jclass clazz, jstring ksud_path) { auto path = env->GetStringUTFChars(ksud_path, nullptr); LOGD("executing magica %s", path); fork_dont_care_and_exec_ksud(path); env->ReleaseStringUTFChars(ksud_path, path); } ================================================ FILE: manager/app/src/main/cpp/ksu.cc ================================================ // // Created by weishu on 2022/12/9. // #include #include #include #include #include #include #include #include #include #include #include #include #include "ksu.h" static int fd = -1; static inline int scan_driver_fd() { const char *kName = "[ksu_driver]"; DIR *dir = opendir("/proc/self/fd"); if (!dir) { return -1; } int found = -1; struct dirent *de; char path[64]; char target[PATH_MAX]; while ((de = readdir(dir)) != NULL) { if (de->d_name[0] == '.') { continue; } char *endptr = NULL; long fd_long = strtol(de->d_name, &endptr, 10); if (!de->d_name[0] || *endptr != '\0' || fd_long < 0 || fd_long > INT_MAX) { continue; } snprintf(path, sizeof(path), "/proc/self/fd/%s", de->d_name); ssize_t n = readlink(path, target, sizeof(target) - 1); if (n < 0) { continue; } target[n] = '\0'; const char *base = strrchr(target, '/'); base = base ? base + 1 : target; if (strstr(base, kName)) { found = (int)fd_long; break; } } closedir(dir); return found; } template static int ksuctl(unsigned long op, Args &&... args) { if (fd < 0) { fd = scan_driver_fd(); } static_assert(sizeof...(Args) <= 1, "ioctl expects at most one extra argument"); return ioctl(fd, op, std::forward(args)...); } static struct ksu_get_info_cmd g_version {}; struct ksu_get_info_cmd get_info() { if (!g_version.version) { ksuctl(KSU_IOCTL_GET_INFO, &g_version); } return g_version; } uint32_t get_version() { auto info = get_info(); return info.version; } bool get_allow_list(struct ksu_new_get_allow_list_cmd *cmd) { return ksuctl(KSU_IOCTL_NEW_GET_ALLOW_LIST, cmd) == 0; } bool is_safe_mode() { struct ksu_check_safemode_cmd cmd = {}; ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &cmd); return cmd.in_safe_mode; } bool is_lkm_mode() { auto info = get_info(); if (info.version > 0) { return (info.flags & KSU_GET_INFO_FLAG_LKM) != 0; } return (legacy_get_info().second & KSU_GET_INFO_FLAG_LKM) != 0; } bool is_late_load_mode() { auto info = get_info(); if (info.version > 0) { return (info.flags & KSU_GET_INFO_FLAG_LATE_LOAD) != 0; } return false; } bool is_manager() { auto info = get_info(); if (info.version > 0) { return (info.flags & KSU_GET_INFO_FLAG_MANAGER) != 0; } return legacy_get_info().first > 0; } bool is_pr_build() { auto info = get_info(); if (info.version > 0) { return (info.flags & KSU_GET_INFO_FLAG_PR_BUILD) != 0; } return false; } bool uid_should_umount(int uid) { struct ksu_uid_should_umount_cmd cmd = {}; cmd.uid = uid; ksuctl(KSU_IOCTL_UID_SHOULD_UMOUNT, &cmd); return cmd.should_umount; } bool set_app_profile(const app_profile *profile) { struct ksu_set_app_profile_cmd cmd = {}; cmd.profile = *profile; return ksuctl(KSU_IOCTL_SET_APP_PROFILE, &cmd) == 0; } int get_app_profile(app_profile *profile) { struct ksu_get_app_profile_cmd cmd = {.profile = *profile}; int ret = ksuctl(KSU_IOCTL_GET_APP_PROFILE, &cmd); *profile = cmd.profile; return ret; } bool set_su_enabled(bool enabled) { struct ksu_set_feature_cmd cmd = {}; cmd.feature_id = KSU_FEATURE_SU_COMPAT; cmd.value = enabled ? 1 : 0; return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0; } bool is_su_enabled() { struct ksu_get_feature_cmd cmd = {}; cmd.feature_id = KSU_FEATURE_SU_COMPAT; if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) { return false; } if (!cmd.supported) { return false; } return cmd.value != 0; } static inline bool get_feature(uint32_t feature_id, uint64_t *out_value, bool *out_supported) { struct ksu_get_feature_cmd cmd = {}; cmd.feature_id = feature_id; if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) != 0) { return false; } if (out_value) *out_value = cmd.value; if (out_supported) *out_supported = cmd.supported; return true; } static inline bool set_feature(uint32_t feature_id, uint64_t value) { struct ksu_set_feature_cmd cmd = {}; cmd.feature_id = feature_id; cmd.value = value; return ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0; } bool set_kernel_umount_enabled(bool enabled) { return set_feature(KSU_FEATURE_KERNEL_UMOUNT, enabled ? 1 : 0); } bool is_kernel_umount_enabled() { uint64_t value = 0; bool supported = false; if (!get_feature(KSU_FEATURE_KERNEL_UMOUNT, &value, &supported)) { return false; } if (!supported) { return false; } return value != 0; } ================================================ FILE: manager/app/src/main/cpp/ksu.h ================================================ // // Created by weishu on 2022/12/9. // #ifndef KERNELSU_KSU_H #define KERNELSU_KSU_H #include #include #include uint32_t get_version(); bool uid_should_umount(int uid); bool is_safe_mode(); bool is_lkm_mode(); bool is_late_load_mode(); bool is_manager(); bool is_pr_build(); #define KSU_APP_PROFILE_VER 2 #define KSU_MAX_PACKAGE_NAME 256 // NGROUPS_MAX for Linux is 65535 generally, but we only supports 32 groups. #define KSU_MAX_GROUPS 32 #define KSU_SELINUX_DOMAIN 64 using p_key_t = char[KSU_MAX_PACKAGE_NAME]; struct root_profile { int32_t uid; int32_t gid; int32_t groups_count; int32_t groups[KSU_MAX_GROUPS]; // kernel_cap_t is u32[2] for capabilities v3 struct { uint64_t effective; uint64_t permitted; uint64_t inheritable; } capabilities; char selinux_domain[KSU_SELINUX_DOMAIN]; int32_t namespaces; }; struct non_root_profile { bool umount_modules; }; struct app_profile { // It may be utilized for backward compatibility, although we have never explicitly made any promises regarding this. uint32_t version; // this is usually the package of the app, but can be other value for special apps char key[KSU_MAX_PACKAGE_NAME]; int32_t current_uid; bool allow_su; union { struct { bool use_default; char template_name[KSU_MAX_PACKAGE_NAME]; struct root_profile profile; } rp_config; struct { bool use_default; struct non_root_profile profile; } nrp_config; }; }; bool set_app_profile(const app_profile *profile); int get_app_profile(app_profile *profile); // Feature IDs enum ksu_feature_id { KSU_FEATURE_SU_COMPAT = 0, KSU_FEATURE_KERNEL_UMOUNT = 1, }; // Generic feature API struct ksu_get_feature_cmd { uint32_t feature_id; // Input: feature ID uint64_t value; // Output: feature value/state uint8_t supported; // Output: whether the feature is supported }; struct ksu_set_feature_cmd { uint32_t feature_id; // Input: feature ID uint64_t value; // Input: feature value/state to set }; struct ksu_become_daemon_cmd { uint8_t token[65]; // Input: daemon token (null-terminated) }; enum ksu_get_info_flag : uint32_t { KSU_GET_INFO_FLAG_LKM = 1U << 0, KSU_GET_INFO_FLAG_MANAGER = 1U << 1, KSU_GET_INFO_FLAG_LATE_LOAD = 1U << 2, KSU_GET_INFO_FLAG_PR_BUILD = 1U << 3, }; struct ksu_get_info_cmd { uint32_t version; // Output: KERNEL_SU_VERSION uint32_t flags; // Output: KSU_GET_INFO_FLAG_* bits uint32_t features; // Output: max feature ID supported (KSU_FEATURE_MAX) }; struct ksu_report_event_cmd { uint32_t event; // Input: EVENT_POST_FS_DATA, EVENT_BOOT_COMPLETED, etc. }; struct ksu_set_sepolicy_cmd { uint64_t cmd; // Input: sepolicy command uint64_t arg; // Input: sepolicy argument pointer }; struct ksu_check_safemode_cmd { uint8_t in_safe_mode; // Output: true if in safe mode, false otherwise }; struct ksu_new_get_allow_list_cmd { uint16_t count; // Input / Output: number of UIDs in array uint16_t total_count; // Output: total number of UIDs in requested list uint32_t uids[0]; // Output: array of allowed/denied UIDs }; struct ksu_uid_granted_root_cmd { uint32_t uid; // Input: target UID to check uint8_t granted; // Output: true if granted, false otherwise }; struct ksu_uid_should_umount_cmd { uint32_t uid; // Input: target UID to check uint8_t should_umount; // Output: true if should umount, false otherwise }; struct ksu_get_manager_appid_cmd { uint32_t appid; // Output: manager app id }; struct ksu_get_app_profile_cmd { struct app_profile profile; // Input/Output: app profile structure }; struct ksu_set_app_profile_cmd { struct app_profile profile; // Input: app profile structure }; // Su compat bool set_su_enabled(bool enabled); bool is_su_enabled(); // Kernel umount bool set_kernel_umount_enabled(bool enabled); bool is_kernel_umount_enabled(); // IOCTL command definitions #define KSU_IOCTL_GRANT_ROOT _IOC(_IOC_NONE, 'K', 1, 0) #define KSU_IOCTL_GET_INFO _IOC(_IOC_READ, 'K', 2, 0) #define KSU_IOCTL_REPORT_EVENT _IOC(_IOC_WRITE, 'K', 3, 0) #define KSU_IOCTL_SET_SEPOLICY _IOC(_IOC_READ|_IOC_WRITE, 'K', 4, 0) #define KSU_IOCTL_CHECK_SAFEMODE _IOC(_IOC_READ, 'K', 5, 0) #define KSU_IOCTL_NEW_GET_ALLOW_LIST _IOWR('K', 6, struct ksu_new_get_allow_list_cmd) #define KSU_IOCTL_NEW_GET_DENY_LIST _IOWR('K', 7, struct ksu_new_get_allow_list_cmd) #define KSU_IOCTL_UID_GRANTED_ROOT _IOC(_IOC_READ|_IOC_WRITE, 'K', 8, 0) #define KSU_IOCTL_UID_SHOULD_UMOUNT _IOC(_IOC_READ|_IOC_WRITE, 'K', 9, 0) #define KSU_IOCTL_GET_MANAGER_APPID _IOC(_IOC_READ, 'K', 10, 0) #define KSU_IOCTL_GET_APP_PROFILE _IOC(_IOC_READ|_IOC_WRITE, 'K', 11, 0) #define KSU_IOCTL_SET_APP_PROFILE _IOC(_IOC_WRITE, 'K', 12, 0) #define KSU_IOCTL_GET_FEATURE _IOC(_IOC_READ|_IOC_WRITE, 'K', 13, 0) #define KSU_IOCTL_SET_FEATURE _IOC(_IOC_WRITE, 'K', 14, 0) bool get_allow_list(struct ksu_new_get_allow_list_cmd *); inline std::pair legacy_get_info() { int32_t version = -1; int32_t flags = 0; int32_t result = 0; prctl(0xDEADBEEF, 2, &version, &flags, &result); return {version, flags}; } #endif //KERNELSU_KSU_H ================================================ FILE: manager/app/src/main/cpp/logging.h ================================================ #pragma once #include #include #include #include #ifndef LOG_TAG # define LOG_TAG "KernelSU" #endif #ifndef NDEBUG #define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__) #define LOGV(...) __android_log_print(ANDROID_LOG_VERBOSE, LOG_TAG, __VA_ARGS__) #else #define LOGD(...) #define LOGV(...) #endif #define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__) #define LOGW(...) __android_log_print(ANDROID_LOG_WARN, LOG_TAG, __VA_ARGS__) #define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__) #define LOGF(...) __android_log_print(ANDROID_LOG_FATAL, LOG_TAG, __VA_ARGS__) #define PLOGE(fmt, args...) LOGE(fmt " failed with %d: %s", ##args, errno, strerror(errno)) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/KernelSUApplication.kt ================================================ package me.weishu.kernelsu import android.app.Application import android.content.pm.ApplicationInfo import android.os.Build import android.system.Os import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import okhttp3.Cache import okhttp3.OkHttpClient import org.lsposed.hiddenapibypass.HiddenApiBypass import java.io.File import java.util.Locale lateinit var ksuApp: KernelSUApplication class KernelSUApplication : Application(), ViewModelStoreOwner { companion object { fun setEnableOnBackInvokedCallback(appInfo: ApplicationInfo, enable: Boolean) { runCatching { val applicationInfoClass = ApplicationInfo::class.java val method = applicationInfoClass.getDeclaredMethod("setEnableOnBackInvokedCallback", Boolean::class.javaPrimitiveType) method.isAccessible = true method.invoke(appInfo, enable) } } } lateinit var okhttpClient: OkHttpClient private val appViewModelStore by lazy { ViewModelStore() } override fun onCreate() { super.onCreate() ksuApp = this if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { val prefs = this.getSharedPreferences("settings", MODE_PRIVATE) val enable = prefs.getBoolean("enable_predictive_back", false) HiddenApiBypass.addHiddenApiExemptions("Landroid/content/pm/ApplicationInfo;->setEnableOnBackInvokedCallback") setEnableOnBackInvokedCallback(applicationInfo, enable) } val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java] superUserViewModel.loadAppList() val webroot = File(dataDir, "webroot") if (!webroot.exists()) { webroot.mkdir() } // Provide working env for rust's temp_dir() Os.setenv("TMPDIR", cacheDir.absolutePath, true) okhttpClient = OkHttpClient.Builder().cache(Cache(File(cacheDir, "okhttp"), 10 * 1024 * 1024)) .addInterceptor { block -> block.proceed( block.request().newBuilder() .header("User-Agent", "KernelSU/${BuildConfig.VERSION_CODE}") .header("Accept-Language", Locale.getDefault().toLanguageTag()).build() ) }.build() } override val viewModelStore: ViewModelStore get() = appViewModelStore } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/Kernels.kt ================================================ package me.weishu.kernelsu import android.system.Os /** * @author weishu * @date 2022/12/10. */ data class KernelVersion(val major: Int, val patchLevel: Int, val subLevel: Int) { override fun toString(): String { return "$major.$patchLevel.$subLevel" } fun isGKI(): Boolean { // kernel 6.x if (major > 5) { return true } // kernel 5.10.x if (major == 5) { return patchLevel >= 10 } return false } } fun parseKernelVersion(version: String): KernelVersion { val find = "(\\d+)\\.(\\d+)\\.(\\d+)".toRegex().find(version) return if (find != null) { KernelVersion(find.groupValues[1].toInt(), find.groupValues[2].toInt(), find.groupValues[3].toInt()) } else { KernelVersion(-1, -1, -1) } } fun getKernelVersion(): KernelVersion { Os.uname().release.let { return parseKernelVersion(it) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/Natives.kt ================================================ package me.weishu.kernelsu import android.os.Parcelable import androidx.annotation.Keep import androidx.compose.runtime.Immutable import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable /** * @author weishu * @date 2022/12/8. */ object Natives { // minimal supported kernel version // 10915: allowlist breaking change, add app profile // 10931: app profile struct add 'version' field // 10946: add capabilities // 10977: change groups_count and groups to avoid overflow write // 11071: Fix the issue of failing to set a custom SELinux type. // 12143: breaking: new supercall impl // 32310: new get_allow_list ioctl // 32336: new set_sepolicy ioctl const val MINIMAL_SUPPORTED_KERNEL = 32336 const val KERNEL_SU_DOMAIN = "u:r:su:s0" const val ROOT_UID = 0 const val ROOT_GID = 0 init { System.loadLibrary("kernelsu") } val version: Int external get val isSafeMode: Boolean external get val isLkmMode: Boolean external get val isLateLoadMode: Boolean external get val isManager: Boolean external get val isPrBuild: Boolean external get external fun uidShouldUmount(uid: Int): Boolean /** * Get the profile of the given package. * @param key usually the package name * @return return null if failed. */ external fun getAppProfile(key: String?, uid: Int): Profile external fun setAppProfile(profile: Profile?): Boolean /** * `su` compat mode can be disabled temporarily. * 0: disabled * 1: enabled * negative : error */ external fun isSuEnabled(): Boolean external fun setSuEnabled(enabled: Boolean): Boolean /** * Kernel module umount can be disabled temporarily. * 0: disabled * 1: enabled * negative : error */ external fun isKernelUmountEnabled(): Boolean external fun setKernelUmountEnabled(enabled: Boolean): Boolean /** * Get the user name for the uid. */ external fun getUserName(uid: Int): String? external fun getSuperuserCount(): Int private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NOBODY_UID = 9999 fun setDefaultUmountModules(umountModules: Boolean): Boolean { Profile( NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID, false, umountModules = umountModules ).let { return setAppProfile(it) } } fun isDefaultUmountModules(): Boolean { getAppProfile(NON_ROOT_DEFAULT_PROFILE_KEY, NOBODY_UID).let { return it.umountModules } } fun requireNewKernel(): Boolean { return version != -1 && version < MINIMAL_SUPPORTED_KERNEL } @Keep @Immutable @Parcelize @Serializable data class Profile( // and there is a default profile for root and non-root val name: String, // current uid for the package, this is convivent for kernel to check // if the package name doesn't match uid, then it should be invalidated. val currentUid: Int = 0, // if this is true, kernel will grant root permission to this package val allowSu: Boolean = false, // these are used for root profile val rootUseDefault: Boolean = true, val rootTemplate: String? = null, val uid: Int = ROOT_UID, val gid: Int = ROOT_GID, val groups: List = mutableListOf(), val capabilities: List = mutableListOf(), val context: String = KERNEL_SU_DOMAIN, val namespace: Int = Namespace.INHERITED.ordinal, val nonRootUseDefault: Boolean = true, val umountModules: Boolean = true, var rules: String = "", // this field is save in ksud!! ) : Parcelable { enum class Namespace { INHERITED, GLOBAL, INDIVIDUAL, } constructor() : this("") } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/model/AppInfo.kt ================================================ package me.weishu.kernelsu.data.model import android.content.pm.PackageInfo import android.os.Parcelable import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.Natives @Parcelize data class AppInfo( val label: String, 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 hasCustomProfile: Boolean get() { if (profile == null) { return false } return if (profile.allowSu) { !profile.rootUseDefault } else { !profile.nonRootUseDefault } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/model/Module.kt ================================================ package me.weishu.kernelsu.data.model import androidx.compose.runtime.Immutable @Immutable data class Module( val id: String, val name: String, val author: String, val version: String, val versionCode: Int, val description: String, val enabled: Boolean, val update: Boolean, val remove: Boolean, val updateJson: String, val hasWebUi: Boolean, val hasActionScript: Boolean, val metamodule: Boolean, val actionIconPath: String?, val webUiIconPath: String?, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/model/ModuleUpdateInfo.kt ================================================ package me.weishu.kernelsu.data.model import androidx.compose.runtime.Immutable @Immutable data class ModuleUpdateInfo( val downloadUrl: String, val version: String, val changelog: String ) { companion object { val Empty = ModuleUpdateInfo("", "", "") } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/model/RepoModule.kt ================================================ package me.weishu.kernelsu.data.model import androidx.compose.runtime.Immutable @Immutable data class Author( val name: String, val link: String, ) @Immutable data class ReleaseAsset( val name: String, val downloadUrl: String, val size: Long ) @Immutable data class RepoModule( val moduleId: String, val moduleName: String, val authors: String, val authorList: List, val summary: String, val metamodule: Boolean, val stargazerCount: Int, val updatedAt: String, val createdAt: String, val latestRelease: String, val latestReleaseTime: String, val latestVersionCode: Int, val latestAsset: ReleaseAsset?, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/model/TemplateInfo.kt ================================================ package me.weishu.kernelsu.data.model import android.os.Parcelable import android.util.Log import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.Natives import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups import org.json.JSONArray import org.json.JSONObject import java.util.Locale @Parcelize data class TemplateInfo( val id: String = "", val name: String = "", val description: String = "", val author: String = "", val local: Boolean = true, val namespace: Int = Natives.Profile.Namespace.INHERITED.ordinal, val uid: Int = Natives.ROOT_UID, val gid: Int = Natives.ROOT_GID, val groups: List = mutableListOf(), val capabilities: List = mutableListOf(), val context: String = Natives.KERNEL_SU_DOMAIN, val rules: List = mutableListOf(), ) : Parcelable { companion object { private const val TAG = "TemplateInfo" fun fromJSON(templateJson: JSONObject): TemplateInfo? { return runCatching { val groupsJsonArray = templateJson.optJSONArray("groups") val capabilitiesJsonArray = templateJson.optJSONArray("capabilities") val context = templateJson.optString("context").takeIf { it.isNotEmpty() } ?: Natives.KERNEL_SU_DOMAIN val namespace = templateJson.optString("namespace").takeIf { it.isNotEmpty() } ?: Natives.Profile.Namespace.INHERITED.name val rulesJsonArray = templateJson.optJSONArray("rules") val templateInfo = TemplateInfo( id = templateJson.getString("id"), name = getLocaleString(templateJson, "name"), description = getLocaleString(templateJson, "description"), author = templateJson.optString("author"), local = templateJson.optBoolean("local"), namespace = Natives.Profile.Namespace.valueOf( namespace.uppercase() ).ordinal, uid = templateJson.optInt("uid", Natives.ROOT_UID), gid = templateJson.optInt("gid", Natives.ROOT_GID), groups = getEnumOrdinals(groupsJsonArray, Groups::class.java).map { it.gid }, capabilities = getEnumOrdinals( capabilitiesJsonArray, Capabilities::class.java ).map { it.cap }, context = context, rules = rulesJsonArray?.mapCatching({ it }, { Log.e(TAG, "ignore invalid rule: $it", it) }).orEmpty() ) templateInfo }.onFailure { Log.e(TAG, "ignore invalid template: $it", it) }.getOrNull() } private fun getLocaleString(json: JSONObject, key: String): String { val fallback = json.getString(key) val locale = Locale.getDefault() val localeKey = "${locale.language}_${locale.country}" json.optJSONObject("locales")?.let { // check locale first it.optJSONObject(localeKey)?.let { json -> return json.optString(key, fallback) } // fallback to language it.optJSONObject(locale.language)?.let { json -> return json.optString(key, fallback) } } return fallback } @Suppress("UNCHECKED_CAST") private fun JSONArray.mapCatching( transform: (T) -> R, onFail: (Throwable) -> Unit ): List { return List(length()) { i -> get(i) as T }.mapNotNull { element -> runCatching { transform(element) }.onFailure(onFail).getOrNull() } } private inline fun > getEnumOrdinals( jsonArray: JSONArray?, enumClass: Class ): List { return jsonArray?.mapCatching({ name -> enumValueOf(name.uppercase()) }, { Log.e(TAG, "ignore invalid enum ${enumClass.simpleName}: $it", it) }).orEmpty() } } fun toJSON(): JSONObject { val template = this return JSONObject().apply { put("id", template.id) put("name", template.name.ifBlank { template.id }) put("description", template.description.ifBlank { template.id }) if (template.author.isNotEmpty()) { put("author", template.author) } put("namespace", Natives.Profile.Namespace.entries[template.namespace].name) put("uid", template.uid) put("gid", template.gid) if (template.groups.isNotEmpty()) { put( "groups", JSONArray( Groups.entries.filter { template.groups.contains(it.gid) }.map { it.name } )) } if (template.capabilities.isNotEmpty()) { put( "capabilities", JSONArray( Capabilities.entries.filter { template.capabilities.contains(it.cap) }.map { it.name } )) } if (template.context.isNotEmpty()) { put("context", template.context) } if (template.rules.isNotEmpty()) { put("rules", JSONArray(template.rules)) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepoRepository.kt ================================================ package me.weishu.kernelsu.data.repository import me.weishu.kernelsu.data.model.RepoModule interface ModuleRepoRepository { suspend fun fetchModules(): Result> } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepoRepositoryImpl.kt ================================================ package me.weishu.kernelsu.data.repository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.data.model.Author import me.weishu.kernelsu.data.model.ReleaseAsset import me.weishu.kernelsu.data.model.RepoModule import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.util.isNetworkAvailable import okhttp3.Request import org.json.JSONArray import org.json.JSONObject class ModuleRepoRepositoryImpl : ModuleRepoRepository { companion object { private const val MODULES_URL = "https://modules.kernelsu.org/modules.json" } override suspend fun fetchModules(): Result> = withContext(Dispatchers.IO) { runCatching { if (!isNetworkAvailable(ksuApp)) { throw Exception("Network unavailable") } val request = Request.Builder().url(MODULES_URL).build() ksuApp.okhttpClient.newCall(request).execute().use { response -> if (!response.isSuccessful) { throw Exception("Fetch failed: ${response.code}") } val body = response.body.string() val json = JSONArray(body) (0 until json.length()).mapNotNull { idx -> val item = json.optJSONObject(idx) ?: return@mapNotNull null parseRepoModule(item) } } } } private fun parseRepoModule(item: JSONObject): RepoModule? { val moduleId = item.optString("moduleId", "") if (moduleId.isEmpty()) return null val moduleName = item.optString("moduleName", "") val authorsArray = item.optJSONArray("authors") val authorList = if (authorsArray != null) { (0 until authorsArray.length()) .mapNotNull { idx -> val authorObj = authorsArray.optJSONObject(idx) ?: return@mapNotNull null val name = authorObj.optString("name", "").trim() var link = authorObj.optString("link", "").trim() if (link.startsWith("`") && link.endsWith("`") && link.length >= 2) { link = link.substring(1, link.length - 1) } if (name.isEmpty()) null else Author(name = name, link = link) } } else { emptyList() } val authors = if (authorList.isNotEmpty()) authorList.joinToString(", ") { it.name } else item.optString("authors", "") val summary = item.optString("summary", "") val metamodule = item.optBoolean("metamodule", false) val stargazerCount = item.optInt("stargazerCount", 0) val updatedAt = item.optString("updatedAt", "") val createdAt = item.optString("createdAt", "") var latestRelease = "" var latestReleaseTime = "" var latestVersionCode = 0 var latestAsset: ReleaseAsset? = null val lr = item.optJSONObject("latestRelease") if (lr != null) { val lrName = lr.optString("name", lr.optString("version", "")) val lrTime = lr.optString("time", "") var lrUrl = lr.optString("downloadUrl", "") lrUrl = lrUrl.trim().let { var s = it if (s.startsWith("`") && s.endsWith("`") && s.length >= 2) { s = s.substring(1, s.length - 1) } s } val vcAny = lr.opt("versionCode") latestVersionCode = when (vcAny) { is Number -> vcAny.toInt() is String -> vcAny.toIntOrNull() ?: 0 else -> 0 } latestRelease = lrName latestReleaseTime = lrTime if (lrUrl.isNotEmpty()) { val fileName = lrUrl.substringAfterLast('/') latestAsset = ReleaseAsset(name = fileName, downloadUrl = lrUrl, size = 0L) } } return RepoModule( moduleId = moduleId, moduleName = moduleName, authors = authors, authorList = authorList, summary = summary, metamodule = metamodule, stargazerCount = stargazerCount, updatedAt = updatedAt, createdAt = createdAt, latestRelease = latestRelease, latestReleaseTime = latestReleaseTime, latestVersionCode = latestVersionCode, latestAsset = latestAsset, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepository.kt ================================================ package me.weishu.kernelsu.data.repository import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo interface ModuleRepository { suspend fun getModules(): Result> suspend fun checkUpdate(module: Module): Result } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/ModuleRepositoryImpl.kt ================================================ package me.weishu.kernelsu.data.repository import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.util.isNetworkAvailable import me.weishu.kernelsu.ui.util.listModules import me.weishu.kernelsu.ui.util.module.sanitizeVersionString import okhttp3.Request import org.json.JSONArray import org.json.JSONObject class ModuleRepositoryImpl : ModuleRepository { companion object { private const val TAG = "ModuleRepository" } override suspend fun getModules(): Result> = withContext(Dispatchers.IO) { runCatching { val result = listModules() val array = JSONArray(result) (0 until array.length()) .asSequence() .map { array.getJSONObject(it) } .map { obj -> Module( id = obj.getString("id"), name = obj.optString("name"), author = obj.optString("author", "Unknown"), version = obj.optString("version", "Unknown"), versionCode = obj.optInt("versionCode", 0), description = obj.optString("description"), enabled = obj.getBoolean("enabled"), update = obj.optBoolean("update"), remove = obj.getBoolean("remove"), updateJson = obj.optString("updateJson"), hasWebUi = obj.optBoolean("web"), hasActionScript = obj.optBoolean("action"), metamodule = (obj.optInt("metamodule") != 0) || obj.optBoolean("metamodule"), actionIconPath = obj.optString("actionIcon").takeIf { it.isNotBlank() }, webUiIconPath = obj.optString("webuiIcon").takeIf { it.isNotBlank() } ) }.toList() } } override suspend fun checkUpdate(module: Module): Result = withContext(Dispatchers.IO) { runCatching { if (!isNetworkAvailable(ksuApp)) { return@runCatching ModuleUpdateInfo.Empty } if (module.updateJson.isEmpty() || module.remove || module.update || !module.enabled) { return@runCatching ModuleUpdateInfo.Empty } val url = module.updateJson val response = ksuApp.okhttpClient.newCall( Request.Builder().url(url).build() ).execute() val result = if (response.isSuccessful) { response.body.string() } else { "" } if (result.isEmpty()) { return@runCatching ModuleUpdateInfo.Empty } val updateJson = JSONObject(result) var version = updateJson.optString("version", "") version = sanitizeVersionString(version) val versionCode = updateJson.optInt("versionCode", 0) val zipUrl = updateJson.optString("zipUrl", "") val changelog = updateJson.optString("changelog", "") if (versionCode <= module.versionCode || zipUrl.isEmpty()) { ModuleUpdateInfo.Empty } else { ModuleUpdateInfo(zipUrl, version, changelog) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/SettingsRepository.kt ================================================ package me.weishu.kernelsu.data.repository interface SettingsRepository { var uiMode: String var checkUpdate: Boolean var checkModuleUpdate: Boolean var themeMode: Int var miuixMonet: Boolean var keyColor: Int var colorStyle: String var colorSpec: String var enablePredictiveBack: Boolean var enableBlur: Boolean var enableFloatingBottomBar: Boolean var enableFloatingBottomBarBlur: Boolean var pageScale: Float var enableWebDebugging: Boolean var autoJailbreak: Boolean suspend fun getSuCompatStatus(): String suspend fun getSuCompatPersistValue(): Long? fun isSuEnabled(): Boolean fun setSuEnabled(enabled: Boolean): Boolean fun setSuCompatModePref(mode: Int) fun getSuCompatModePref(): Int suspend fun getKernelUmountStatus(): String fun isKernelUmountEnabled(): Boolean fun setKernelUmountEnabled(enabled: Boolean): Boolean fun isDefaultUmountModules(): Boolean fun setDefaultUmountModules(enabled: Boolean): Boolean fun isLkmMode(): Boolean fun execKsudFeatureSave() } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/SettingsRepositoryImpl.kt ================================================ package me.weishu.kernelsu.data.repository import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.util.Log import androidx.core.content.edit import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.magica.BootCompletedReceiver import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.util.execKsud import me.weishu.kernelsu.ui.util.getFeaturePersistValue import me.weishu.kernelsu.ui.util.getFeatureStatus class SettingsRepositoryImpl : SettingsRepository { private val prefs by lazy { ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE) } override var uiMode: String get() = prefs.getString("ui_mode", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE set(value) = prefs.edit { putString("ui_mode", value) } override var checkUpdate: Boolean get() = prefs.getBoolean("check_update", true) set(value) = prefs.edit { putBoolean("check_update", value) } override var checkModuleUpdate: Boolean get() = prefs.getBoolean("module_check_update", true) set(value) = prefs.edit { putBoolean("module_check_update", value) } override var themeMode: Int get() = prefs.getInt("color_mode", 0) set(value) = prefs.edit { putInt("color_mode", value) } override var miuixMonet: Boolean get() = prefs.getBoolean("miuix_monet", false) set(value) = prefs.edit { putBoolean("miuix_monet", value) } override var keyColor: Int get() = prefs.getInt("key_color", 0) set(value) = prefs.edit { putInt("key_color", value) } override var colorStyle: String get() = prefs.getString("color_style", PaletteStyle.TonalSpot.name) ?: PaletteStyle.TonalSpot.name set(value) = prefs.edit { putString("color_style", value) } override var colorSpec: String get() = prefs.getString("color_spec", ColorSpec.SpecVersion.Default.name) ?: ColorSpec.SpecVersion.Default.name set(value) = prefs.edit { putString("color_spec", value) } override var enablePredictiveBack: Boolean get() = prefs.getBoolean("enable_predictive_back", false) set(value) = prefs.edit { putBoolean("enable_predictive_back", value) } override var enableBlur: Boolean get() = prefs.getBoolean("enable_blur", false) set(value) = prefs.edit { putBoolean("enable_blur", value) } override var enableFloatingBottomBar: Boolean get() = prefs.getBoolean("enable_floating_bottom_bar", false) set(value) = prefs.edit { putBoolean("enable_floating_bottom_bar", value) } override var enableFloatingBottomBarBlur: Boolean get() = prefs.getBoolean("enable_floating_bottom_bar_blur", false) set(value) = prefs.edit { putBoolean("enable_floating_bottom_bar_blur", value) } override var pageScale: Float get() = prefs.getFloat("page_scale", 1.0f) set(value) = prefs.edit { putFloat("page_scale", value) } override var enableWebDebugging: Boolean get() = prefs.getBoolean("enable_web_debugging", false) set(value) = prefs.edit { putBoolean("enable_web_debugging", value) } override var autoJailbreak: Boolean get() = prefs.getBoolean("auto_jailbreak", false) set(value) { runCatching { ksuApp.packageManager.setComponentEnabledSetting( ComponentName(ksuApp, BootCompletedReceiver::class.java), if (value) PackageManager.COMPONENT_ENABLED_STATE_ENABLED else PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP ) }.onFailure { Log.e("Settings", "failed to change boot receiver state to $value", it) } prefs.edit { putBoolean("auto_jailbreak", value) } } override suspend fun getSuCompatStatus(): String = getFeatureStatus("su_compat") override suspend fun getSuCompatPersistValue(): Long? = getFeaturePersistValue("su_compat") override fun isSuEnabled(): Boolean = Natives.isSuEnabled() override fun setSuEnabled(enabled: Boolean): Boolean = Natives.setSuEnabled(enabled) override fun setSuCompatModePref(mode: Int) = prefs.edit { putInt("su_compat_mode", mode) } override fun getSuCompatModePref(): Int = prefs.getInt("su_compat_mode", 0) override suspend fun getKernelUmountStatus(): String = getFeatureStatus("kernel_umount") override fun isKernelUmountEnabled(): Boolean = Natives.isKernelUmountEnabled() override fun setKernelUmountEnabled(enabled: Boolean): Boolean = Natives.setKernelUmountEnabled(enabled) override fun isDefaultUmountModules(): Boolean = Natives.isDefaultUmountModules() override fun setDefaultUmountModules(enabled: Boolean): Boolean = Natives.setDefaultUmountModules(enabled) override fun isLkmMode(): Boolean = Natives.isLkmMode override fun execKsudFeatureSave() { execKsud("feature save", true) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/SuperUserRepository.kt ================================================ package me.weishu.kernelsu.data.repository import me.weishu.kernelsu.data.model.AppInfo interface SuperUserRepository { suspend fun getAppList(): Result, List>> suspend fun refreshProfiles(currentApps: List): Result> } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/SuperUserRepositoryImpl.kt ================================================ package me.weishu.kernelsu.data.repository import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection import android.os.Handler import android.os.IBinder import android.os.Looper import android.os.SystemClock import android.util.Log import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ipc.RootService import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext import me.weishu.kernelsu.IKsuInterface import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.model.AppInfo import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.KsuService import me.weishu.kernelsu.ui.util.KsuCli import kotlin.coroutines.resume class SuperUserRepositoryImpl : SuperUserRepository { companion object { private const val TAG = "SuperUserRepository" } override suspend fun getAppList(): Result, List>> = withContext(Dispatchers.IO) { runCatching { val result = connectKsuService { Log.w(TAG, "KsuService disconnected") } var currentBinder = result.first var currentConnection = result.second try { suspend fun reconnect(): IKsuInterface { withContext(Dispatchers.Main) { RootService.unbind(currentConnection) } val retry = connectKsuService { Log.w(TAG, "KsuService disconnected") } currentBinder = retry.first currentConnection = retry.second return IKsuInterface.Stub.asInterface(currentBinder) } val pm = ksuApp.packageManager val start = SystemClock.elapsedRealtime() var iface = IKsuInterface.Stub.asInterface(currentBinder) val idsArray = try { iface.userIds } catch (_: Exception) { iface = reconnect() iface.userIds } val slice = try { iface.getPackages(0) } catch (_: Exception) { iface = reconnect() iface.getPackages(0) } val packages = slice.list val newApps = packages.map { val appInfo = it.applicationInfo val uid = appInfo!!.uid val profile = Natives.getAppProfile(it.packageName, uid) AppInfo( label = appInfo.loadLabel(pm).toString(), packageInfo = it, profile = profile, ) }.filter { val ai = it.packageInfo.applicationInfo!! !ai.isResourceOverlay } Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") Pair(newApps, idsArray.toList()) } finally { withContext(Dispatchers.Main) { RootService.unbind(currentConnection) } } } } override suspend fun refreshProfiles(currentApps: List): Result> = withContext(Dispatchers.IO) { runCatching { if (currentApps.isEmpty()) return@runCatching emptyList() currentApps.map { val profile = Natives.getAppProfile(it.packageName, it.uid) it.copy(profile = profile) } } } private suspend inline fun connectKsuService( crossinline onDisconnect: () -> Unit = {} ): Pair = withContext(Dispatchers.Main) { suspendCancellableCoroutine { cont -> val connection = object : ServiceConnection { override fun onServiceDisconnected(name: ComponentName?) { onDisconnect() } override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { if (cont.isActive) { cont.resume(binder as IBinder to this) } } } cont.invokeOnCancellation { if (Looper.myLooper() == Looper.getMainLooper()) { RootService.unbind(connection) } else { Handler(Looper.getMainLooper()).post { RootService.unbind(connection) } } } val intent = Intent(ksuApp, KsuService::class.java) val task = RootService.bindOrTask( intent, Shell.EXECUTOR, connection, ) val shell = KsuCli.SHELL task?.let { shell.execTask(it) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/TemplateRepository.kt ================================================ package me.weishu.kernelsu.data.repository import me.weishu.kernelsu.data.model.TemplateInfo interface TemplateRepository { suspend fun getTemplates(sync: Boolean): Result> suspend fun importTemplates(jsonString: String): Result suspend fun exportTemplates(): Result suspend fun getTemplate(id: String): Result } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/data/repository/TemplateRepositoryImpl.kt ================================================ package me.weishu.kernelsu.data.repository import android.util.Log import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.data.model.TemplateInfo import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.util.getAppProfileTemplate import me.weishu.kernelsu.ui.util.listAppProfileTemplates import me.weishu.kernelsu.ui.util.setAppProfileTemplate import okhttp3.Request import org.json.JSONArray import org.json.JSONObject class TemplateRepositoryImpl : TemplateRepository { companion object { private const val TAG = "TemplateRepository" private const val TEMPLATE_INDEX_URL = "https://kernelsu.org/templates/index.json" private const val TEMPLATE_URL = "https://kernelsu.org/templates/%s" } override suspend fun getTemplates(sync: Boolean): Result> = withContext(Dispatchers.IO) { runCatching { val localTemplateIds = listAppProfileTemplates() Log.i(TAG, "localTemplateIds: $localTemplateIds") if (localTemplateIds.isEmpty() || sync) { fetchRemoteTemplates() } listAppProfileTemplates().mapNotNull { getTemplateInfoById(it) } } } override suspend fun importTemplates(jsonString: String): Result = withContext(Dispatchers.IO) { runCatching { val array = try { JSONArray(jsonString) } catch (e: Exception) { try { val json = JSONObject(jsonString) JSONArray().apply { put(json) } } catch (e: Exception) { throw Exception("invalid templates: $jsonString") } } (0 until array.length()).forEach { i -> runCatching { val template = array.getJSONObject(i) val id = template.getString("id") template.put("local", true) setAppProfileTemplate(id, template.toString()) }.onFailure { e -> Log.e(TAG, "ignore invalid template: $array", e) } } } } override suspend fun exportTemplates(): Result = withContext(Dispatchers.IO) { runCatching { val templates = listAppProfileTemplates() .mapNotNull { getTemplateInfoById(it) } .filter { it.local } if (templates.isEmpty()) { throw Exception("No templates to export") } JSONArray(templates.map { it.toJSON() }).toString() } } override suspend fun getTemplate(id: String): Result = withContext(Dispatchers.IO) { runCatching { getTemplateInfoById(id) ?: throw Exception("Template not found: $id") } } private fun fetchRemoteTemplates() { runCatching { ksuApp.okhttpClient.newCall( Request.Builder().url(TEMPLATE_INDEX_URL).build() ).execute().use { response -> if (!response.isSuccessful) { return } val remoteTemplateIds = JSONArray(response.body.string()) Log.i(TAG, "fetchRemoteTemplates: $remoteTemplateIds") (0 until remoteTemplateIds.length()).forEach { i -> val id = remoteTemplateIds.getString(i) Log.i(TAG, "fetch template: $id") val templateJson = ksuApp.okhttpClient.newCall( Request.Builder().url(TEMPLATE_URL.format(id)).build() ).runCatching { execute().use { response -> if (!response.isSuccessful) { return@forEach } response.body.string() } }.getOrNull() ?: return@forEach Log.i(TAG, "template: $templateJson") // validate remote template runCatching { val json = JSONObject(templateJson) TemplateInfo.fromJSON(json)?.let { // force local template json.put("local", false) setAppProfileTemplate(id, json.toString()) } }.onFailure { Log.e(TAG, "ignore invalid template: $it", it) return@forEach } } } }.onFailure { Log.e(TAG, "fetchRemoteTemplates: $it", it) } } private fun getTemplateInfoById(id: String): TemplateInfo? { return runCatching { TemplateInfo.fromJSON(JSONObject(getAppProfileTemplate(id))) }.onFailure { Log.e(TAG, "ignore invalid template: $it", it) }.getOrNull() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/magica/AppZygotePreload.java ================================================ package me.weishu.kernelsu.magica; import android.app.ZygotePreload; import android.content.pm.ApplicationInfo; import android.util.Log; import androidx.annotation.NonNull; import java.io.File; public class AppZygotePreload implements ZygotePreload { public static final String TAG = "KernelSUMagica"; private static native void forkDontCareAndExecKsud(String ksudPath); @Override public void doPreload(@NonNull ApplicationInfo appInfo) { File f = new File(appInfo.nativeLibraryDir, "libksud.so"); try { System.loadLibrary("kernelsu"); Log.d(TAG, "executing magica ..."); forkDontCareAndExecKsud(f.getAbsolutePath()); } catch (Throwable t) { Log.e(TAG, "failed to late load", t); } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/magica/BootCompletedReceiver.java ================================================ package me.weishu.kernelsu.magica; import static me.weishu.kernelsu.magica.AppZygotePreload.TAG; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.util.Log; public class BootCompletedReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent == null) { return; } var action = intent.getAction(); if (!Intent.ACTION_LOCKED_BOOT_COMPLETED.equals(action) && !Intent.ACTION_BOOT_COMPLETED.equals(action) && !"me.weishu.kernelsu.magica.LAUNCH".equals(action)) { return; } try { context.startService(new Intent(context, MagicaService.class)); Log.i(TAG, "MagicaService started from boot action: " + action); } catch (Throwable e) { Log.e(TAG, "Failed to start MagicaService from boot action: " + action, e); } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/magica/MagicaService.java ================================================ package me.weishu.kernelsu.magica; import android.app.Service; import android.content.Intent; import android.os.Binder; import android.os.IBinder; import androidx.annotation.Nullable; public class MagicaService extends Service { @Nullable @Override public IBinder onBind(Intent intent) { return new Binder(); } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/profile/Capabilities.kt ================================================ package me.weishu.kernelsu.profile /** * @author weishu * @date 2023/6/3. */ enum class Capabilities(val cap: Int, val display: String, val desc: String) { CAP_CHOWN(0, "CHOWN", "Make arbitrary changes to file UIDs and GIDs (see chown(2))"), CAP_DAC_OVERRIDE(1, "DAC_OVERRIDE", "Bypass file read, write, and execute permission checks"), CAP_DAC_READ_SEARCH(2, "DAC_READ_SEARCH", "Bypass file read permission checks and directory read and execute permission checks"), CAP_FOWNER(3, "FOWNER", "Bypass permission checks on operations that normally require the filesystem UID of the process to match the UID of the file (e.g., chmod(2), utime(2)), excluding those operations covered by CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH"), CAP_FSETID(4, "FSETID", "Don’t clear set-user-ID and set-group-ID permission bits when a file is modified; set the set-group-ID bit for a file whose GID does not match the filesystem or any of the supplementary GIDs of the calling process"), CAP_KILL(5, "KILL", "Bypass permission checks for sending signals (see kill(2))."), CAP_SETGID(6, "SETGID", "Make arbitrary manipulations of process GIDs and supplementary GID list; allow setgid(2) manipulation of the caller’s effective and real group IDs"), CAP_SETUID(7, "SETUID", "Make arbitrary manipulations of process UIDs (setuid(2), setreuid(2), setresuid(2), setfsuid(2)); allow changing the current process user IDs; allow changing of the current process group ID to any value in the system’s range of legal group IDs"), CAP_SETPCAP(8, "SETPCAP", "If file capabilities are supported: grant or remove any capability in the caller’s permitted capability set to or from any other process. (This property supersedes the obsolete notion of giving a process all capabilities by granting all capabilities in its permitted set, and of removing all capabilities from a process by granting no capabilities in its permitted set. It does not permit any actions that were not permitted before.)"), CAP_LINUX_IMMUTABLE(9, "LINUX_IMMUTABLE", "Set the FS_APPEND_FL and FS_IMMUTABLE_FL inode flags (see chattr(1))."), CAP_NET_BIND_SERVICE(10, "NET_BIND_SERVICE", "Bind a socket to Internet domain"), CAP_NET_BROADCAST(11, "NET_BROADCAST", "Make socket broadcasts, and listen to multicasts"), CAP_NET_ADMIN(12, "NET_ADMIN", "Perform various network-related operations: interface configuration, administration of IP firewall, masquerading, and accounting, modify routing tables, bind to any address for transparent proxying, set type-of-service (TOS), clear driver statistics, set promiscuous mode, enabling multicasting, use setsockopt(2) to set the following socket options: SO_DEBUG, SO_MARK, SO_PRIORITY (for a priority outside the range 0 to 6), SO_RCVBUFFORCE, and SO_SNDBUFFORCE"), CAP_NET_RAW(13, "NET_RAW", "Use RAW and PACKET sockets"), CAP_IPC_LOCK(14, "IPC_LOCK", "Lock memory (mlock(2), mlockall(2), mmap(2), shmctl(2))"), CAP_IPC_OWNER(15, "IPC_OWNER", "Bypass permission checks for operations on System V IPC objects"), CAP_SYS_MODULE(16, "SYS_MODULE", "Load and unload kernel modules (see init_module(2) and delete_module(2)); in kernels before 2.6.25, this also granted rights for various other operations related to kernel modules"), CAP_SYS_RAWIO(17, "SYS_RAWIO", "Perform I/O port operations (iopl(2) and ioperm(2)); access /proc/kcore"), CAP_SYS_CHROOT(18, "SYS_CHROOT", "Use chroot(2)"), CAP_SYS_PTRACE(19, "SYS_PTRACE", "Trace arbitrary processes using ptrace(2)"), CAP_SYS_PACCT(20, "SYS_PACCT", "Use acct(2)"), CAP_SYS_ADMIN(21, "SYS_ADMIN", "Perform a range of system administration operations including: quotactl(2), mount(2), umount(2), swapon(2), swapoff(2), sethostname(2), and setdomainname(2); set and modify process resource limits (setrlimit(2)); perform various network-related operations (e.g., setting privileged socket options, enabling multicasting, interface configuration); perform various IPC operations (e.g., SysV semaphores, POSIX message queues, System V shared memory); allow reboot and kexec_load(2); override /proc/sys kernel tunables; perform ptrace(2) PTRACE_SECCOMP_GET_FILTER operation; perform some tracing and debugging operations (see ptrace(2)); administer the lifetime of kernel tracepoints (tracefs(5)); perform the KEYCTL_CHOWN and KEYCTL_SETPERM keyctl(2) operations; perform the following keyctl(2) operations: KEYCTL_CAPABILITIES, KEYCTL_CAPSQUASH, and KEYCTL_PKEY_ OPERATIONS; set state for the Extensible Authentication Protocol (EAP) kernel module; and override the RLIMIT_NPROC resource limit; allow ioperm/iopl access to I/O ports"), CAP_SYS_BOOT(22, "SYS_BOOT", "Use reboot(2) and kexec_load(2), reboot and load a new kernel for later execution"), CAP_SYS_NICE(23, "SYS_NICE", "Raise process nice value (nice(2), setpriority(2)) and change the nice value for arbitrary processes; set real-time scheduling policies for calling process, and set scheduling policies and priorities for arbitrary processes (sched_setscheduler(2), sched_setparam(2)"), CAP_SYS_RESOURCE(24, "SYS_RESOURCE", "Override resource Limits. Set resource limits (setrlimit(2), prlimit(2)), override quota limits (quota(2), quotactl(2)), override reserved space on ext2 filesystem (ext2_ioctl(2)), override size restrictions on IPC message queues (msg(2)) and system V shared memory segments (shmget(2)), and override the /proc/sys/fs/pipe-size-max limit"), CAP_SYS_TIME(25, "SYS_TIME", "Set system clock (settimeofday(2), stime(2), adjtimex(2)); set real-time (hardware) clock"), CAP_SYS_TTY_CONFIG(26, "SYS_TTY_CONFIG", "Use vhangup(2); employ various privileged ioctl(2) operations on virtual terminals"), CAP_MKNOD(27, "MKNOD", "Create special files using mknod(2)"), CAP_LEASE(28, "LEASE", "Establish leases on arbitrary files (see fcntl(2))"), CAP_AUDIT_WRITE(29, "AUDIT_WRITE", "Write records to kernel auditing log"), CAP_AUDIT_CONTROL(30, "AUDIT_CONTROL", "Enable and disable kernel auditing; change auditing filter rules; retrieve auditing status and filtering rules"), CAP_SETFCAP(31, "SETFCAP", "If file capabilities are supported: grant or remove any capability in any capability set to any file"), CAP_MAC_OVERRIDE(32, "MAC_OVERRIDE", "Override Mandatory Access Control (MAC). Implemented for the Smack Linux Security Module (LSM)"), CAP_MAC_ADMIN(33, "MAC_ADMIN", "Allow MAC configuration or state changes. Implemented for the Smack LSM"), CAP_SYSLOG(34, "SYSLOG", "Perform privileged syslog(2) operations. See syslog(2) for information on which operations require privilege"), CAP_WAKE_ALARM(35, "WAKE_ALARM", "Trigger something that will wake up the system"), CAP_BLOCK_SUSPEND(36, "BLOCK_SUSPEND", "Employ features that can block system suspend"), CAP_AUDIT_READ(37, "AUDIT_READ", "Allow reading the audit log via a multicast netlink socket"), CAP_PERFMON(38, "PERFMON", "Allow performance monitoring via perf_event_open(2)"), CAP_BPF(39, "BPF", "Allow BPF operations via bpf(2)"), CAP_CHECKPOINT_RESTORE(40, "CHECKPOINT_RESTORE", "Allow processes to be checkpointed via checkpoint/restore in user namespace(2)"), } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/profile/Groups.kt ================================================ package me.weishu.kernelsu.profile /** * https://cs.android.com/android/platform/superproject/main/+/main:system/core/libcutils/include/private/android_filesystem_config.h * @author weishu * @date 2023/6/3. */ enum class Groups(val gid: Int, val display: String, val desc: String) { ROOT(0, "root", "traditional unix root user"), DAEMON(1, "daemon", "Traditional unix daemon owner."), BIN(2, "bin", "Traditional unix binaries owner."), SYS(3, "sys", "A group with the same gid on Linux/macOS/Android."), SYSTEM(1000, "system", "system server"), RADIO(1001, "radio", "telephony subsystem, RIL"), BLUETOOTH(1002, "bluetooth", "bluetooth subsystem"), GRAPHICS(1003, "graphics", "graphics devices"), INPUT(1004, "input", "input devices"), AUDIO(1005, "audio", "audio devices"), CAMERA(1006, "camera", "camera devices"), LOG(1007, "log", "log devices"), COMPASS(1008, "compass", "compass device"), MOUNT(1009, "mount", "mountd socket"), WIFI(1010, "wifi", "wifi subsystem"), ADB(1011, "adb", "android debug bridge (adbd)"), INSTALL(1012, "install", "group for installing packages"), MEDIA(1013, "media", "mediaserver process"), DHCP(1014, "dhcp", "dhcp client"), SDCARD_RW(1015, "sdcard_rw", "external storage write access"), VPN(1016, "vpn", "vpn system"), KEYSTORE(1017, "keystore", "keystore subsystem"), USB(1018, "usb", "USB devices"), DRM(1019, "drm", "DRM server"), MDNSR(1020, "mdnsr", "MulticastDNSResponder (service discovery)"), GPS(1021, "gps", "GPS daemon"), UNUSED1(1022, "unused1", "deprecated, DO NOT USE"), MEDIA_RW(1023, "media_rw", "internal media storage write access"), MTP(1024, "mtp", "MTP USB driver access"), UNUSED2(1025, "unused2", "deprecated, DO NOT USE"), DRMRPC(1026, "drmrpc", "group for drm rpc"), NFC(1027, "nfc", "nfc subsystem"), SDCARD_R(1028, "sdcard_r", "external storage read access"), CLAT(1029, "clat", "clat part of nat464"), LOOP_RADIO(1030, "loop_radio", "loop radio devices"), MEDIA_DRM(1031, "media_drm", "MediaDrm plugins"), PACKAGE_INFO(1032, "package_info", "access to installed package details"), SDCARD_PICS(1033, "sdcard_pics", "external storage photos access"), SDCARD_AV(1034, "sdcard_av", "external storage audio/video access"), SDCARD_ALL(1035, "sdcard_all", "access all users external storage"), LOGD(1036, "logd", "log daemon"), SHARED_RELRO(1037, "shared_relro", "creator of shared GNU RELRO files"), DBUS(1038, "dbus", "dbus-daemon IPC broker process"), TLSDATE(1039, "tlsdate", "tlsdate unprivileged user"), MEDIA_EX(1040, "media_ex", "mediaextractor process"), AUDIOSERVER(1041, "audioserver", "audioserver process"), METRICS_COLL(1042, "metrics_coll", "metrics_collector process"), METRICSD(1043, "metricsd", "metricsd process"), WEBSERV(1044, "webserv", "webservd process"), DEBUGGERD(1045, "debuggerd", "debuggerd unprivileged user"), MEDIA_CODEC(1046, "media_codec", "media_codec process"), CAMERASERVER(1047, "cameraserver", "cameraserver process"), FIREWALL(1048, "firewall", "firewall process"), TRUNKS(1049, "trunks", "trunksd process"), NVRAM(1050, "nvram", "nvram daemon"), DNS(1051, "dns", "DNS resolution daemon (system: netd)"), DNS_TETHER(1052, "dns_tether", "DNS resolution daemon (tether: dnsmasq)"), WEBVIEW_ZYGOTE(1053, "webview_zygote", "WebView zygote process"), VEHICLE_NETWORK(1054, "vehicle_network", "Vehicle network service"), MEDIA_AUDIO(1055, "media_audio", "GID for audio files on internal media storage"), MEDIA_VIDEO(1056, "media_video", "GID for video files on internal media storage"), MEDIA_IMAGE(1057, "media_image", "GID for image files on internal media storage"), TOMBSTONED(1058, "tombstoned", "tombstoned user"), MEDIA_OBB(1059, "media_obb", "GID for OBB files on internal media storage"), ESE(1060, "ese", "embedded secure element (eSE) subsystem"), OTA_UPDATE(1061, "ota_update", "resource tracking UID for OTA updates"), AUTOMOTIVE_EVS(1062, "automotive_evs", "Automotive rear and surround view system"), LOWPAN(1063, "lowpan", "LoWPAN subsystem"), HSM(1064, "lowpan", "hardware security module subsystem"), RESERVED_DISK(1065, "reserved_disk", "GID that has access to reserved disk space"), STATSD(1066, "statsd", "statsd daemon"), INCIDENTD(1067, "incidentd", "incidentd daemon"), SECURE_ELEMENT(1068, "secure_element", "secure element subsystem"), LMKD(1069, "lmkd", "low memory killer daemon"), LLKD(1070, "llkd", "live lock daemon"), IORAPD(1071, "iorapd", "input/output readahead and pin daemon"), GPU_SERVICE(1072, "gpu_service", "GPU service daemon"), NETWORK_STACK(1073, "network_stack", "network stack service"), GSID(1074, "GSID", "GSI service daemon"), FSVERITY_CERT(1075, "fsverity_cert", "fs-verity key ownership in keystore"), CREDSTORE(1076, "credstore", "identity credential manager service"), EXTERNAL_STORAGE(1077, "external_storage", "Full external storage access including USB OTG volumes"), EXT_DATA_RW(1078, "ext_data_rw", "GID for app-private data directories on external storage"), EXT_OBB_RW(1079, "ext_obb_rw", "GID for OBB directories on external storage"), CONTEXT_HUB(1080, "context_hub", "GID for access to the Context Hub"), VIRTUALIZATIONSERVICE(1081, "virtualizationservice", "VirtualizationService daemon"), ARTD(1082, "artd", "ART Service daemon"), UWB(1083, "uwb", "UWB subsystem"), THREAD_NETWORK(1084, "thread_network", "Thread Network subsystem"), DICED(1085, "diced", "Android's DICE daemon"), DMESGD(1086, "dmesgd", "dmesg parsing daemon for kernel report collection"), JC_WEAVER(1087, "jc_weaver", "Javacard Weaver HAL - to manage omapi ARA rules"), JC_STRONGBOX(1088, "jc_strongbox", "Javacard Strongbox HAL - to manage omapi ARA rules"), JC_IDENTITYCRED(1089, "jc_identitycred", "Javacard Identity Cred HAL - to manage omapi ARA rules"), SDK_SANDBOX(1090, "sdk_sandbox", "SDK sandbox virtual UID"), SECURITY_LOG_WRITER(1091, "security_log_writer", "write to security log"), PRNG_SEEDER(1092, "prng_seeder", "PRNG seeder daemon"), UPROBESTATS(1093, "uprobestats", "uid for uprobestats"), CROS_EC(1094, "cros_ec", "uid for accessing ChromeOS EC (cros_ec)"), MMD(1095, "mmd", "uid for memory management daemon"), SHELL(2000, "shell", "adb and debug shell user"), CACHE(2001, "cache", "cache access"), DIAG(2002, "diag", "access to diagnostic resources"), /* The 3000 series are intended for use as supplemental group id's only. * They indicate special Android capabilities that the kernel is aware of. */ NET_BT_ADMIN(3001, "net_bt_admin", "bluetooth: create any socket"), NET_BT(3002, "net_bt", "bluetooth: create sco, rfcomm or l2cap sockets"), INET(3003, "inet", "can create AF_INET and AF_INET6 sockets"), NET_RAW(3004, "net_raw", "can create raw INET sockets"), NET_ADMIN(3005, "net_admin", "can configure interfaces and routing tables."), NET_BW_STATS(3006, "net_bw_stats", "read bandwidth statistics"), NET_BW_ACCT(3007, "net_bw_acct", "change bandwidth statistics accounting"), NET_BT_STACK(3008, "net_bt_stack", "access to various bluetooth management functions"), READPROC(3009, "readproc", "Allow /proc read access"), WAKELOCK(3010, "wakelock", "Allow system wakelock read/write access"), UHID(3011, "uhid", "Allow read/write to /dev/uhid node"), READTRACEFS(3012, "readtracefs", "Allow tracefs read"), VIRTUALMACHINE(3013, "virtualmachine", "Allows VMs to tune for performance"), EVERYBODY(9997, "everybody", "Shared external storage read/write"), MISC(9998, "misc", "Access to misc storage"), NOBODY(9999, "nobody", "Reserved"), APP(10000, "app", "Access to app data"), } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/KsuService.kt ================================================ package me.weishu.kernelsu.ui import android.content.Intent import android.content.pm.PackageInfo import android.content.pm.PackageManager import android.os.IBinder import android.os.UserManager import android.util.Log import com.topjohnwu.superuser.ipc.RootService import me.weishu.kernelsu.IKsuInterface import rikka.parcelablelist.ParcelableListSlice /** * @author weishu * @date 2023/4/18. */ class KsuService : RootService() { companion object { private const val TAG = "KsuService" } override fun onBind(intent: Intent): IBinder { return Stub() } private fun getAllUserIds(): IntArray { val um = getSystemService(USER_SERVICE) as UserManager // getUsers(boolean excludeDying) was added in API 17, but marked as deprecated try { val method = um.javaClass.getMethod("getUsers", Boolean::class.javaPrimitiveType) val users = method.invoke(um, true) as List<*> return extractUserIds(users) } catch (e: Exception) { Log.w(TAG, "getUsers reflection failed", e) } // getAliveUsers() was added in API 31 try { val method = um.javaClass.getMethod("getAliveUsers") val users = method.invoke(um) as List<*> return extractUserIds(users) } catch (e: Exception) { Log.e(TAG, "getAliveUsers reflection failed", e) } return intArrayOf(0) } private fun extractUserIds(users: List<*>?): IntArray { if (users.isNullOrEmpty()) return intArrayOf(0) return try { users.map { user -> user!!.javaClass.getField("id").getInt(user) }.toIntArray() } catch (e: Exception) { Log.e(TAG, "Error extracting ID from UserInfo", e) intArrayOf(0) } } private fun getInstalledPackagesAll(flags: Int): ArrayList { val packages = ArrayList() for (userId in getAllUserIds()) { Log.i(TAG, "getInstalledPackagesAll: $userId") packages.addAll(getInstalledPackagesAsUser(flags, userId)) } return packages } @Suppress("UNCHECKED_CAST") private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List { return try { val pm: PackageManager = packageManager val method = pm.javaClass.getDeclaredMethod( "getInstalledPackagesAsUser", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType ) method.invoke(pm, flags, userId) as List } catch (e: Throwable) { Log.e(TAG, "err", e) ArrayList() } } private inner class Stub : IKsuInterface.Stub() { override fun getPackages(flags: Int): ParcelableListSlice { val list = getInstalledPackagesAll(flags) Log.i(TAG, "getPackages: ${list.size}") return ParcelableListSlice(list) } override fun getUserIds(): IntArray { val ids = getAllUserIds() Log.i(TAG, "getUserIds: ${ids.contentToString()}") return ids } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/MainActivity.kt ================================================ package me.weishu.kernelsu.ui import android.annotation.SuppressLint import android.content.Intent import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.LocalActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.Dp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationBackHandler import androidx.navigationevent.compose.rememberNavigationEventState import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.flow.MutableStateFlow import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.bottombar.BottomBar import me.weishu.kernelsu.ui.component.bottombar.MainPagerState import me.weishu.kernelsu.ui.component.bottombar.SideRail import me.weishu.kernelsu.ui.component.bottombar.rememberMainPagerState import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.navigation3.HandleDeepLink import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Navigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.navigation3.rememberNavigator import me.weishu.kernelsu.ui.screen.about.AboutScreen import me.weishu.kernelsu.ui.screen.appprofile.AppProfileScreen import me.weishu.kernelsu.ui.screen.colorpalette.ColorPaletteScreen import me.weishu.kernelsu.ui.screen.executemoduleaction.ExecuteModuleActionScreen import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.screen.flash.FlashScreen import me.weishu.kernelsu.ui.screen.home.HomePager import me.weishu.kernelsu.ui.screen.install.InstallScreen import me.weishu.kernelsu.ui.screen.module.ModulePager import me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoDetailScreen import me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoScreen import me.weishu.kernelsu.ui.screen.settings.SettingPager import me.weishu.kernelsu.ui.screen.superuser.SuperUserPager import me.weishu.kernelsu.ui.screen.template.AppProfileTemplateScreen import me.weishu.kernelsu.ui.screen.templateeditor.TemplateEditorScreen import me.weishu.kernelsu.ui.theme.KernelSUTheme import me.weishu.kernelsu.ui.theme.LocalColorMode import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBar import me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBarBlur import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.getFileName import me.weishu.kernelsu.ui.util.install import me.weishu.kernelsu.ui.util.rootAvailable import me.weishu.kernelsu.ui.viewmodel.MainActivityViewModel import me.weishu.kernelsu.ui.webui.WebUIActivity import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.theme.MiuixTheme class MainActivity : ComponentActivity() { private val intentState = MutableStateFlow(0) @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val isManager = Natives.isManager if (isManager && !Natives.requireNewKernel()) install() setContent { val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() val appSettings = uiState.appSettings val uiMode = uiState.uiMode val darkMode = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && isSystemInDarkTheme()) DisposableEffect(darkMode) { enableEdgeToEdge( statusBarStyle = SystemBarStyle.auto( android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT ) { darkMode }, navigationBarStyle = SystemBarStyle.auto( android.graphics.Color.TRANSPARENT, android.graphics.Color.TRANSPARENT ) { darkMode }, ) window.isNavigationBarContrastEnforced = false onDispose { } } val navigator = rememberNavigator(Route.Main) val snackBarHostState = remember { SnackbarHostState() } val systemDensity = LocalDensity.current val density = remember(systemDensity, uiState.pageScale) { Density(systemDensity.density * uiState.pageScale, systemDensity.fontScale) } CompositionLocalProvider( LocalNavigator provides navigator, LocalDensity provides density, LocalColorMode provides appSettings.colorMode.value, LocalEnableBlur provides uiState.enableBlur, LocalEnableFloatingBottomBar provides uiState.enableFloatingBottomBar, LocalEnableFloatingBottomBarBlur provides uiState.enableFloatingBottomBarBlur, LocalUiMode provides uiMode, LocalSnackbarHost provides snackBarHostState ) { KernelSUTheme(appSettings = appSettings, uiMode = uiMode) { HandleDeepLink(intentState = intentState.collectAsState()) ZipFileIntentHandler(intentState = intentState, isManager = isManager) ShortcutIntentHandler(intentState = intentState) val navDisplay = @Composable { NavDisplay( backStack = navigator.backStack, entryDecorators = listOf( rememberSaveableStateHolderNavEntryDecorator(), rememberViewModelStoreNavEntryDecorator() ), onBack = { when (val top = navigator.current()) { is Route.TemplateEditor -> { if (!top.readOnly) { navigator.setResult("template_edit", true) } else { navigator.pop() } } else -> navigator.pop() } }, entryProvider = entryProvider { entry { MainScreen() } entry { AboutScreen() } entry { ColorPaletteScreen() } entry { AppProfileTemplateScreen() } entry { key -> TemplateEditorScreen(key.template, key.readOnly) } entry { key -> AppProfileScreen(key.uid) } entry { ModuleRepoScreen() } entry { key -> ModuleRepoDetailScreen(key.module) } entry { InstallScreen() } entry { key -> FlashScreen(key.flashIt) } entry { key -> ExecuteModuleActionScreen(key.moduleId, key.fromShortcut) } entry { MainScreen() } entry { MainScreen() } entry { MainScreen() } entry { MainScreen() } } ) } when (uiMode) { UiMode.Material -> androidx.compose.material3.Scaffold { navDisplay() } UiMode.Miuix -> Scaffold { navDisplay() } } } } } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) setIntent(intent) // Increment intentState to trigger LaunchedEffect re-execution intentState.value += 1 } } val LocalMainPagerState = staticCompositionLocalOf { error("LocalMainPagerState not provided") } @SuppressLint("UnusedMaterial3ScaffoldPaddingParameter") @Composable fun MainScreen() { val navController = LocalNavigator.current val enableBlur = LocalEnableBlur.current val enableFloatingBottomBar = LocalEnableFloatingBottomBar.current val enableFloatingBottomBarBlur = LocalEnableFloatingBottomBarBlur.current val pagerState = rememberPagerState(pageCount = { 4 }) val mainPagerState = rememberMainPagerState(pagerState) val isManager = Natives.isManager val isFullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() var userScrollEnabled by remember(isFullFeatured) { mutableStateOf(isFullFeatured) } val uiMode = LocalUiMode.current val surfaceColor = when (uiMode) { UiMode.Material -> MaterialTheme.colorScheme.surface // Haze is not used in Material, this is just a placeholder UiMode.Miuix -> MiuixTheme.colorScheme.surface } val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = surfaceColor, tint = HazeTint(surfaceColor.copy(0.8f)) ) } else { HazeStyle.Unspecified } val backdrop = rememberLayerBackdrop { drawRect(surfaceColor) drawContent() } LaunchedEffect(mainPagerState.pagerState.currentPage) { mainPagerState.syncPage() } MainScreenBackHandler(mainPagerState, navController) val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val useNavigationRail = isLandscape && !(uiMode == UiMode.Miuix && enableFloatingBottomBar) CompositionLocalProvider( LocalMainPagerState provides mainPagerState ) { val pagerContent = @Composable { bottomInnerPadding: Dp -> HorizontalPager( modifier = Modifier .then(if (enableBlur) Modifier.hazeSource(state = hazeState) else Modifier) .then(if (enableFloatingBottomBar && enableFloatingBottomBarBlur) Modifier.layerBackdrop(backdrop) else Modifier), state = mainPagerState.pagerState, beyondViewportPageCount = 3, userScrollEnabled = userScrollEnabled, ) { when (it) { 0 -> HomePager(navController, bottomInnerPadding) 1 -> SuperUserPager(navController, bottomInnerPadding) 2 -> ModulePager(bottomInnerPadding) 3 -> SettingPager(navController, bottomInnerPadding) } } } if (useNavigationRail) { val startInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout) .only(WindowInsetsSides.Start) val navBarBottomPadding = WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() when (uiMode) { UiMode.Material -> androidx.compose.material3.Scaffold { Row { SideRail( hazeState = hazeState, hazeStyle = hazeStyle, ) Box( modifier = Modifier .weight(1f) .consumeWindowInsets(startInsets) ) { pagerContent(navBarBottomPadding) } } } UiMode.Miuix -> Scaffold { _ -> Row { SideRail( hazeState = hazeState, hazeStyle = hazeStyle, ) Box( modifier = Modifier .weight(1f) .consumeWindowInsets(startInsets) ) { pagerContent(navBarBottomPadding) } } } } } else { val bottomBar = @Composable { Box( modifier = Modifier.fillMaxWidth() ) { BottomBar( hazeState = hazeState, hazeStyle = hazeStyle, backdrop = backdrop, modifier = Modifier.align(Alignment.BottomCenter), ) } } when (uiMode) { UiMode.Material -> androidx.compose.material3.Scaffold(bottomBar = bottomBar) { innerPadding -> pagerContent(innerPadding.calculateBottomPadding()) } UiMode.Miuix -> Scaffold(bottomBar = bottomBar) { innerPadding -> pagerContent(innerPadding.calculateBottomPadding()) } } } } } @Composable private fun MainScreenBackHandler( mainState: MainPagerState, navController: Navigator, ) { val isPagerBackHandlerEnabled by remember { derivedStateOf { navController.current() is Route.Main && navController.backStackSize() == 1 && mainState.selectedPage != 0 } } val navEventState = rememberNavigationEventState(NavigationEventInfo.None) NavigationBackHandler( state = navEventState, isBackEnabled = isPagerBackHandlerEnabled, onBackCompleted = { mainState.animateToPage(0) } ) } /** * Handles ZIP file installation from external apps (e.g., file managers). * - In normal mode: Shows a confirmation dialog before installation * - In safe mode: Shows a Toast notification and prevents installation */ @SuppressLint("StringFormatInvalid", "LocalContextGetResourceValueCall") @Composable private fun ZipFileIntentHandler( intentState: MutableStateFlow, isManager: Boolean, ) { val activity = LocalActivity.current ?: return val context = LocalContext.current var zipUri by remember { mutableStateOf(null) } val isSafeMode = Natives.isSafeMode val clearZipUri = { zipUri = null } val navigator = LocalNavigator.current val installDialog = rememberConfirmDialog( onConfirm = { zipUri?.let { uri -> navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri)))) } clearZipUri() }, onDismiss = clearZipUri ) fun getDisplayName(uri: Uri): String { return uri.getFileName(context) ?: uri.lastPathSegment ?: "Unknown" } val intentStateValue by intentState.collectAsState() LaunchedEffect(intentStateValue) { val currentIntent = activity.intent val uri = currentIntent?.data ?: return@LaunchedEffect if (!isManager || uri.scheme != "content" || currentIntent.type != "application/zip") { return@LaunchedEffect } activity.intent.data = null activity.intent.type = null if (isSafeMode) { Toast.makeText(context, context.getString(R.string.safe_mode_module_disabled), Toast.LENGTH_SHORT).show() } else { zipUri = uri installDialog.showConfirm( title = context.getString(R.string.module), content = context.getString( R.string.module_install_prompt_with_name, "\n${getDisplayName(uri)}" ) ) } } } @Composable private fun ShortcutIntentHandler( intentState: MutableStateFlow, ) { val activity = LocalActivity.current ?: return val context = LocalContext.current val intentStateValue by intentState.collectAsState() val navigator = LocalNavigator.current LaunchedEffect(intentStateValue) { val intent = activity.intent val type = intent?.getStringExtra("shortcut_type") ?: return@LaunchedEffect when (type) { "module_action" -> { val moduleId = intent.getStringExtra("module_id") ?: return@LaunchedEffect navigator.push(Route.ExecuteModuleAction(moduleId, fromShortcut = true)) intent.removeExtra("shortcut_type") intent.removeExtra("module_id") } "module_webui" -> { val moduleId = intent.getStringExtra("module_id") ?: return@LaunchedEffect val webIntent = Intent(context, WebUIActivity::class.java) .setData("kernelsu://webui/$moduleId".toUri()) context.startActivity(webIntent) } else -> return@LaunchedEffect } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/UiMode.kt ================================================ package me.weishu.kernelsu.ui import androidx.compose.runtime.staticCompositionLocalOf enum class UiMode(val value: String) { Miuix("miuix"), Material("material"); companion object { fun fromValue(value: String): UiMode = when (value) { Material.value -> Material else -> Miuix } val DEFAULT_VALUE = Miuix.value } } val LocalUiMode = staticCompositionLocalOf { UiMode.Miuix } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/animation/DampedDragAnimation.kt ================================================ package me.weishu.kernelsu.ui.animation import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.spring import androidx.compose.foundation.MutatorMutex import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.util.VelocityTracker import androidx.compose.ui.unit.IntSize import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.android.awaitFrame import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.first import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.modifier.inspectDragGestures import kotlin.math.abs class DampedDragAnimation( private val animationScope: CoroutineScope, val initialValue: Float, val valueRange: ClosedRange, val visibilityThreshold: Float, val initialScale: Float, val pressedScale: Float, val canDrag: (Offset) -> Boolean = { true }, val onDragStarted: DampedDragAnimation.(position: Offset) -> Unit, val onDragStopped: DampedDragAnimation.() -> Unit, val onDrag: DampedDragAnimation.(size: IntSize, dragAmount: Offset) -> Unit, ) { private val valueAnimationSpec = spring(1f, 1000f, visibilityThreshold) private val velocityAnimationSpec = spring(0.5f, 300f, visibilityThreshold * 10f) private val pressProgressAnimationSpec = spring(1f, 1000f, 0.001f) private val scaleXAnimationSpec = spring(0.6f, 250f, 0.001f) private val scaleYAnimationSpec = spring(0.7f, 250f, 0.001f) private val valueAnimation = Animatable(initialValue, visibilityThreshold) private val velocityAnimation = Animatable(0f, 5f) private val pressProgressAnimation = Animatable(0f, 0.001f) private val scaleXAnimation = Animatable(initialScale, 0.001f) private val scaleYAnimation = Animatable(initialScale, 0.001f) private val mutatorMutex = MutatorMutex() private val velocityTracker = VelocityTracker() val value: Float get() = valueAnimation.value val targetValue: Float get() = valueAnimation.targetValue val pressProgress: Float get() = pressProgressAnimation.value val scaleX: Float get() = scaleXAnimation.value val scaleY: Float get() = scaleYAnimation.value val velocity: Float get() = velocityAnimation.value val modifier: Modifier = Modifier.pointerInput(Unit) { inspectDragGestures( onDragStart = { down -> onDragStarted(down.position) press() }, onDragEnd = { onDragStopped() release() }, onDragCancel = { onDragStopped() release() } ) { change, dragAmount -> val position = change.position val previousPosition = change.previousPosition val isInside = canDrag(position) val wasInside = canDrag(previousPosition) if (isInside && wasInside) { onDrag(size, dragAmount) } } } fun press() { velocityTracker.resetTracking() animationScope.launch { launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } launch { scaleXAnimation.animateTo(pressedScale, scaleXAnimationSpec) } launch { scaleYAnimation.animateTo(pressedScale, scaleYAnimationSpec) } } } fun release() { animationScope.launch { awaitFrame() if (value != targetValue) { val threshold = (valueRange.endInclusive - valueRange.start) * 0.025f snapshotFlow { valueAnimation.value } .filter { abs(it - valueAnimation.targetValue) < threshold } .first() } launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } launch { scaleXAnimation.animateTo(initialScale, scaleXAnimationSpec) } launch { scaleYAnimation.animateTo(initialScale, scaleYAnimationSpec) } } } fun updateValue(value: Float) { val targetValue = value.coerceIn(valueRange) animationScope.launch { launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) { updateVelocity() } } } } fun animateToValue(value: Float) { animationScope.launch { mutatorMutex.mutate { press() val targetValue = value.coerceIn(valueRange) launch { valueAnimation.animateTo(targetValue, valueAnimationSpec) } if (velocity != 0f) { launch { velocityAnimation.animateTo(0f, velocityAnimationSpec) } } release() } } } private fun updateVelocity() { velocityTracker.addPosition( System.currentTimeMillis(), Offset(value, 0f) ) val targetVelocity = velocityTracker.calculateVelocity().x / (valueRange.endInclusive - valueRange.start) animationScope.launch { velocityAnimation.animateTo(targetVelocity, velocityAnimationSpec) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/animation/InteractiveHighlight.kt ================================================ package me.weishu.kernelsu.ui.animation import android.annotation.SuppressLint import android.graphics.RuntimeShader import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter import androidx.compose.animation.core.VisibilityThreshold import androidx.compose.animation.core.spring import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.BlendMode import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ShaderBrush import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.util.fastCoerceIn import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.modifier.inspectDragGestures import org.intellij.lang.annotations.Language @SuppressLint("NewApi") class InteractiveHighlight( val animationScope: CoroutineScope, val position: (size: Size, offset: Offset) -> Offset = { _, offset -> offset } ) { private val pressProgressAnimationSpec = spring(0.5f, 300f, 0.001f) private val positionAnimationSpec = spring(0.5f, 300f, Offset.VisibilityThreshold) private val pressProgressAnimation = Animatable(0f, 0.001f) private val positionAnimation = Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold) private var startPosition = Offset.Zero val offset: Offset get() = positionAnimation.value - startPosition @Language("AGSL") private val shader = RuntimeShader( """ uniform float2 size; layout(color) uniform half4 color; uniform float radius; uniform float2 position; half4 main(float2 coord) { float dist = distance(coord, position); float intensity = smoothstep(radius, radius * 0.5, dist); return color * intensity; }""" ) val modifier: Modifier = Modifier.drawWithContent { val progress = pressProgressAnimation.value if (progress > 0f) { drawRect( Color.White.copy(0.06f * progress), blendMode = BlendMode.Plus ) shader.apply { val position = position(size, positionAnimation.value) setFloatUniform("size", size.width, size.height) setColorUniform("color", Color.White.copy(0.12f * progress).toArgb()) setFloatUniform("radius", size.minDimension * 1.2f) setFloatUniform( "position", position.x.fastCoerceIn(0f, size.width), position.y.fastCoerceIn(0f, size.height) ) } drawRect( ShaderBrush(shader), blendMode = BlendMode.Plus ) } drawContent() } val gestureModifier: Modifier = Modifier.pointerInput(animationScope) { inspectDragGestures( onDragStart = { down -> startPosition = down.position animationScope.launch { launch { pressProgressAnimation.animateTo(1f, pressProgressAnimationSpec) } launch { positionAnimation.snapTo(startPosition) } } }, onDragEnd = { animationScope.launch { launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } } }, onDragCancel = { animationScope.launch { launch { pressProgressAnimation.animateTo(0f, pressProgressAnimationSpec) } launch { positionAnimation.animateTo(startPosition, positionAnimationSpec) } } } ) { change, _ -> animationScope.launch { positionAnimation.snapTo(change.position) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/AppIconImage.kt ================================================ package me.weishu.kernelsu.ui.component import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.unit.dp import com.kyant.capsule.ContinuousRoundedRectangle import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.ui.util.AppIconCache import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable fun AppIconImage( modifier: Modifier = Modifier, applicationInfo: ApplicationInfo, label: String? = null ) { val density = LocalDensity.current val context = LocalContext.current val targetSizePx = with(density) { 48.dp.roundToPx() } Box(modifier = modifier) { var appIcon by remember { mutableStateOf(null) } LaunchedEffect(applicationInfo) { val loadedIcon = AppIconCache.loadIcon(context, applicationInfo, targetSizePx) appIcon = loadedIcon.asImageBitmap() } val appLabel by produceState(initialValue = label, key1 = applicationInfo) { if (label != null) { value = label } else { withContext(Dispatchers.IO) { val pm = context.packageManager val appLabel = pm.getApplicationLabel(applicationInfo).toString() value = appLabel } } } Crossfade( targetState = appIcon, animationSpec = tween(durationMillis = 150), label = "IconFade" ) { icon -> if (icon == null) { PlaceHolderBox(Modifier.fillMaxSize()) } else { Image( bitmap = icon, contentDescription = appLabel, modifier = Modifier.fillMaxSize() ) } } } } @Composable fun AppIconImage(modifier: Modifier = Modifier, packageInfo: PackageInfo, label: String? = null) { val appInfo = packageInfo.applicationInfo if (appInfo == null) { PlaceHolderBox(modifier) return } AppIconImage(modifier, appInfo, label) } @Composable private fun PlaceHolderBox(modifier: Modifier = Modifier) { Box( modifier = modifier .clip(ContinuousRoundedRectangle(12.dp)) .background(colorScheme.secondaryContainer) ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/FloatingBottomBar.kt ================================================ package me.weishu.kernelsu.ui.component import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.EaseOut import androidx.compose.animation.core.spring import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastCoerceIn import androidx.compose.ui.util.fastRoundToInt import androidx.compose.ui.util.lerp import com.kyant.backdrop.Backdrop import com.kyant.backdrop.backdrops.layerBackdrop import com.kyant.backdrop.backdrops.rememberCombinedBackdrop import com.kyant.backdrop.backdrops.rememberLayerBackdrop import com.kyant.backdrop.drawBackdrop import com.kyant.backdrop.effects.blur import com.kyant.backdrop.effects.lens import com.kyant.backdrop.effects.vibrancy import com.kyant.backdrop.highlight.Highlight import com.kyant.backdrop.shadow.InnerShadow import com.kyant.backdrop.shadow.Shadow import com.kyant.capsule.ContinuousCapsule import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.drop import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.animation.DampedDragAnimation import me.weishu.kernelsu.ui.animation.InteractiveHighlight import me.weishu.kernelsu.ui.theme.isInDarkTheme import top.yukonga.miuix.kmp.theme.MiuixTheme import kotlin.math.abs import kotlin.math.sign val LocalFloatingBottomBarTabScale = staticCompositionLocalOf { { 1f } } @Composable fun RowScope.FloatingBottomBarItem( onClick: () -> Unit, modifier: Modifier = Modifier, content: @Composable ColumnScope.() -> Unit ) { val scale = LocalFloatingBottomBarTabScale.current Column( modifier .clip(ContinuousCapsule) .clickable( interactionSource = null, indication = null, role = Role.Tab, onClick = onClick ) .fillMaxHeight() .weight(1f) .graphicsLayer { val scale = scale() scaleX = scale scaleY = scale }, verticalArrangement = Arrangement.spacedBy(1.dp, Alignment.CenterVertically), horizontalAlignment = Alignment.CenterHorizontally, content = content ) } @Composable fun FloatingBottomBar( modifier: Modifier = Modifier, selectedIndex: () -> Int, onSelected: (index: Int) -> Unit, backdrop: Backdrop, tabsCount: Int, isBlurEnabled: Boolean = true, content: @Composable RowScope.() -> Unit ) { val isInLightTheme = !isInDarkTheme() val accentColor = MiuixTheme.colorScheme.primary val containerColor = if (isBlurEnabled) { MiuixTheme.colorScheme.surfaceContainer.copy(0.4f) } else { MiuixTheme.colorScheme.surfaceContainer } val tabsBackdrop = rememberLayerBackdrop() val density = LocalDensity.current val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr val animationScope = rememberCoroutineScope() var tabWidthPx by remember { mutableFloatStateOf(0f) } var totalWidthPx by remember { mutableFloatStateOf(0f) } val offsetAnimation = remember { Animatable(0f) } val panelOffset by remember(density) { derivedStateOf { if (totalWidthPx == 0f) 0f else { val fraction = (offsetAnimation.value / totalWidthPx).fastCoerceIn(-1f, 1f) with(density) { 4f.dp.toPx() * fraction.sign * EaseOut.transform(abs(fraction)) } } } } var currentIndex by remember(selectedIndex) { mutableIntStateOf(selectedIndex()) } class DampedDragAnimationHolder { var instance: DampedDragAnimation? = null } val holder = remember { DampedDragAnimationHolder() } val dampedDragAnimation = remember(animationScope, tabsCount, density, isLtr) { DampedDragAnimation( animationScope = animationScope, initialValue = selectedIndex().toFloat(), valueRange = 0f..(tabsCount - 1).toFloat(), visibilityThreshold = 0.001f, initialScale = 1f, pressedScale = 78f / 56f, canDrag = { offset -> val anim = holder.instance ?: return@DampedDragAnimation true if (tabWidthPx == 0f) return@DampedDragAnimation false val currentValue = anim.value val indicatorX = currentValue * tabWidthPx val padding = with(density) { 4.dp.toPx() } val globalTouchX = if (isLtr) { val touchX = indicatorX + offset.x padding + touchX } else { val touchX = totalWidthPx - padding - tabWidthPx - indicatorX + offset.x touchX } globalTouchX in 0f..totalWidthPx }, onDragStarted = {}, onDragStopped = { val targetIndex = targetValue.fastRoundToInt().fastCoerceIn(0, tabsCount - 1) currentIndex = targetIndex animateToValue(targetIndex.toFloat()) animationScope.launch { offsetAnimation.animateTo(0f, spring(1f, 300f, 0.5f)) } }, onDrag = { _, dragAmount -> if (tabWidthPx > 0) { updateValue( (targetValue + dragAmount.x / tabWidthPx * if (isLtr) 1f else -1f) .fastCoerceIn(0f, (tabsCount - 1).toFloat()) ) animationScope.launch { offsetAnimation.snapTo(offsetAnimation.value + dragAmount.x) } } } ).also { holder.instance = it } } LaunchedEffect(selectedIndex) { snapshotFlow { selectedIndex() }.collectLatest { currentIndex = it } } LaunchedEffect(dampedDragAnimation) { snapshotFlow { currentIndex }.drop(1).collectLatest { index -> dampedDragAnimation.animateToValue(index.toFloat()) onSelected(index) } } val interactiveHighlight = remember(animationScope, tabWidthPx) { InteractiveHighlight( animationScope = animationScope, position = { size, _ -> Offset( if (isLtr) (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset else size.width - (dampedDragAnimation.value + 0.5f) * tabWidthPx + panelOffset, size.height / 2f ) } ) } Box( modifier = modifier.width(IntrinsicSize.Min), contentAlignment = Alignment.CenterStart ) { Row( Modifier .onGloballyPositioned { coords -> totalWidthPx = coords.size.width.toFloat() val contentWidthPx = totalWidthPx - with(density) { 8.dp.toPx() } tabWidthPx = contentWidthPx / tabsCount } .graphicsLayer { translationX = panelOffset } .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {} ) .drawBackdrop( backdrop = backdrop, shape = { ContinuousCapsule }, effects = { if (isBlurEnabled) { vibrancy() blur(8f.dp.toPx()) lens(24f.dp.toPx(), 24f.dp.toPx()) } }, highlight = { Highlight.Default.copy(alpha = if (isBlurEnabled) 1f else 0f) }, shadow = { Shadow.Default.copy( color = Color.Black.copy(if (isInLightTheme) 0.1f else 0.2f), ) }, layerBlock = { if (isBlurEnabled) { val progress = dampedDragAnimation.pressProgress val scale = lerp(1f, 1f + 16f.dp.toPx() / size.width, progress) scaleX = scale scaleY = scale } }, onDrawSurface = { drawRect(containerColor) } ) .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier) .height(64.dp) .padding(4.dp), verticalAlignment = Alignment.CenterVertically, content = content ) CompositionLocalProvider( LocalFloatingBottomBarTabScale provides { if (isBlurEnabled) lerp(1f, 1.2f, dampedDragAnimation.pressProgress) else 1f } ) { Row( Modifier .clearAndSetSemantics {} .alpha(0f) .layerBackdrop(tabsBackdrop) .graphicsLayer { translationX = panelOffset } .drawBackdrop( backdrop = backdrop, shape = { ContinuousCapsule }, effects = { if (isBlurEnabled) { val progress = dampedDragAnimation.pressProgress vibrancy() blur(8f.dp.toPx()) lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress) } }, highlight = { Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) }, onDrawSurface = { drawRect(containerColor) } ) .then(if (isBlurEnabled) interactiveHighlight.modifier else Modifier) .height(56.dp) .padding(horizontal = 4.dp) .graphicsLayer(colorFilter = ColorFilter.tint(accentColor)), verticalAlignment = Alignment.CenterVertically, content = content ) } if (tabWidthPx > 0f) { Box( Modifier .padding(horizontal = 4.dp) .graphicsLayer { val contentWidth = totalWidthPx - with(density) { 8.dp.toPx() } val singleTabWidth = contentWidth / tabsCount val progressOffset = dampedDragAnimation.value * singleTabWidth translationX = if (isLtr) { progressOffset + panelOffset } else { -progressOffset + panelOffset } } .then(if (isBlurEnabled) interactiveHighlight.gestureModifier else Modifier) .then(dampedDragAnimation.modifier) .drawBackdrop( backdrop = rememberCombinedBackdrop(backdrop, tabsBackdrop), shape = { ContinuousCapsule }, effects = { if (isBlurEnabled) { val progress = dampedDragAnimation.pressProgress lens(10f.dp.toPx() * progress, 14f.dp.toPx() * progress, true) } }, highlight = { Highlight.Default.copy(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) }, shadow = { Shadow(alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f) }, innerShadow = { InnerShadow( radius = 8f.dp * dampedDragAnimation.pressProgress, alpha = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f ) }, layerBlock = { if (isBlurEnabled) { scaleX = dampedDragAnimation.scaleX scaleY = dampedDragAnimation.scaleY val velocity = dampedDragAnimation.velocity / 10f scaleX /= 1f - (velocity * 0.75f).fastCoerceIn(-0.2f, 0.2f) scaleY *= 1f - (velocity * 0.25f).fastCoerceIn(-0.2f, 0.2f) } }, onDrawSurface = { val progress = if (isBlurEnabled) dampedDragAnimation.pressProgress else 0f drawRect( color = if (isInLightTheme) { Color.Black.copy(0.1f) } else { Color.White.copy(0.1f) }, alpha = 1f - progress ) drawRect( Color.Black.copy(alpha = 0.03f * progress) ) } ) .height(56.dp) .width(with(density) { ((totalWidthPx - 8.dp.toPx()) / tabsCount).toDp() }) ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/GithubMarkdown.kt ================================================ package me.weishu.kernelsu.ui.component import android.annotation.SuppressLint import android.content.ActivityNotFoundException import android.content.Intent import android.graphics.Color import android.util.Log import android.view.MotionEvent import android.view.View import android.webkit.JavascriptInterface import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebSettings import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.webkit.WebViewAssetLoader import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.theme.isInDarkTheme import me.weishu.kernelsu.ui.util.adjustLightnessArgb import me.weishu.kernelsu.ui.util.cssColorFromArgb import me.weishu.kernelsu.ui.util.ensureVisibleByMix import me.weishu.kernelsu.ui.util.relativeLuminance import okhttp3.Headers.Companion.toHeaders import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import okio.IOException import top.yukonga.miuix.kmp.theme.MiuixTheme import java.io.ByteArrayInputStream import java.nio.charset.StandardCharsets import kotlin.math.abs private val SEMICOLON_SPLIT = ";\\s*".toRegex() private val EQUALS_SPLIT = "=\\s*".toRegex() @SuppressLint("JavascriptInterface", "SetJavaScriptEnabled", "ClickableViewAccessibility") @Composable fun GithubMarkdown( content: String, onLoadingChange: (Boolean) -> Unit = {}, containerColor: androidx.compose.ui.graphics.Color? = null, ) { val density = LocalDensity.current val systemDensity = LocalResources.current.displayMetrics.density val fontScale = density.fontScale val pageScale = density.density / systemDensity val newTextZoom = (90 * pageScale * fontScale).toInt() val scrollInterface = remember { MarkdownScrollInterface() } val isDark = isInDarkTheme() val dir = if (LocalLayoutDirection.current == LayoutDirection.Rtl) "rtl" else "ltr" val colors = getMarkdownColors(containerColor) val bgDefault = colors.bgDefault val bgMuted = colors.bgMuted val bgNeutralMuted = colors.bgNeutralMuted val bgAttentionMuted = colors.bgAttentionMuted val fgLink = colors.fgLink val cssHref = "https://appassets.androidplatform.net/assets/github-markdown.css" val html = """
${content}
""".trimIndent() AndroidView( factory = { context -> val frameLayout = FrameLayout(context) val webView = WebView(context).apply { try { setBackgroundColor(Color.TRANSPARENT) isVerticalScrollBarEnabled = false isHorizontalScrollBarEnabled = false settings.apply { offscreenPreRaster = true javaScriptEnabled = true domStorageEnabled = true mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW allowContentAccess = false allowFileAccess = false cacheMode = WebSettings.LOAD_CACHE_ELSE_NETWORK textZoom = newTextZoom setSupportZoom(false) setGeolocationEnabled(false) } layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ) addJavascriptInterface(scrollInterface, "AndroidScroll") webViewClient = object : WebViewClient() { private val assetLoader = WebViewAssetLoader.Builder() .addPathHandler("/assets/", WebViewAssetLoader.AssetsPathHandler(context)) .build() override fun onPageFinished(view: WebView, url: String) { super.onPageFinished(view, url) val js = """ (function() { if (window.androidScrollInjected) return; window.androidScrollInjected = true; function checkScroll(target) { if (!target || target === document.body || target === document.documentElement) return {l: false, r: false}; var style = window.getComputedStyle(target); if (style.overflowX !== 'auto' && style.overflowX !== 'scroll') return {l: false, r: false}; if (target.scrollWidth <= target.clientWidth) return {l: false, r: false}; var atLeft = target.scrollLeft <= 0; var atRight = Math.ceil(target.scrollLeft + target.clientWidth) >= target.scrollWidth; return {l: !atLeft, r: !atRight}; } var lastTarget = null; var lastState = {l: false, r: false}; function update(l, r) { if (lastState.l !== l || lastState.r !== r) { lastState = {l: l, r: r}; AndroidScroll.updateScrollState(l, r); } } document.addEventListener('touchstart', function(e) { var t = e.target; var found = false; while(t && t !== document.body) { var s = checkScroll(t); if (s.l || s.r) { lastTarget = t; update(s.l, s.r); found = true; break; } t = t.parentElement; } if (!found) { lastTarget = null; update(false, false); } }, {passive: true}); document.addEventListener('touchmove', function(e) { if (lastTarget) { var s = checkScroll(lastTarget); update(s.l, s.r); } }, {passive: true}); document.addEventListener('scroll', function(e) { if (lastTarget && (e.target === lastTarget || e.target.contains(lastTarget))) { var s = checkScroll(lastTarget); update(s.l, s.r); } }, {passive: true, capture: true}); })(); """.trimIndent() view.evaluateJavascript(js, null) } override fun shouldOverrideUrlLoading( view: WebView, request: WebResourceRequest ): Boolean { try { val intent = Intent(Intent.ACTION_VIEW, request.url) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context.startActivity(intent) } catch (_: ActivityNotFoundException) { Log.w("GithubMarkdown", "No activity to handle: ${request.url}") } return true } override fun onPageCommitVisible(view: WebView?, url: String?) { onLoadingChange(false) } override fun shouldInterceptRequest( view: WebView, request: WebResourceRequest ): WebResourceResponse? { assetLoader.shouldInterceptRequest(request.url)?.let { return it } val scheme = request.url.scheme ?: return null if (!scheme.startsWith("http")) return null val client: OkHttpClient = ksuApp.okhttpClient val call = client.newCall( Request.Builder() .url(request.url.toString()) .method(request.method, null) .headers(request.requestHeaders.toHeaders()) .build() ) return try { val reply: Response = call.execute() val header = reply.header("content-type", "text/plain; charset=utf-8") val contentTypes = header?.split(SEMICOLON_SPLIT) ?: emptyList() val mimeType = contentTypes.firstOrNull() ?: "image/*" val charset = contentTypes.getOrNull(1)?.split(EQUALS_SPLIT)?.getOrNull(1) ?: "utf-8" val bytes = reply.body.bytes() WebResourceResponse(mimeType, charset, ByteArrayInputStream(bytes)) } catch (e: IOException) { Log.e("GithubMarkdown", "Resource load failed", e) WebResourceResponse( "text/html", "utf-8", ByteArrayInputStream(ByteArray(0)) ) } } } setOnTouchListener(object : View.OnTouchListener { private var isHorizontalScrollLocked = false private var initialDownX = 0f private var initialDownY = 0f @SuppressLint("ClickableViewAccessibility") override fun onTouch(v: View, event: MotionEvent): Boolean { when (event.action) { MotionEvent.ACTION_DOWN -> { initialDownX = event.x initialDownY = event.y isHorizontalScrollLocked = false v.parent.requestDisallowInterceptTouchEvent(true) } MotionEvent.ACTION_MOVE -> { if (isHorizontalScrollLocked) { v.parent.requestDisallowInterceptTouchEvent(true) } else { val dx = event.x - initialDownX val dy = event.y - initialDownY if (abs(dx) > abs(dy)) { val canScroll = if (dx < 0) scrollInterface.canScrollRight else scrollInterface.canScrollLeft if (canScroll) { isHorizontalScrollLocked = true v.parent.requestDisallowInterceptTouchEvent(true) } else { v.parent.requestDisallowInterceptTouchEvent(false) } } else { v.parent.requestDisallowInterceptTouchEvent(false) return true } } } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { v.parent.requestDisallowInterceptTouchEvent(false) isHorizontalScrollLocked = false } } return false } }) loadDataWithBaseURL( "https://appassets.androidplatform.net", html, "text/html", StandardCharsets.UTF_8.name(), null ) } catch (e: Throwable) { Log.e("GithubMarkdown", "WebView setup failed", e) } } frameLayout.addView(webView) frameLayout }, update = { frameLayout -> val webView = frameLayout.getChildAt(0) as? WebView ?: return@AndroidView webView.settings.textZoom = newTextZoom onLoadingChange(true) webView.loadDataWithBaseURL( "https://appassets.androidplatform.net", html, "text/html", StandardCharsets.UTF_8.name(), null ) }, onRelease = { frameLayout -> val webView = frameLayout.getChildAt(0) as? WebView frameLayout.removeAllViews() webView?.apply { stopLoading() destroy() } }, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .clipToBounds(), ) } class MarkdownScrollInterface { @Volatile var canScrollLeft = false @Volatile var canScrollRight = false @JavascriptInterface fun updateScrollState(left: Boolean, right: Boolean) { canScrollLeft = left canScrollRight = right } } private data class MarkdownColors( val bgDefault: String, val bgMuted: String, val bgNeutralMuted: String, val bgAttentionMuted: String, val fgLink: String ) @Composable private fun getMarkdownColors(containerColor: androidx.compose.ui.graphics.Color?): MarkdownColors { val uiMode = LocalUiMode.current return when (uiMode) { UiMode.Material -> { val bgArgb = containerColor?.toArgb() ?: MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp).toArgb() MarkdownColors( cssColorFromArgb(bgArgb), cssColorFromArgb(MaterialTheme.colorScheme.surfaceContainerHigh.toArgb()), cssColorFromArgb(MaterialTheme.colorScheme.surfaceDim.toArgb()), cssColorFromArgb(MaterialTheme.colorScheme.surfaceBright.toArgb()), cssColorFromArgb(MaterialTheme.colorScheme.primary.toArgb()) ) } UiMode.Miuix -> { val bgArgb = containerColor?.toArgb() ?: MiuixTheme.colorScheme.surfaceContainer.toArgb() val bgLuminance = relativeLuminance(bgArgb) fun makeVariant(delta: Float): Int { val candidate = adjustLightnessArgb(bgArgb, delta) val madeLighter = delta > 0f return ensureVisibleByMix(bgArgb, candidate, 1.15, madeLighter) } MarkdownColors( cssColorFromArgb(bgArgb), cssColorFromArgb(makeVariant(if (bgLuminance > 0.6) -0.06f else 0.06f)), cssColorFromArgb(makeVariant(if (bgLuminance > 0.6) -0.12f else 0.12f)), cssColorFromArgb(makeVariant(-0.12f)), cssColorFromArgb(MiuixTheme.colorScheme.primary.toArgb()) ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/KeyEventBlocker.kt ================================================ package me.weishu.kernelsu.ui.component import androidx.compose.foundation.focusable import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.onKeyEvent @Composable fun KeyEventBlocker(predicate: (KeyEvent) -> Boolean) { val requester = remember { FocusRequester() } Box( Modifier .onKeyEvent { predicate(it) } .focusRequester(requester) .focusable() ) LaunchedEffect(Unit) { requester.requestFocus() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/KsuValidCheck.kt ================================================ package me.weishu.kernelsu.ui.component import androidx.compose.runtime.Composable import me.weishu.kernelsu.Natives @Composable fun KsuIsValid( content: @Composable () -> Unit ) { val isManager = Natives.isManager val ksuVersion = if (isManager) Natives.version else null if (ksuVersion != null) { content() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/Markdown.kt ================================================ package me.weishu.kernelsu.ui.component import android.graphics.text.LineBreaker import android.text.Layout import android.text.method.LinkMovementMethod import android.view.ViewGroup import android.widget.FrameLayout import android.widget.ScrollView import android.widget.TextView import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.viewinterop.AndroidView import io.noties.markwon.Markwon import io.noties.markwon.utils.NoCopySpannableFactory import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import top.yukonga.miuix.kmp.theme.MiuixTheme private const val TEXTVIEW_TAG = "markdownTextView" @Composable fun Markdown(content: String) { val contentColor = when (LocalUiMode.current) { UiMode.Material -> MaterialTheme.colorScheme.onBackground.toArgb() UiMode.Miuix -> MiuixTheme.colorScheme.onBackground.toArgb() } AndroidView( factory = { context -> val frameLayout = FrameLayout(context) val scrollView = ScrollView(context) val textView = TextView(context).apply { tag = TEXTVIEW_TAG movementMethod = LinkMovementMethod.getInstance() setSpannableFactory(NoCopySpannableFactory.getInstance()) breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } scrollView.addView(textView) frameLayout.addView(scrollView) frameLayout }, modifier = Modifier .fillMaxWidth() .wrapContentHeight() .clipToBounds(), update = { frameLayout -> frameLayout.findViewWithTag(TEXTVIEW_TAG)?.let { textView -> Markwon.create(textView.context).setMarkdown(textView, content) textView.setTextColor(contentColor) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/MenuPositionProvider.kt ================================================ package me.weishu.kernelsu.ui.component import androidx.compose.foundation.layout.PaddingValues import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntRect import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import top.yukonga.miuix.kmp.basic.PopupPositionProvider object ListPopupDefaults { val MenuPositionProvider = object : PopupPositionProvider { override fun calculatePosition( anchorBounds: IntRect, windowBounds: IntRect, layoutDirection: LayoutDirection, popupContentSize: IntSize, popupMargin: IntRect, alignment: PopupPositionProvider.Align, ): IntOffset { val offsetX: Int val offsetY: Int when (alignment.resolve(layoutDirection)) { PopupPositionProvider.Align.TopStart -> { offsetX = anchorBounds.left + popupMargin.left offsetY = anchorBounds.bottom + popupMargin.top } PopupPositionProvider.Align.TopEnd -> { offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right offsetY = anchorBounds.bottom + popupMargin.top } PopupPositionProvider.Align.BottomStart -> { offsetX = anchorBounds.left + popupMargin.left offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom } PopupPositionProvider.Align.BottomEnd -> { offsetX = anchorBounds.right - popupContentSize.width - popupMargin.right offsetY = anchorBounds.top - popupContentSize.height - popupMargin.bottom } else -> { // Fallback offsetX = if (alignment.resolve(layoutDirection) == PopupPositionProvider.Align.End) { anchorBounds.right - popupContentSize.width - popupMargin.right } else { anchorBounds.left + popupMargin.left } offsetY = if (windowBounds.bottom - anchorBounds.bottom > popupContentSize.height) { // Show below anchorBounds.bottom + popupMargin.bottom } else if (anchorBounds.top - windowBounds.top > popupContentSize.height) { // Show above anchorBounds.top - popupContentSize.height - popupMargin.top } else { // Middle anchorBounds.top + anchorBounds.height / 2 - popupContentSize.height / 2 } } } return IntOffset( x = offsetX.coerceIn( windowBounds.left, (windowBounds.right - popupContentSize.width - popupMargin.right).coerceAtLeast(windowBounds.left), ), y = offsetY.coerceIn( (windowBounds.top + popupMargin.top).coerceAtMost(windowBounds.bottom - popupContentSize.height - popupMargin.bottom), windowBounds.bottom - popupContentSize.height - popupMargin.bottom, ), ) } override fun getMargins(): PaddingValues = PaddingValues(start = 20.dp) } } private fun PopupPositionProvider.Align.resolve(layoutDirection: LayoutDirection): PopupPositionProvider.Align { if (layoutDirection == LayoutDirection.Ltr) return this return when (this) { PopupPositionProvider.Align.Start -> PopupPositionProvider.Align.End PopupPositionProvider.Align.End -> PopupPositionProvider.Align.Start PopupPositionProvider.Align.TopStart -> PopupPositionProvider.Align.TopEnd PopupPositionProvider.Align.TopEnd -> PopupPositionProvider.Align.TopStart PopupPositionProvider.Align.BottomStart -> PopupPositionProvider.Align.BottomEnd PopupPositionProvider.Align.BottomEnd -> PopupPositionProvider.Align.BottomStart } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/SearchStatus.kt ================================================ package me.weishu.kernelsu.ui.component import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import me.weishu.kernelsu.ui.util.defaultHazeEffect import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Stable data class SearchStatus( val label: String, val searchText: String = "", val current: Status = Status.COLLAPSED, val offsetY: Dp = 0.dp, val resultStatus: ResultStatus = ResultStatus.DEFAULT ) { fun isExpand() = current == Status.EXPANDED fun isCollapsed() = current == Status.COLLAPSED fun shouldExpand() = current == Status.EXPANDED || current == Status.EXPANDING fun shouldCollapsed() = current == Status.COLLAPSED || current == Status.COLLAPSING fun isAnimatingExpand() = current == Status.EXPANDING fun onAnimationComplete(): SearchStatus { return when (current) { Status.EXPANDING -> copy(current = Status.EXPANDED) Status.COLLAPSING -> copy(searchText = "", current = Status.COLLAPSED) else -> this } } @Composable fun TopAppBarAnim( modifier: Modifier = Modifier, visible: Boolean = shouldCollapsed(), hazeState: HazeState? = null, hazeStyle: HazeStyle? = null, content: @Composable () -> Unit ) { val topAppBarAlpha = animateFloatAsState( if (visible) 1f else 0f, animationSpec = tween(if (visible) 550 else 0, easing = FastOutSlowInEasing), ) Box(modifier = modifier) { Box( modifier = Modifier .matchParentSize() .then( if (hazeState != null && hazeStyle != null) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier.background(colorScheme.surface) } ) ) Box( modifier = Modifier .graphicsLayer { alpha = topAppBarAlpha.value } ) { content() } } } enum class Status { EXPANDED, EXPANDING, COLLAPSED, COLLAPSING } enum class ResultStatus { DEFAULT, EMPTY, LOAD, SHOW } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBar.kt ================================================ package me.weishu.kernelsu.ui.component.bottombar import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.core.tween import androidx.compose.foundation.gestures.animateScrollBy import androidx.compose.foundation.pager.PagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import com.kyant.backdrop.Backdrop import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Job import kotlinx.coroutines.job import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import kotlin.math.abs class MainPagerState( val pagerState: PagerState, private val coroutineScope: CoroutineScope ) { var selectedPage by mutableIntStateOf(pagerState.currentPage) private set var isNavigating by mutableStateOf(false) private set private var navJob: Job? = null fun animateToPage(targetIndex: Int) { if (targetIndex == selectedPage) return navJob?.cancel() selectedPage = targetIndex isNavigating = true val distance = abs(targetIndex - pagerState.currentPage).coerceAtLeast(2) val duration = 100 * distance + 100 val layoutInfo = pagerState.layoutInfo val pageSize = layoutInfo.pageSize + layoutInfo.pageSpacing val currentDistanceInPages = targetIndex - pagerState.currentPage - pagerState.currentPageOffsetFraction val scrollPixels = currentDistanceInPages * pageSize navJob = coroutineScope.launch { val myJob = coroutineContext.job try { pagerState.animateScrollBy( value = scrollPixels, animationSpec = tween(easing = EaseInOut, durationMillis = duration) ) } finally { if (navJob == myJob) { isNavigating = false if (pagerState.currentPage != targetIndex) { selectedPage = pagerState.currentPage } } } } } fun syncPage() { if (!isNavigating && selectedPage != pagerState.currentPage) { selectedPage = pagerState.currentPage } } } @Composable fun rememberMainPagerState( pagerState: PagerState, coroutineScope: CoroutineScope = rememberCoroutineScope() ): MainPagerState { return remember(pagerState, coroutineScope) { MainPagerState(pagerState, coroutineScope) } } @Composable fun BottomBar( hazeState: HazeState, hazeStyle: HazeStyle, backdrop: Backdrop, modifier: Modifier = Modifier, ) { when (LocalUiMode.current) { UiMode.Miuix -> BottomBarMiuix(hazeState, hazeStyle, backdrop, modifier) UiMode.Material -> BottomBarMaterial() } } @Composable fun SideRail( hazeState: HazeState, hazeStyle: HazeStyle, modifier: Modifier = Modifier, ) { when (LocalUiMode.current) { UiMode.Miuix -> NavigationRailMiuix(hazeState, hazeStyle, modifier) UiMode.Material -> NavigationRailMaterial(modifier) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBarMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.bottombar import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Extension import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Shield import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FlexibleBottomAppBar import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalMainPagerState import me.weishu.kernelsu.ui.util.rootAvailable @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun BottomBarMaterial() { val isManager = Natives.isManager val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() val mainPagerState = LocalMainPagerState.current if (!fullFeatured) return val items = listOf( Triple(R.string.home, Icons.Filled.Home, Icons.Outlined.Home), Triple(R.string.superuser, Icons.Filled.Shield, Icons.Outlined.Shield), Triple(R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension), Triple(R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings) ) FlexibleBottomAppBar( windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( WindowInsetsSides.Horizontal + WindowInsetsSides.Bottom ) ) { items.forEachIndexed { index, (label, selectedIcon, unselectedIcon) -> val selected = mainPagerState.selectedPage == index NavigationBarItem( selected = selected, onClick = { if (!selected) { mainPagerState.animateToPage(index) } }, icon = { Icon( if (selected) selectedIcon else unselectedIcon, stringResource(label) ) }, label = { Text( stringResource(label), maxLines = 1, overflow = TextOverflow.Ellipsis ) } ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/BottomBarMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.bottombar import androidx.annotation.StringRes import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Cottage import androidx.compose.material.icons.rounded.Extension import androidx.compose.material.icons.rounded.Security import androidx.compose.material.icons.rounded.Settings import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.backdrop.Backdrop import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.util.defaultHazeEffect import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalMainPagerState import me.weishu.kernelsu.ui.component.FloatingBottomBar import me.weishu.kernelsu.ui.component.FloatingBottomBarItem import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBar import me.weishu.kernelsu.ui.theme.LocalEnableFloatingBottomBarBlur import me.weishu.kernelsu.ui.util.rootAvailable import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.NavigationBar import top.yukonga.miuix.kmp.basic.NavigationBarItem import top.yukonga.miuix.kmp.basic.NavigationItem import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun BottomBarMiuix( hazeState: HazeState, hazeStyle: HazeStyle, backdrop: Backdrop, modifier: Modifier, ) { val isManager = Natives.isManager val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() if (!fullFeatured) return val mainState = LocalMainPagerState.current val enableBlur = LocalEnableBlur.current val enableFloatingBottomBar = LocalEnableFloatingBottomBar.current val enableFloatingBottomBarBlur = LocalEnableFloatingBottomBarBlur.current val items = BottomBarDestination.entries.map { destination -> NavigationItem( label = stringResource(destination.label), icon = destination.icon, ) } if (!enableFloatingBottomBar) { NavigationBar( modifier = modifier .then( if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else Modifier ), color = if (enableBlur) Color.Transparent else MiuixTheme.colorScheme.surface, content = { items.forEachIndexed { index, item -> NavigationBarItem( modifier = Modifier.weight(1f), icon = item.icon, label = item.label, selected = mainState.selectedPage == index, onClick = { mainState.animateToPage(index) } ) } } ) } else { FloatingBottomBar( modifier = modifier .clickable( interactionSource = remember { MutableInteractionSource() }, indication = null, onClick = {}, ) .padding(bottom = 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()), selectedIndex = { mainState.selectedPage }, onSelected = { mainState.animateToPage(it) }, backdrop = backdrop, tabsCount = items.size, isBlurEnabled = enableFloatingBottomBarBlur, ) { items.forEachIndexed { index, item -> FloatingBottomBarItem( onClick = { mainState.animateToPage(index) }, modifier = Modifier.defaultMinSize(minWidth = 76.dp) ) { Icon( imageVector = item.icon, contentDescription = item.label, tint = MiuixTheme.colorScheme.onSurface ) Text( text = item.label, fontSize = 11.sp, lineHeight = 14.sp, color = MiuixTheme.colorScheme.onSurface, maxLines = 1, softWrap = false, overflow = TextOverflow.Visible ) } } } } } enum class BottomBarDestination( @get:StringRes val label: Int, val icon: ImageVector, ) { Home(R.string.home, Icons.Rounded.Cottage), SuperUser(R.string.superuser, Icons.Rounded.Security), Module(R.string.module, Icons.Rounded.Extension), Setting(R.string.settings, Icons.Rounded.Settings) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/NavigationRailMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.bottombar import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.union import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Extension import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Shield import androidx.compose.material.icons.outlined.Extension import androidx.compose.material.icons.outlined.Home import androidx.compose.material.icons.outlined.Settings import androidx.compose.material.icons.outlined.Shield import androidx.compose.material3.Icon import androidx.compose.material3.NavigationRail import androidx.compose.material3.NavigationRailItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalMainPagerState import me.weishu.kernelsu.ui.util.rootAvailable @Composable fun NavigationRailMaterial( modifier: Modifier = Modifier, ) { val isManager = Natives.isManager val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() val mainPagerState = LocalMainPagerState.current if (!fullFeatured) return val items = listOf( Triple(R.string.home, Icons.Filled.Home, Icons.Outlined.Home), Triple(R.string.superuser, Icons.Filled.Shield, Icons.Outlined.Shield), Triple(R.string.module, Icons.Filled.Extension, Icons.Outlined.Extension), Triple(R.string.settings, Icons.Filled.Settings, Icons.Outlined.Settings) ) NavigationRail( modifier = modifier.fillMaxHeight(), windowInsets = WindowInsets.systemBars.union(WindowInsets.displayCutout).only( WindowInsetsSides.Start + WindowInsetsSides.Vertical ) ) { Spacer(Modifier.weight(1f)) items.forEachIndexed { index, (label, selectedIcon, unselectedIcon) -> val selected = mainPagerState.selectedPage == index NavigationRailItem( selected = selected, onClick = { if (!selected) { mainPagerState.animateToPage(index) } }, icon = { Icon( if (selected) selectedIcon else unselectedIcon, stringResource(label) ) }, label = { Text(stringResource(label)) } ) } Spacer(Modifier.weight(1f)) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/bottombar/NavigationRailMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.bottombar import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.util.defaultHazeEffect import me.weishu.kernelsu.ui.LocalMainPagerState import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.rootAvailable import top.yukonga.miuix.kmp.basic.NavigationRail import top.yukonga.miuix.kmp.basic.NavigationRailItem import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun NavigationRailMiuix( hazeState: HazeState, hazeStyle: HazeStyle, modifier: Modifier = Modifier, ) { val isManager = Natives.isManager val fullFeatured = isManager && !Natives.requireNewKernel() && rootAvailable() if (!fullFeatured) return val mainState = LocalMainPagerState.current val enableBlur = LocalEnableBlur.current val items = BottomBarDestination.entries.map { destination -> Pair(stringResource(destination.label), destination.icon) } NavigationRail( modifier = modifier .fillMaxHeight() .then( if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else Modifier ), color = if (enableBlur) Color.Transparent else MiuixTheme.colorScheme.surface, ) { Spacer(modifier = Modifier.weight(1f)) items.forEachIndexed { index, (label, icon) -> NavigationRailItem( icon = icon, label = label, selected = mainState.selectedPage == index, onClick = { mainState.animateToPage(index) }, modifier = Modifier.padding(vertical = 4.dp) ) } Spacer(modifier = Modifier.weight(1f)) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialog.kt ================================================ package me.weishu.kernelsu.ui.component.choosekmidialog import androidx.compose.runtime.Composable import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun ChooseKmiDialog( show: Boolean, onDismissRequest: () -> Unit, onSelected: (String?) -> Unit ) { when (LocalUiMode.current) { UiMode.Miuix -> ChooseKmiDialogMiuix(show, onDismissRequest, onSelected) UiMode.Material -> ChooseKmiDialogMaterial(show, onDismissRequest, onSelected) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialogMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.choosekmidialog import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedRadioItem import me.weishu.kernelsu.ui.util.getCurrentKmi import me.weishu.kernelsu.ui.util.getSupportedKmis @Composable fun ChooseKmiDialogMaterial( show: Boolean, onDismissRequest: () -> Unit, onSelected: (String?) -> Unit ) { if (!show) return val supportedKMIs by produceState(initialValue = emptyList()) { value = getSupportedKmis() } val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } val selectedKmi = remember(currentKmi) { mutableStateOf(currentKmi) } AlertDialog( onDismissRequest = { onDismissRequest() selectedKmi.value = currentKmi }, confirmButton = { TextButton( onClick = { onSelected(selectedKmi.value) onDismissRequest() }, enabled = supportedKMIs.contains(selectedKmi.value) ) { Text(stringResource(id = R.string.confirm)) } }, dismissButton = { TextButton(onClick = { onDismissRequest() selectedKmi.value = currentKmi }) { Text(stringResource(id = android.R.string.cancel)) } }, title = { Text( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp), text = stringResource(R.string.select_kmi), textAlign = TextAlign.Center ) }, text = { SegmentedColumn( content = supportedKMIs.map { kmi -> { SegmentedRadioItem( title = kmi, summary = if (kmi == currentKmi) stringResource(R.string.current_device_kmi) else null, selected = selectedKmi.value == kmi, onClick = { selectedKmi.value = kmi } ) } } ) } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/choosekmidialog/ChooseKmiDialogMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.choosekmidialog import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.util.getCurrentKmi import me.weishu.kernelsu.ui.util.getSupportedKmis import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.extra.CheckboxLocation import top.yukonga.miuix.kmp.extra.SuperCheckbox import top.yukonga.miuix.kmp.extra.SuperDialog @Composable fun ChooseKmiDialogMiuix( show: Boolean, onDismissRequest: () -> Unit, onSelected: (String?) -> Unit ) { val supportedKMIs by produceState(initialValue = emptyList()) { value = getSupportedKmis() } val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } val currentSelection = rememberSaveable(currentKmi) { mutableStateOf(currentKmi) } SuperDialog( show = show, title = stringResource(R.string.select_kmi), summary = stringResource(R.string.current_kmi, currentKmi.let { it.ifBlank { "Unknown" } }), onDismissRequest = { onDismissRequest() currentSelection.value = currentKmi }, insideMargin = DpSize(0.dp, 24.dp), content = { Column(modifier = Modifier.heightIn(max = 500.dp)) { LazyColumn(modifier = Modifier.weight(1f, fill = false)) { items(supportedKMIs) { kmi -> SuperCheckbox( title = kmi, summary = if (kmi == currentKmi) stringResource(R.string.current_device_kmi) else null, insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), checkboxLocation = CheckboxLocation.End, checked = currentSelection.value == kmi, holdDownState = currentSelection.value == kmi, onCheckedChange = { _ -> currentSelection.value = kmi } ) } } Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.padding(horizontal = 24.dp), horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { onDismissRequest() currentSelection.value = currentKmi }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f), ) Spacer(modifier = Modifier.width(20.dp)) TextButton( enabled = supportedKMIs.contains(currentSelection.value), onClick = { onSelected(currentSelection.value) onDismissRequest() }, text = stringResource(R.string.confirm), modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/Dialog.kt ================================================ package me.weishu.kernelsu.ui.component.dialog import android.os.Parcelable import android.util.Log import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.rememberSaveable import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.ReceiveChannel import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.consumeAsFlow import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import kotlin.coroutines.resume private const val TAG = "DialogComponent" interface ConfirmDialogVisuals : Parcelable { val title: String val content: String? val isMarkdown: Boolean val isHtml: Boolean val confirm: String? val dismiss: String? } @Parcelize private data class ConfirmDialogVisualsImpl( override val title: String, override val content: String?, override val isMarkdown: Boolean, override val isHtml: Boolean, override val confirm: String?, override val dismiss: String?, ) : ConfirmDialogVisuals { companion object { val Empty: ConfirmDialogVisuals = ConfirmDialogVisualsImpl("", "", isMarkdown = false, isHtml = false, confirm = null, dismiss = null) } } interface DialogHandle { val isShown: Boolean val dialogType: String fun show() fun hide() } interface LoadingDialogHandle : DialogHandle { suspend fun withLoading(block: suspend () -> R): R fun showLoading() } sealed interface ConfirmResult { object Confirmed : ConfirmResult object Canceled : ConfirmResult } interface ConfirmDialogHandle : DialogHandle { val visuals: ConfirmDialogVisuals fun showConfirm( title: String, content: String? = null, markdown: Boolean = false, html: Boolean = false, confirm: String? = null, dismiss: String? = null ) suspend fun awaitConfirm( title: String, content: String? = null, markdown: Boolean = false, html: Boolean = false, confirm: String? = null, dismiss: String? = null ): ConfirmResult } private abstract class DialogHandleBase( val visible: MutableState, val coroutineScope: CoroutineScope ) : DialogHandle { override val isShown: Boolean get() = visible.value override fun show() { coroutineScope.launch { visible.value = true } } final override fun hide() { coroutineScope.launch { visible.value = false } } override fun toString(): String { return dialogType } } private class LoadingDialogHandleImpl( visible: MutableState, coroutineScope: CoroutineScope ) : LoadingDialogHandle, DialogHandleBase(visible, coroutineScope) { override suspend fun withLoading(block: suspend () -> R): R { return coroutineScope.async { try { visible.value = true block() } finally { visible.value = false } }.await() } override fun showLoading() { show() } override val dialogType: String get() = "LoadingDialog" } typealias NullableCallback = (() -> Unit)? interface ConfirmCallback { val onConfirm: NullableCallback val onDismiss: NullableCallback val isEmpty: Boolean get() = onConfirm == null && onDismiss == null companion object { operator fun invoke( onConfirmProvider: () -> NullableCallback, onDismissProvider: () -> NullableCallback ): ConfirmCallback { return object : ConfirmCallback { override val onConfirm: NullableCallback get() = onConfirmProvider() override val onDismiss: NullableCallback get() = onDismissProvider() } } } } private class ConfirmDialogHandleImpl( visible: MutableState, coroutineScope: CoroutineScope, callback: ConfirmCallback, override var visuals: ConfirmDialogVisuals = ConfirmDialogVisualsImpl.Empty, private val resultFlow: ReceiveChannel ) : ConfirmDialogHandle, DialogHandleBase(visible, coroutineScope) { private class ResultCollector( private val callback: ConfirmCallback ) : FlowCollector { fun handleResult(result: ConfirmResult) { Log.d(TAG, "handleResult: ${result.javaClass.simpleName}") when (result) { ConfirmResult.Confirmed -> onConfirm() ConfirmResult.Canceled -> onDismiss() } } fun onConfirm() { callback.onConfirm?.invoke() } fun onDismiss() { callback.onDismiss?.invoke() } override suspend fun emit(value: ConfirmResult) { handleResult(value) } } private val resultCollector = ResultCollector(callback) private var awaitContinuation: CancellableContinuation? = null private val isCallbackEmpty = callback.isEmpty init { coroutineScope.launch { resultFlow .consumeAsFlow() .onEach { result -> awaitContinuation?.let { awaitContinuation = null if (it.isActive) { it.resume(result) } } } .onEach { hide() } .collect(resultCollector) } } private suspend fun awaitResult(): ConfirmResult { return suspendCancellableCoroutine { awaitContinuation = it.apply { if (isCallbackEmpty) { invokeOnCancellation { visible.value = false } } } } } fun updateVisuals(visuals: ConfirmDialogVisuals) { this.visuals = visuals } override fun show() { if (visuals !== ConfirmDialogVisualsImpl.Empty) { super.show() } else { throw UnsupportedOperationException("can't show confirm dialog with the Empty visuals") } } override fun showConfirm( title: String, content: String?, markdown: Boolean, html: Boolean, confirm: String?, dismiss: String? ) { coroutineScope.launch { updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, html, confirm, dismiss)) show() } } override suspend fun awaitConfirm( title: String, content: String?, markdown: Boolean, html: Boolean, confirm: String?, dismiss: String? ): ConfirmResult { coroutineScope.launch { updateVisuals(ConfirmDialogVisualsImpl(title, content, markdown, html, confirm, dismiss)) show() } return awaitResult() } override val dialogType: String get() = "ConfirmDialog" override fun toString(): String { return "${super.toString()}(visuals: $visuals)" } companion object { fun Saver( visible: MutableState, coroutineScope: CoroutineScope, callback: ConfirmCallback, resultChannel: ReceiveChannel ) = Saver( save = { it.visuals }, restore = { Log.d(TAG, "ConfirmDialog restore, visuals: $it") ConfirmDialogHandleImpl(visible, coroutineScope, callback, it, resultChannel) } ) } } @Composable fun rememberLoadingDialog(): LoadingDialogHandle { val visible = remember { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() when (LocalUiMode.current) { UiMode.Miuix -> LoadingDialogMiuix(visible) UiMode.Material -> LoadingDialogMaterial(visible) } return remember { LoadingDialogHandleImpl(visible, coroutineScope) } } @Composable private fun rememberConfirmDialog(visuals: ConfirmDialogVisuals, callback: ConfirmCallback): ConfirmDialogHandle { val visible = rememberSaveable { mutableStateOf(false) } val coroutineScope = rememberCoroutineScope() val resultChannel = remember { Channel() } val handle = rememberSaveable( saver = ConfirmDialogHandleImpl.Saver(visible, coroutineScope, callback, resultChannel), init = { ConfirmDialogHandleImpl(visible, coroutineScope, callback, visuals, resultChannel) } ) if (visible.value) { when (LocalUiMode.current) { UiMode.Miuix -> ConfirmDialogMiuix( handle.visuals, confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }, showDialog = visible ) UiMode.Material -> ConfirmDialogMaterial( handle.visuals, confirm = { coroutineScope.launch { resultChannel.send(ConfirmResult.Confirmed) } }, dismiss = { coroutineScope.launch { resultChannel.send(ConfirmResult.Canceled) } }, showDialog = visible ) } } return handle } @Composable fun rememberConfirmCallback(onConfirm: NullableCallback, onDismiss: NullableCallback): ConfirmCallback { val currentOnConfirm by rememberUpdatedState(newValue = onConfirm) val currentOnDismiss by rememberUpdatedState(newValue = onDismiss) return remember { ConfirmCallback({ currentOnConfirm }, { currentOnDismiss }) } } @Composable fun rememberConfirmDialog(onConfirm: NullableCallback = null, onDismiss: NullableCallback = null): ConfirmDialogHandle { return rememberConfirmDialog(rememberConfirmCallback(onConfirm, onDismiss)) } @Composable fun rememberConfirmDialog(callback: ConfirmCallback): ConfirmDialogHandle { return rememberConfirmDialog(ConfirmDialogVisualsImpl.Empty, callback) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/DialogMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.dialog import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import me.weishu.kernelsu.ui.component.GithubMarkdown import me.weishu.kernelsu.ui.component.Markdown @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun LoadingDialogMaterial(showDialog: MutableState) { if (showDialog.value) { Dialog( onDismissRequest = {}, properties = DialogProperties(dismissOnClickOutside = false, dismissOnBackPress = false) ) { Surface( modifier = Modifier.size(100.dp), shape = RoundedCornerShape(8.dp) ) { Box( contentAlignment = Alignment.Center, ) { LoadingIndicator() } } } } } @Composable fun ConfirmDialogMaterial( visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit, showDialog: MutableState ) { if (showDialog.value) { AlertDialog( onDismissRequest = { dismiss() showDialog.value = false }, title = { Text(visuals.title) }, text = { visuals.content?.let { content -> when { visuals.isMarkdown -> Markdown(content = content) visuals.isHtml -> GithubMarkdown(content = content, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh) else -> Text(text = content) } } }, confirmButton = { TextButton( onClick = { confirm() showDialog.value = false } ) { Text(visuals.confirm ?: stringResource(id = android.R.string.ok)) } }, dismissButton = { TextButton( onClick = { dismiss() showDialog.value = false } ) { Text(visuals.dismiss ?: stringResource(id = android.R.string.cancel)) } } ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/dialog/DialogMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.dialog import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxWidth 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.layout.windowInsetsPadding import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.GithubMarkdown import me.weishu.kernelsu.ui.component.Markdown import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.extra.WindowDialog import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun LoadingDialogMiuix(showDialog: MutableState) { WindowDialog( show = showDialog.value, content = { Box( modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.CenterStart ) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Start, ) { InfiniteProgressIndicator( color = MiuixTheme.colorScheme.onBackground ) Text( modifier = Modifier.padding(start = 12.dp), text = stringResource(R.string.processing), fontWeight = FontWeight.Medium ) } } } ) } @Composable fun ConfirmDialogMiuix( visuals: ConfirmDialogVisuals, confirm: () -> Unit, dismiss: () -> Unit, showDialog: MutableState ) { WindowDialog( show = showDialog.value, modifier = Modifier.windowInsetsPadding(WindowInsets.systemBars.only(WindowInsetsSides.Top)), title = visuals.title, onDismissRequest = { dismiss() showDialog.value = false }, content = { Layout( content = { visuals.content?.let { content -> when { visuals.isMarkdown -> Markdown(content = content) visuals.isHtml -> GithubMarkdown(content = content) else -> Text(text = content) } } Row( horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.padding(top = 12.dp) ) { TextButton( text = visuals.dismiss ?: stringResource(id = android.R.string.cancel), onClick = { dismiss() showDialog.value = false }, modifier = Modifier.weight(1f) ) Spacer(Modifier.width(20.dp)) TextButton( text = visuals.confirm ?: stringResource(id = android.R.string.ok), onClick = { confirm() showDialog.value = false }, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } ) { measurables, constraints -> if (measurables.size != 2) { val button = measurables[0].measure(constraints) layout(constraints.maxWidth, button.height) { button.place(0, 0) } } else { val button = measurables[1].measure(constraints) val lazyList = measurables[0].measure(constraints.copy(maxHeight = constraints.maxHeight - button.height)) layout(constraints.maxWidth, lazyList.height + button.height) { lazyList.place(0, 0) button.place(0, lazyList.height) } } } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/BaseFieldFilter.kt ================================================ package me.weishu.kernelsu.ui.component.filter import androidx.compose.runtime.mutableStateOf import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue open class BaseFieldFilter() { private var inputValue = mutableStateOf(TextFieldValue()) constructor(value: String) : this() { inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) } protected open fun onFilter(inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue): TextFieldValue { return TextFieldValue() } protected open fun computePos(): Int { // TODO return 0 } protected fun getNewTextRange( lastTextFiled: TextFieldValue, inputTextFieldValue: TextFieldValue ): TextRange? { return null } protected fun getNewText( lastTextFiled: TextFieldValue, inputTextFieldValue: TextFieldValue ): TextRange? { return null } fun setInputValue(value: String) { inputValue.value = TextFieldValue(value, TextRange(value.lastIndex + 1)) } fun getInputValue(): TextFieldValue { return inputValue.value } fun onValueChange(): (TextFieldValue) -> Unit { return { inputValue.value = onFilter(it, inputValue.value) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/filter/FilterNumber.kt ================================================ package me.weishu.kernelsu.ui.component.filter import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue class FilterNumber( private val value: Int, private val minValue: Int = Int.MIN_VALUE, private val maxValue: Int = Int.MAX_VALUE, ) : BaseFieldFilter(value.toString()) { override fun onFilter( inputTextFieldValue: TextFieldValue, lastTextFieldValue: TextFieldValue ): TextFieldValue { return filterInputNumber(inputTextFieldValue, lastTextFieldValue, minValue, maxValue) } private fun filterInputNumber( inputTextFieldValue: TextFieldValue, lastInputTextFieldValue: TextFieldValue, minValue: Int = Int.MIN_VALUE, maxValue: Int = Int.MAX_VALUE, ): TextFieldValue { val inputString = inputTextFieldValue.text lastInputTextFieldValue.text val newString = StringBuilder() val supportNegative = minValue < 0 var isNegative = false // 只允许负号在首位,并且只允许一个负号 if (supportNegative && inputString.isNotEmpty() && inputString.first() == '-') { isNegative = true newString.append('-') } for ((i, c) in inputString.withIndex()) { if (i == 0 && isNegative) continue // 首字符已经处理 when (c) { in '0'..'9' -> { newString.append(c) // 检查是否超出范围 val tempText = newString.toString() // 只在不是单独 '-' 时做判断(因为 '-' toInt 会异常) if (tempText != "-" && tempText.isNotEmpty()) { try { val tempValue = tempText.toInt() if (tempValue !in minValue..maxValue) { newString.deleteCharAt(newString.lastIndex) } } catch (e: NumberFormatException) { // 超出int范围 newString.deleteCharAt(newString.lastIndex) } } } // 忽略其他字符(包括点号) } } val textRange: TextRange if (inputTextFieldValue.selection.collapsed) { // 表示的是光标范围 if (inputTextFieldValue.selection.end != inputTextFieldValue.text.length) { // 光标没有指向末尾 var newPosition = inputTextFieldValue.selection.end + (newString.length - inputString.length) if (newPosition < 0) { newPosition = inputTextFieldValue.selection.end } textRange = TextRange(newPosition) } else { // 光标指向了末尾 textRange = TextRange(newString.length) } } else { textRange = TextRange(newString.length) } return lastInputTextFieldValue.copy( text = newString.toString(), selection = textRange ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/ExpressiveSwitch.kt ================================================ package me.weishu.kernelsu.ui.component.material import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Switch import androidx.compose.material3.SwitchColors import androidx.compose.material3.SwitchDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier @Composable fun ExpressiveSwitch( checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?, modifier: Modifier = Modifier, thumbContent: (@Composable () -> Unit)? = null, enabled: Boolean = true, colors: SwitchColors = SwitchDefaults.colors(), interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, showThumbIcon: Boolean = true, ) { Switch( checked = checked, onCheckedChange = onCheckedChange, modifier = modifier, thumbContent = thumbContent ?: if (showThumbIcon) { { if (checked) { Icon( imageVector = Icons.Filled.Check, contentDescription = null, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(SwitchDefaults.IconSize), ) } else { Icon( imageVector = Icons.Filled.Close, contentDescription = null, tint = MaterialTheme.colorScheme.surfaceContainerHighest, modifier = Modifier.size(SwitchDefaults.IconSize), ) } } } else null, enabled = enabled, colors = colors, interactionSource = interactionSource ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SearchBar.kt ================================================ package me.weishu.kernelsu.ui.component.material import androidx.activity.compose.BackHandler import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExpandedFullScreenContainedSearchBar import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.SearchBar import androidx.compose.material3.SearchBarDefaults import androidx.compose.material3.SearchBarValue import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Surface import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberContainedSearchBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalSoftwareKeyboardController import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.util.LocalSnackbarHost @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SearchAppBar( title: @Composable () -> Unit, searchText: String, onSearchTextChange: (String) -> Unit, onClearClick: () -> Unit, navigationIcon: @Composable (() -> Unit)? = null, actions: @Composable (() -> Unit)? = null, scrollBehavior: TopAppBarScrollBehavior? = null, searchContent: @Composable BoxScope.(bottomPadding: Dp, closeSearch: () -> Unit) -> Unit = { _, _ -> } ) { val keyboardController = LocalSoftwareKeyboardController.current val focusManager = LocalFocusManager.current val interactionSource = remember { MutableInteractionSource() } val scope = rememberCoroutineScope() val searchBarState = rememberContainedSearchBarState() val textFieldState = rememberTextFieldState() val currentQuery = textFieldState.text.toString() val latestSearchText by rememberUpdatedState(searchText) val isSearchExpanded = searchBarState.currentValue != SearchBarValue.Collapsed || searchBarState.targetValue != SearchBarValue.Collapsed var previousSearchBarValue by remember { mutableStateOf(searchBarState.currentValue) } var shouldClearOnCollapse by remember { mutableStateOf(true) } val clearSearchText: () -> Unit = { textFieldState.setTextAndPlaceCursorAtEnd("") onClearClick() } val collapseAndClear: () -> Unit = { shouldClearOnCollapse = false clearSearchText() scope.launch { searchBarState.animateToCollapsed() } focusManager.clearFocus() keyboardController?.hide() } DisposableEffect(Unit) { onDispose { keyboardController?.hide() } } LaunchedEffect(searchText) { val current = textFieldState.text.toString() if (current != searchText) { textFieldState.setTextAndPlaceCursorAtEnd(searchText) } } LaunchedEffect(textFieldState, latestSearchText) { snapshotFlow { textFieldState.text.toString() } .distinctUntilChanged() .collect { value -> if (value != latestSearchText) { onSearchTextChange(value) } } } LaunchedEffect(searchBarState) { snapshotFlow { searchBarState.currentValue } .distinctUntilChanged() .collect { value -> val collapsedFromExpanded = previousSearchBarValue != SearchBarValue.Collapsed && value == SearchBarValue.Collapsed previousSearchBarValue = value if (collapsedFromExpanded) { if (shouldClearOnCollapse) { clearSearchText() } shouldClearOnCollapse = true focusManager.clearFocus() keyboardController?.hide() } } } BackHandler(isSearchExpanded) { if (isSearchExpanded) { collapseAndClear() } } val inputField: @Composable () -> Unit = { SearchBarDefaults.InputField( textFieldState = textFieldState, searchBarState = searchBarState, onSearch = { focusManager.clearFocus() keyboardController?.hide() }, leadingIcon = { if (isSearchExpanded) { IconButton( onClick = { collapseAndClear() }, content = { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) } ) } else { Icon(Icons.Filled.Search, null) } }, trailingIcon = { if (isSearchExpanded && currentQuery.isNotEmpty()) { IconButton( onClick = { clearSearchText() }, content = { Icon(Icons.Filled.Close, null) } ) } }, interactionSource = interactionSource ) } Surface { Column( modifier = Modifier.fillMaxWidth() ) { LargeFlexibleTopAppBar( title = title, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), navigationIcon = { if (navigationIcon != null) navigationIcon() }, actions = { if (actions != null) actions() }, windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) SearchBar( modifier = Modifier .fillMaxWidth() .windowInsetsPadding(WindowInsets.safeDrawing.only(WindowInsetsSides.Horizontal)) .padding(horizontal = 16.dp) .padding(bottom = 8.dp), state = searchBarState, inputField = inputField, ) } } ExpandedFullScreenContainedSearchBar( state = searchBarState, inputField = inputField, windowInsets = { SearchBarDefaults.fullScreenWindowInsets.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) }, colors = SearchBarDefaults.colors( containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, inputFieldColors = SearchBarDefaults.inputFieldColors( focusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, unfocusedContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, disabledContainerColor = MaterialTheme.colorScheme.surfaceContainerHighest, ) ), content = { val snackBarHostState = LocalSnackbarHost.current val bottomPadding = SearchBarDefaults.fullScreenWindowInsets.asPaddingValues().calculateBottomPadding() Box(modifier = Modifier.fillMaxSize()) { if (currentQuery.isNotEmpty()) { searchContent(bottomPadding, collapseAndClear) } SnackbarHost( hostState = snackBarHostState, modifier = Modifier .align(Alignment.BottomCenter) .navigationBarsPadding() .imePadding() .padding(bottom = 16.dp) ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SegmentedList.kt ================================================ package me.weishu.kernelsu.ui.component.material import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.relocation.BringIntoViewRequester import androidx.compose.foundation.relocation.bringIntoViewRequester import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.ListItemShapes import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.RadioButton import androidx.compose.material3.SegmentedListItem import androidx.compose.material3.Text import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch @OptIn(ExperimentalMaterial3ExpressiveApi::class) val LocalListItemShapes = compositionLocalOf { null } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun defaultSegmentedColors(): ListItemColors = ListItemDefaults.segmentedColors().copy( containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), disabledContainerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), supportingContentColor = MaterialTheme.colorScheme.outline ) @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun defaultSingleSegmentedShape(index: Int, count: Int): ListItemShapes { val base = ListItemDefaults.segmentedShapes(index, count) return if (count == 1) { base.copy(shape = MaterialTheme.shapes.large) } else { base } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SegmentedColumn( modifier: Modifier = Modifier, title: String = "", visibleLen: Int = 0, content: List<@Composable () -> Unit>, ) { if (content.isEmpty()) return Column(modifier = modifier) { if (title.isNotEmpty()) { Text( text = title, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 16.dp, bottom = 8.dp) ) } Column(verticalArrangement = Arrangement.spacedBy(2.dp)) { content.forEachIndexed { index, itemContent -> CompositionLocalProvider( LocalListItemShapes provides defaultSingleSegmentedShape( index = index, count = if (visibleLen > 0) visibleLen else content.size ), ) { itemContent() } } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SegmentedLazyColumn( modifier: Modifier = Modifier, state: LazyListState = rememberLazyListState(), contentPadding: PaddingValues = PaddingValues(all = 16.dp), title: String = "", key: ((T) -> Any)? = null, items: List, itemContent: @Composable (T) -> Unit ) { Column(modifier = modifier) { if (title.isNotEmpty()) { Text( text = title, style = MaterialTheme.typography.titleSmall, color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(start = 16.dp, bottom = 8.dp) ) } LazyColumn( state = state, modifier = Modifier.fillMaxSize(), verticalArrangement = Arrangement.spacedBy(2.dp), contentPadding = contentPadding ) { itemsIndexed( items = items, key = if (key != null) { _, item -> key(item) } else null ) { index, item -> CompositionLocalProvider( LocalListItemShapes provides defaultSingleSegmentedShape(index, items.size), ) { itemContent(item) } } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SegmentedListItem( modifier: Modifier = Modifier, onClick: (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, enabled: Boolean = true, colors: ListItemColors = defaultSegmentedColors(), interactionSource: MutableInteractionSource? = null, headlineContent: @Composable () -> Unit, overlineContent: @Composable (() -> Unit)? = null, supportingContent: @Composable (() -> Unit)? = null, leadingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null, bottomContent: @Composable (() -> Unit)? = null, ) { Column(modifier = Modifier.fillMaxWidth()) { SegmentedListItem( onClick = onClick ?: {}, onLongClick = onLongClick, enabled = enabled, colors = colors, interactionSource = interactionSource, shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1), modifier = modifier, leadingContent = leadingContent, trailingContent = trailingContent, overlineContent = overlineContent, supportingContent = { Column { supportingContent?.invoke() bottomContent?.invoke() } }, verticalAlignment = Alignment.CenterVertically, content = headlineContent ) } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SegmentedListItem( checked: Boolean, onCheckedChange: (Boolean) -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ListItemColors = defaultSegmentedColors(), interactionSource: MutableInteractionSource? = null, headlineContent: @Composable () -> Unit, overlineContent: @Composable (() -> Unit)? = null, supportingContent: @Composable (() -> Unit)? = null, leadingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { SegmentedListItem( checked = checked, onCheckedChange = onCheckedChange, shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1), modifier = modifier, enabled = enabled, colors = colors, interactionSource = interactionSource, leadingContent = leadingContent, trailingContent = trailingContent, overlineContent = overlineContent, supportingContent = supportingContent, verticalAlignment = Alignment.CenterVertically, onLongClick = onLongClick, content = headlineContent ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun SegmentedListItem( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, colors: ListItemColors = defaultSegmentedColors(), interactionSource: MutableInteractionSource? = null, headlineContent: @Composable () -> Unit, overlineContent: @Composable (() -> Unit)? = null, supportingContent: @Composable (() -> Unit)? = null, leadingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null, onLongClick: (() -> Unit)? = null, ) { SegmentedListItem( selected = selected, onClick = onClick, shapes = LocalListItemShapes.current ?: ListItemDefaults.segmentedShapes(0, 1), modifier = modifier, enabled = enabled, colors = colors, interactionSource = interactionSource, leadingContent = leadingContent, trailingContent = trailingContent, overlineContent = overlineContent, supportingContent = supportingContent, verticalAlignment = Alignment.CenterVertically, onLongClick = onLongClick, content = headlineContent ) } @Composable fun SegmentedSwitchItem( icon: ImageVector? = null, title: String, summary: String? = null, colors: ListItemColors = defaultSegmentedColors(), checked: Boolean, enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } SegmentedListItem( onClick = { onCheckedChange(!checked) }, enabled = enabled, interactionSource = interactionSource, colors = colors, headlineContent = { Text(title) }, leadingContent = icon?.let { { Icon(it, title) } }, trailingContent = { ExpressiveSwitch( checked = checked, enabled = enabled, onCheckedChange = null, interactionSource = interactionSource, ) }, supportingContent = summary?.let { { Text(it) } } ) } @Composable fun SegmentedDropdownItem( icon: ImageVector? = null, title: String, summary: String? = null, items: List, colors: ListItemColors = defaultSegmentedColors(), enabled: Boolean = true, selectedIndex: Int, onItemSelected: (Int) -> Unit, ) { var expanded by remember { mutableStateOf(false) } val hasItems = items.isNotEmpty() val safeIndex = if (hasItems) { selectedIndex.coerceIn(0, items.lastIndex) } else { -1 } SegmentedListItem( onClick = if (enabled) { { expanded = true } } else null, enabled = enabled, colors = colors, leadingContent = icon?.let { { Icon(it, title) } }, headlineContent = { Text(text = title) }, supportingContent = summary?.let { { Text(it) } }, trailingContent = { Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { Text( text = if (hasItems && safeIndex >= 0) items[safeIndex] else "", textAlign = TextAlign.End, modifier = Modifier.fillMaxWidth(0.3f), color = if (enabled) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant ) DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { items.forEachIndexed { index, text -> DropdownMenuItem( text = { Text(text) }, onClick = { if (index in items.indices) { onItemSelected(index) } expanded = false } ) } } } } ) } @Composable fun SegmentedRadioItem( title: String, summary: String? = null, colors: ListItemColors = defaultSegmentedColors(), selected: Boolean, enabled: Boolean = true, onClick: () -> Unit, ) { SegmentedListItem( selected = selected, onClick = onClick, enabled = enabled, colors = colors, headlineContent = { Text(title) }, leadingContent = { RadioButton( selected = selected, onClick = null, enabled = enabled ) }, supportingContent = summary?.let { { Text(it) } } ) } @Composable fun SegmentedCheckboxItem( title: String, summary: String? = null, colors: ListItemColors = defaultSegmentedColors(), checked: Boolean, enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit, ) { val interactionSource = remember { MutableInteractionSource() } SegmentedListItem( checked = checked, onCheckedChange = onCheckedChange, enabled = enabled, colors = colors, interactionSource = interactionSource, headlineContent = { Text(title) }, leadingContent = { Checkbox( checked = checked, enabled = enabled, onCheckedChange = null, interactionSource = interactionSource, modifier = Modifier.size(24.dp) ) }, supportingContent = summary?.let { { Text(it) } } ) } @Composable fun SegmentedTextField( modifier: Modifier = Modifier, label: String = "", value: String, onValueChange: (String) -> Unit, enabled: Boolean = true, readOnly: Boolean = false, colors: ListItemColors = defaultSegmentedColors(), textStyle: TextStyle = MaterialTheme.typography.bodyLarge, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, keyboardActions: KeyboardActions = KeyboardActions.Default, singleLine: Boolean = false, maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, minLines: Int = 1, visualTransformation: VisualTransformation = VisualTransformation.None, onTextLayout: (TextLayoutResult) -> Unit = {}, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, cursorBrush: Brush = SolidColor(MaterialTheme.colorScheme.primary), placeholder: @Composable (() -> Unit)? = { Text("-") }, leadingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null, supportingContent: @Composable (() -> Unit)? = null, isError: Boolean = false ) { val bringIntoViewRequester = remember { BringIntoViewRequester() } val coroutineScope = rememberCoroutineScope() val focusRequester = remember { FocusRequester() } SegmentedListItem( modifier = modifier .bringIntoViewRequester(bringIntoViewRequester) .focusRequester(focusRequester), colors = colors, onClick = { focusRequester.requestFocus() }, leadingContent = leadingContent, supportingContent = supportingContent, trailingContent = trailingContent, headlineContent = { Column { if (label.isNotEmpty()) { Text(text = label, color = if (isError) MaterialTheme.colorScheme.error else colors.contentColor) } BasicTextField( value = value, onValueChange = onValueChange, modifier = Modifier .fillMaxWidth() .focusRequester(focusRequester) .onFocusChanged { if (it.isFocused) { coroutineScope.launch { bringIntoViewRequester.bringIntoView() } } }, enabled = enabled, readOnly = readOnly, textStyle = textStyle.copy( colors.supportingContentColor, fontSize = MaterialTheme.typography.bodyMedium.fontSize, lineHeight = MaterialTheme.typography.bodyMedium.lineHeight ), keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = singleLine, maxLines = maxLines, minLines = minLines, visualTransformation = visualTransformation, onTextLayout = onTextLayout, interactionSource = interactionSource, cursorBrush = cursorBrush, decorationBox = { innerTextField -> if (value.isEmpty() && placeholder != null) { Box(contentAlignment = Alignment.CenterStart) { CompositionLocalProvider( LocalContentColor provides colors.supportingContentColor ) { ProvideTextStyle(value = MaterialTheme.typography.bodyMedium) { placeholder() } } } } innerTextField() } ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SendLogBottomSheet.kt ================================================ package me.weishu.kernelsu.ui.component.material import android.content.Intent import android.net.Uri import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Share import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.LineHeightStyle import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.getBugreportFile import java.time.LocalDateTime import java.time.format.DateTimeFormatter @OptIn(ExperimentalMaterial3Api::class) @Composable fun SendLogBottomSheet(onDismiss: () -> Unit) { val context = LocalContext.current val logSaved = stringResource(R.string.log_saved) val sendLog = stringResource(R.string.send_log) val snackBarHost = LocalSnackbarHost.current val loadingDialog = rememberLoadingDialog() val haptic = LocalHapticFeedback.current val sheetState = rememberModalBottomSheetState() val scope = rememberCoroutineScope() val dismiss = { scope.launch { sheetState.hide() } onDismiss() } val exportBugreportLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("application/gzip") ) { uri: Uri? -> if (uri == null) return@rememberLauncherForActivityResult scope.launch(Dispatchers.IO) { loadingDialog.show() context.contentResolver.openOutputStream(uri)?.use { output -> getBugreportFile(context).inputStream().use { it.copyTo(output) } } loadingDialog.hide() snackBarHost.currentSnackbarData?.dismiss() snackBarHost.showSnackbar(logSaved) } } ModalBottomSheet( onDismissRequest = { dismiss() }, containerColor = MaterialTheme.colorScheme.surfaceContainer, content = { Row( modifier = Modifier .padding(top = 16.dp, bottom = 32.dp) .align(Alignment.CenterHorizontally) ) { Column(horizontalAlignment = Alignment.CenterHorizontally) { FilledTonalIconButton( modifier = Modifier.size(64.dp), onClick = { haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") val current = LocalDateTime.now().format(formatter) exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") dismiss() }) { Icon( Icons.Filled.Save, contentDescription = stringResource(id = R.string.save_log), modifier = Modifier.align(Alignment.CenterHorizontally) ) } Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(id = R.string.save_log), textAlign = TextAlign.Center.also { LineHeightStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None ) } ) } Spacer(Modifier.width(16.dp)) Column(horizontalAlignment = Alignment.CenterHorizontally) { FilledTonalIconButton( modifier = Modifier.size(64.dp), onClick = { haptic.performHapticFeedback(HapticFeedbackType.VirtualKey) scope.launch { val bugreport = loadingDialog.withLoading { withContext(Dispatchers.IO) { getBugreportFile(context) } } val uri: Uri = FileProvider.getUriForFile( context, "${BuildConfig.APPLICATION_ID}.fileprovider", bugreport ) val shareIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) setDataAndType(uri, "application/gzip") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } context.startActivity( Intent.createChooser( shareIntent, sendLog ) ) } }) { Icon( Icons.Filled.Share, contentDescription = stringResource(id = R.string.send_log), modifier = Modifier.align(Alignment.CenterHorizontally) ) } Spacer(modifier = Modifier.height(8.dp)) Text( text = stringResource(id = R.string.send_log), textAlign = TextAlign.Center.also { LineHeightStyle( alignment = LineHeightStyle.Alignment.Center, trim = LineHeightStyle.Trim.None ) }) } } }) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/material/SettingsItem.kt ================================================ package me.weishu.kernelsu.ui.component.material import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.selection.toggleable import androidx.compose.material3.Icon import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.semantics.Role @Composable fun SwitchItem( icon: ImageVector? = null, title: String, summary: String? = null, checked: Boolean, enabled: Boolean = true, onCheckedChange: (Boolean) -> Unit ) { val interactionSource = remember { MutableInteractionSource() } ListItem( modifier = Modifier .toggleable( value = checked, interactionSource = interactionSource, role = Role.Switch, enabled = enabled, indication = LocalIndication.current, onValueChange = onCheckedChange ), headlineContent = { Text(title) }, leadingContent = icon?.let { { Icon(icon, title) } }, trailingContent = { ExpressiveSwitch( checked = checked, enabled = enabled, onCheckedChange = onCheckedChange, interactionSource = interactionSource ) }, supportingContent = { if (summary != null) { Text(summary, color = MaterialTheme.colorScheme.outline) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/DropdownItem.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberUpdatedState import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import top.yukonga.miuix.kmp.basic.DropdownColors import top.yukonga.miuix.kmp.basic.DropdownDefaults import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun DropdownItem( text: String, optionSize: Int, index: Int, dropdownColors: DropdownColors = DropdownDefaults.dropdownColors(), onSelectedIndexChange: (Int) -> Unit ) { val currentOnSelectedIndexChange = rememberUpdatedState(onSelectedIndexChange) val additionalTopPadding = if (index == 0) 20f.dp else 12f.dp val additionalBottomPadding = if (index == optionSize - 1) 20f.dp else 12f.dp Row( modifier = Modifier .clickable { currentOnSelectedIndexChange.value(index) } .background(dropdownColors.containerColor) .padding(horizontal = 20.dp) .padding( top = additionalTopPadding, bottom = additionalBottomPadding ), verticalAlignment = Alignment.CenterVertically ) { Text( text = text, fontSize = MiuixTheme.textStyles.body1.fontSize, fontWeight = FontWeight.Medium, color = dropdownColors.contentColor, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/EditText.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.FocusInteraction import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.MutableState import androidx.compose.runtime.Stable import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.layout.Layout import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import kotlin.math.max @Composable fun EditText( title: String, summary: String? = null, textValue: MutableState, onTextValueChange: (String) -> Unit = {}, textHint: String = "", enabled: Boolean = true, keyboardOptions: KeyboardOptions = KeyboardOptions.Default, titleColor: BasicComponentColors = EditTextDefaults.titleColor(), summaryColor: BasicComponentColors = EditTextDefaults.summaryColor(), rightActionColor: BasicComponentColors = EditTextDefaults.rightActionColors(), isError: Boolean = false, ) { val interactionSource = remember { MutableInteractionSource() } val coroutineScope = rememberCoroutineScope() val focused = interactionSource.collectIsFocusedAsState().value val focusRequester = remember { FocusRequester() } if (focused) { focusRequester.requestFocus() } Box( modifier = Modifier .clickable( indication = null, interactionSource = null ) { if (enabled) { coroutineScope.launch { interactionSource.emit(FocusInteraction.Focus()) } } } .heightIn(min = 56.dp) .fillMaxWidth() .padding(EditTextDefaults.InsideMargin), ) { Layout( content = { Text( text = title, fontSize = MiuixTheme.textStyles.headline1.fontSize, fontWeight = FontWeight.Medium, color = titleColor.color(enabled) ) summary?.let { Text( text = it, fontSize = MiuixTheme.textStyles.body2.fontSize, color = summaryColor.color(enabled) ) } BasicTextField( value = textValue.value, onValueChange = { onTextValueChange(it) }, modifier = Modifier .focusRequester(focusRequester) .semantics { onClick { focusRequester.requestFocus() true } }, enabled = enabled, textStyle = MiuixTheme.textStyles.main.copy( textAlign = TextAlign.End, color = if (isError) { Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) } else { rightActionColor.color(enabled) } ), keyboardOptions = keyboardOptions, cursorBrush = SolidColor(colorScheme.primary), interactionSource = interactionSource, decorationBox = @Composable { innerTextField -> Box( contentAlignment = Alignment.CenterEnd ) { Text( text = if (textValue.value.isEmpty()) textHint else "", color = rightActionColor.color(enabled), textAlign = TextAlign.End, softWrap = false, maxLines = 1 ) innerTextField() } } ) } ) { measurables, constraints -> val leftConstraints = constraints.copy(maxWidth = constraints.maxWidth / 2) val hasSummary = measurables.size > 2 val titleText = measurables[0].measure(leftConstraints) val summaryText = (if (hasSummary) measurables[1] else null)?.measure(leftConstraints) val leftWidth = max(titleText.width, (summaryText?.width ?: 0)) val leftHeight = titleText.height + (summaryText?.height ?: 0) val rightWidth = constraints.maxWidth - leftWidth - 16.dp.roundToPx() val rightConstraints = constraints.copy(maxWidth = rightWidth) val inputField = (if (hasSummary) measurables[2] else measurables[1]).measure(rightConstraints) val totalHeight = max(leftHeight, inputField.height) layout(constraints.maxWidth, totalHeight) { val titleY = (totalHeight - leftHeight) / 2 titleText.placeRelative(0, titleY) summaryText?.placeRelative(0, titleY + titleText.height) inputField.placeRelative(constraints.maxWidth - inputField.width, (totalHeight - inputField.height) / 2) } } } } object EditTextDefaults { val InsideMargin = PaddingValues(16.dp) @Composable fun titleColor( color: Color = colorScheme.onSurface, disabledColor: Color = colorScheme.disabledOnSecondaryVariant ): BasicComponentColors { return BasicComponentColors( color = color, disabledColor = disabledColor ) } @Composable fun summaryColor( color: Color = colorScheme.onSurfaceVariantSummary, disabledColor: Color = colorScheme.disabledOnSecondaryVariant ): BasicComponentColors { return BasicComponentColors( color = color, disabledColor = disabledColor ) } @Composable fun rightActionColors( color: Color = colorScheme.onSurfaceVariantActions, disabledColor: Color = colorScheme.disabledOnSecondaryVariant, ): BasicComponentColors { return BasicComponentColors( color = color, disabledColor = disabledColor ) } } @Immutable class BasicComponentColors( private val color: Color, private val disabledColor: Color ) { @Stable fun color(enabled: Boolean): Color = if (enabled) color else disabledColor } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/ScaleDialog.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import top.yukonga.miuix.kmp.basic.ButtonDefaults 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.extra.SuperDialog import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable fun ScaleDialog( show: Boolean, onDismissRequest: () -> Unit, volumeState: () -> Float, onVolumeChange: (Float) -> Unit, ) { SuperDialog( show = show, title = stringResource(R.string.settings_page_scale), summary = "80% - 110%", onDismissRequest = onDismissRequest, content = { var text by remember(show) { mutableStateOf((volumeState() * 100).toInt().toString()) } TextField( modifier = Modifier.padding(bottom = 16.dp), value = text, maxLines = 1, trailingIcon = { Text( text = "%", modifier = Modifier.padding(horizontal = 16.dp), color = colorScheme.onSurfaceVariantActions, ) }, onValueChange = { newValue -> if (newValue.isEmpty()) { text = "" } else { val valid = newValue.all { it.isDigit() } if (valid) { text = newValue } } }, ) Row(horizontalArrangement = Arrangement.SpaceBetween) { TextButton( text = stringResource(android.R.string.cancel), onClick = onDismissRequest, modifier = Modifier.weight(1f), ) Spacer(Modifier.width(20.dp)) TextButton( text = stringResource(android.R.string.ok), onClick = { val parsed = text.toIntOrNull() val clamped = parsed?.coerceIn(80, 110) ?: (volumeState() * 100).toInt() onVolumeChange(clamped / 100f) onDismissRequest() }, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary(), ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SendLogDialog.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Save import androidx.compose.material.icons.rounded.Share import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import androidx.core.content.FileProvider import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.LoadingDialogHandle import me.weishu.kernelsu.ui.util.getBugreportFile import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDialog import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import java.time.LocalDateTime import java.time.format.DateTimeFormatter @Composable fun SendLogDialog( show: Boolean, onDismissRequest: () -> Unit, loadingDialog: LoadingDialogHandle, ) { val context = LocalContext.current val scope = rememberCoroutineScope() val logSavedText = stringResource(R.string.log_saved) val sendLogText = stringResource(R.string.send_log) val exportBugreportLauncher = rememberLauncherForActivityResult( ActivityResultContracts.CreateDocument("application/gzip") ) { uri: Uri? -> if (uri == null) return@rememberLauncherForActivityResult scope.launch(Dispatchers.IO) { loadingDialog.show() context.contentResolver.openOutputStream(uri)?.use { output -> getBugreportFile(context).inputStream().use { it.copyTo(output) } } loadingDialog.hide() withContext(Dispatchers.Main) { Toast.makeText(context, logSavedText, Toast.LENGTH_SHORT).show() } } } SuperDialog( show = show, onDismissRequest = onDismissRequest, insideMargin = DpSize(0.dp, 0.dp), content = { Text( modifier = Modifier .fillMaxWidth() .padding(top = 24.dp, bottom = 12.dp), text = stringResource(R.string.send_log), fontSize = MiuixTheme.textStyles.title4.fontSize, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, color = colorScheme.onSurface ) SuperArrow( title = stringResource(id = R.string.save_log), startAction = { Icon( Icons.Rounded.Save, contentDescription = null, modifier = Modifier.padding(end = 16.dp), tint = colorScheme.onSurface ) }, onClick = { val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") val current = LocalDateTime.now().format(formatter) exportBugreportLauncher.launch("KernelSU_bugreport_${current}.tar.gz") onDismissRequest() }, insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) ) SuperArrow( title = stringResource(id = R.string.send_log), startAction = { Icon( Icons.Rounded.Share, contentDescription = null, modifier = Modifier.padding(end = 16.dp), tint = colorScheme.onSurface ) }, onClick = { scope.launch { onDismissRequest() val bugreport = loadingDialog.withLoading { withContext(Dispatchers.IO) { getBugreportFile(context) } } val uri: Uri = FileProvider.getUriForFile( context, "${BuildConfig.APPLICATION_ID}.fileprovider", bugreport ) val shareIntent = Intent(Intent.ACTION_SEND).apply { putExtra(Intent.EXTRA_STREAM, uri) setDataAndType(uri, "application/gzip") addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) } context.startActivity( Intent.createChooser( shareIntent, sendLogText ) ) } }, insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) ) TextButton( text = stringResource(id = android.R.string.cancel), onClick = { onDismissRequest() }, modifier = Modifier .fillMaxWidth() .padding(top = 12.dp, bottom = 24.dp) .padding(horizontal = 24.dp) ) } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SuperEditArrow.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.filter.FilterNumber import top.yukonga.miuix.kmp.basic.BasicComponentColors import top.yukonga.miuix.kmp.basic.BasicComponentDefaults import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDialog @Composable fun SuperEditArrow( modifier: Modifier = Modifier, title: String, titleColor: BasicComponentColors = BasicComponentDefaults.titleColor(), defaultValue: Int = -1, summaryColor: BasicComponentColors = BasicComponentDefaults.summaryColor(), startAction: @Composable (() -> Unit)? = null, insideMargin: PaddingValues = BasicComponentDefaults.InsideMargin, enabled: Boolean = true, onValueChange: ((Int) -> Unit)? = null ) { val showDialog = remember { mutableStateOf(false) } val dialogTextFieldValue = remember { mutableIntStateOf(defaultValue) } SuperArrow( title = title, titleColor = titleColor, summary = dialogTextFieldValue.intValue.toString(), summaryColor = summaryColor, startAction = startAction, modifier = modifier, insideMargin = insideMargin, onClick = { showDialog.value = true }, holdDownState = showDialog.value, enabled = enabled ) EditDialog( title = title, show = showDialog.value, onDismissRequest = { showDialog.value = false }, dialogTextFieldValue = dialogTextFieldValue.intValue, onValueChange = { dialogTextFieldValue.intValue = it onValueChange?.invoke(dialogTextFieldValue.intValue) } ) } @Composable private fun EditDialog( title: String, show: Boolean, onDismissRequest: () -> Unit, dialogTextFieldValue: Int, onValueChange: (Int) -> Unit, ) { val inputTextFieldValue = remember { mutableIntStateOf(dialogTextFieldValue) } val filter = remember(key1 = inputTextFieldValue.intValue) { FilterNumber(dialogTextFieldValue) } SuperDialog( show = show, title = title, onDismissRequest = { onDismissRequest() filter.setInputValue(dialogTextFieldValue.toString()) }, content = { TextField( modifier = Modifier.padding(bottom = 16.dp), value = filter.getInputValue(), maxLines = 1, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Number, ), onValueChange = filter.onValueChange() ) Row( horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( text = stringResource(android.R.string.cancel), onClick = { onDismissRequest() filter.setInputValue(dialogTextFieldValue.toString()) }, modifier = Modifier.weight(1f) ) Spacer(Modifier.width(20.dp)) TextButton( text = stringResource(R.string.confirm), onClick = { onDismissRequest() with(filter.getInputValue().text) { if (isEmpty()) { onValueChange(0) filter.setInputValue("0") } else { onValueChange(this@with.toInt()) } } }, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/miuix/SuperSearchBar.kt ================================================ package me.weishu.kernelsu.ui.component.miuix import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.slideOutVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.layout.positionInWindow import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.onClick import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.zIndex import androidx.navigationevent.NavigationEventInfo import androidx.navigationevent.compose.NavigationBackHandler import androidx.navigationevent.compose.rememberNavigationEventState import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import me.weishu.kernelsu.ui.component.SearchStatus import me.weishu.kernelsu.ui.util.defaultHazeEffect import me.weishu.kernelsu.ui.theme.LocalEnableBlur import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.InputField import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.basic.Search import top.yukonga.miuix.kmp.icon.basic.SearchCleanup import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme // Search Box Composable @Composable fun SearchStatus.SearchBox( onSearchStatusChange: (SearchStatus) -> Unit, collapseBar: @Composable (SearchStatus, Dp, PaddingValues) -> Unit = { searchStatus, topPadding, innerPadding -> SearchBarFake(searchStatus.label, topPadding, innerPadding) }, searchBarTopPadding: Dp = 12.dp, contentPadding: PaddingValues = PaddingValues(0.dp), hazeState: HazeState? = null, hazeStyle: HazeStyle? = null, content: @Composable (MutableState) -> Unit ) { val searchStatus = this val density = LocalDensity.current val offsetY = remember { mutableIntStateOf(0) } val boxHeight = remember { mutableStateOf(0.dp) } Box( modifier = Modifier .fillMaxWidth() .zIndex(10f) .alpha(if (searchStatus.isCollapsed()) 1f else 0f) .offset(y = contentPadding.calculateTopPadding()) .onGloballyPositioned { it.positionInWindow().y.apply { offsetY.intValue = (this@apply * 0.9).toInt() with(density) { val newOffsetY = this@apply.toDp() val newBoxHeight = it.size.height.toDp() if (searchStatus.offsetY != newOffsetY) { onSearchStatusChange(searchStatus.copy(offsetY = newOffsetY)) } boxHeight.value = newBoxHeight } } } .pointerInput(Unit) { detectTapGestures { onSearchStatusChange(searchStatus.copy(current = SearchStatus.Status.EXPANDING)) } } .then( if (hazeState != null && hazeStyle != null) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier.background(colorScheme.surface) } ) ) { collapseBar(searchStatus, searchBarTopPadding, contentPadding) } Box { AnimatedVisibility( visible = searchStatus.shouldCollapsed(), enter = fadeIn(tween(300, easing = LinearOutSlowInEasing)) + slideInVertically( tween( 300, easing = LinearOutSlowInEasing ) ) { -offsetY.intValue }, exit = fadeOut(tween(300, easing = LinearOutSlowInEasing)) + slideOutVertically( tween( 300, easing = LinearOutSlowInEasing ) ) { -offsetY.intValue } ) { content(boxHeight) } } } // Search Pager Composable @Composable fun SearchStatus.SearchPager( onSearchStatusChange: (SearchStatus) -> Unit, defaultResult: @Composable () -> Unit, expandBar: @Composable (SearchStatus, (SearchStatus) -> Unit, Dp) -> Unit = { searchStatus, onStatusChange, padding -> SearchBar(searchStatus, onStatusChange, padding) }, searchBarTopPadding: Dp = 12.dp, result: @Composable () -> Unit ) { val searchStatus = this val systemBarsPadding = WindowInsets.systemBars.asPaddingValues().calculateTopPadding() val topPadding by animateDpAsState( targetValue = if (searchStatus.shouldExpand()) { systemBarsPadding + 5.dp } else { max(searchStatus.offsetY, 0.dp) }, animationSpec = tween(300, easing = LinearOutSlowInEasing), label = "SearchPagerTopPadding" ) { onSearchStatusChange(searchStatus.onAnimationComplete()) } val surfaceAlpha by animateFloatAsState( if (searchStatus.shouldExpand()) 1f else 0f, animationSpec = tween(200, easing = FastOutSlowInEasing), label = "SearchPagerSurfaceAlpha" ) val surfaceColor = colorScheme.surface Column( modifier = Modifier .fillMaxSize() .zIndex(5f) .drawBehind { drawRect(surfaceColor.copy(alpha = surfaceAlpha)) } .semantics { onClick { false } } .then( if (!searchStatus.isCollapsed()) Modifier.pointerInput(Unit) { } else Modifier ) ) { Row( Modifier .fillMaxWidth() .padding(top = topPadding) .then( if (!searchStatus.isCollapsed()) Modifier.background(colorScheme.surface) else Modifier ), horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically ) { if (!searchStatus.isCollapsed()) { Box( modifier = Modifier .weight(1f) .background(colorScheme.surface) ) { expandBar(searchStatus, onSearchStatusChange, searchBarTopPadding) } } AnimatedVisibility( visible = searchStatus.isExpand() || searchStatus.isAnimatingExpand(), enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }), exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }) ) { Text( text = stringResource(android.R.string.cancel), fontWeight = FontWeight.Bold, color = colorScheme.primary, modifier = Modifier .padding(start = 4.dp, end = 16.dp, top = searchBarTopPadding) .clickable( interactionSource = null, enabled = searchStatus.isExpand(), indication = null ) { onSearchStatusChange( searchStatus.copy( searchText = "", current = SearchStatus.Status.COLLAPSING ) ) } ) run { val navEventState = rememberNavigationEventState(NavigationEventInfo.None) NavigationBackHandler( state = navEventState, isBackEnabled = true, onBackCompleted = { onSearchStatusChange( searchStatus.copy( searchText = "", current = SearchStatus.Status.COLLAPSING ) ) } ) } } } AnimatedVisibility( visible = searchStatus.isExpand(), modifier = Modifier .fillMaxSize() .zIndex(1f), enter = fadeIn(), exit = fadeOut() ) { when (searchStatus.resultStatus) { SearchStatus.ResultStatus.DEFAULT -> defaultResult() SearchStatus.ResultStatus.EMPTY -> {} SearchStatus.ResultStatus.LOAD -> {} SearchStatus.ResultStatus.SHOW -> result() } } } } @Composable fun SearchBar( searchStatus: SearchStatus, onSearchStatusChange: (SearchStatus) -> Unit, searchBarTopPadding: Dp = 12.dp, ) { val focusRequester = remember { FocusRequester() } var expanded by rememberSaveable { mutableStateOf(false) } InputField( query = searchStatus.searchText, onQueryChange = { onSearchStatusChange(searchStatus.copy(searchText = it)) }, label = "", leadingIcon = { Icon( imageVector = MiuixIcons.Basic.Search, contentDescription = "back", modifier = Modifier .size(44.dp) .padding(start = 16.dp, end = 8.dp), tint = colorScheme.onSurfaceContainerHigh, ) }, trailingIcon = { AnimatedVisibility( searchStatus.searchText.isNotEmpty(), enter = fadeIn() + scaleIn(), exit = fadeOut() + scaleOut(), ) { Icon( imageVector = MiuixIcons.Basic.SearchCleanup, tint = colorScheme.onSurface, contentDescription = "Clean", modifier = Modifier .size(44.dp) .padding(start = 8.dp, end = 16.dp) .clickable( interactionSource = null, indication = null ) { onSearchStatusChange(searchStatus.copy(searchText = "")) }, ) } }, modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(top = searchBarTopPadding, bottom = 6.dp) .focusRequester(focusRequester), onSearch = { }, expanded = searchStatus.shouldExpand(), onExpandedChange = { onSearchStatusChange( searchStatus.copy( current = if (it) SearchStatus.Status.EXPANDED else SearchStatus.Status.COLLAPSED ) ) } ) LaunchedEffect(Unit) { if (!expanded && searchStatus.shouldExpand()) { focusRequester.requestFocus() expanded = true } } } @Composable fun SearchBarFake( label: String, searchBarTopPadding: Dp = 12.dp, innerPadding: PaddingValues = PaddingValues(0.dp) ) { val layoutDirection = LocalLayoutDirection.current val enableBlur = LocalEnableBlur.current InputField( query = "", onQueryChange = { }, label = label, leadingIcon = { Icon( imageVector = MiuixIcons.Basic.Search, contentDescription = "Clean", modifier = Modifier .size(44.dp) .padding(start = 16.dp, end = 8.dp), tint = colorScheme.onSurfaceContainerHigh, ) }, modifier = Modifier .let { if (!enableBlur) it.background(colorScheme.surface) else it } .fillMaxWidth() .padding(horizontal = 12.dp) .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ) .padding(top = searchBarTopPadding, bottom = 6.dp), onSearch = { }, enabled = false, expanded = false, onExpandedChange = { } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfigMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem import me.weishu.kernelsu.ui.component.material.SegmentedTextField @Composable fun AppProfileConfigMaterial( modifier: Modifier = Modifier, fixedName: Boolean, enabled: Boolean, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { Column(modifier = modifier) { if (!fixedName) { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf { SegmentedTextField( value = profile.name, onValueChange = { onProfileChange(profile.copy(name = it)) }, label = stringResource(R.string.profile_name), readOnly = !enabled, singleLine = true ) } ) } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf { SegmentedSwitchItem( title = stringResource(R.string.profile_umount_modules), summary = stringResource(R.string.profile_umount_modules_summary), checked = if (enabled) { profile.umountModules } else { Natives.isDefaultUmountModules() }, enabled = enabled, onCheckedChange = { onProfileChange( profile.copy( umountModules = it, nonRootUseDefault = false ) ) } ) } ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/AppProfileConfigMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.Column import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.miuix.EditText import top.yukonga.miuix.kmp.extra.SuperSwitch @Composable fun AppProfileConfigMiuix( modifier: Modifier = Modifier, fixedName: Boolean, enabled: Boolean, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { Column(modifier = modifier) { if (!fixedName) { EditText( title = stringResource(R.string.profile_name), textValue = remember { mutableStateOf(profile.name) }, onTextValueChange = { onProfileChange(profile.copy(name = it)) }, enabled = enabled, ) } SuperSwitch( title = stringResource(R.string.profile_umount_modules), summary = stringResource(R.string.profile_umount_modules_summary), checked = if (enabled) { profile.umountModules } else { Natives.isDefaultUmountModules() }, enabled = enabled, onCheckedChange = { onProfileChange( profile.copy( umountModules = it, nonRootUseDefault = false ) ) } ) } } @Preview @Composable private fun AppProfileConfigPreview() { var profile by remember { mutableStateOf(Natives.Profile("")) } AppProfileConfigMiuix(fixedName = true, enabled = false, profile = profile) { profile = it } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/ProfileConfig.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun AppProfileConfig( modifier: Modifier = Modifier, fixedName: Boolean, enabled: Boolean, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { when (LocalUiMode.current) { UiMode.Miuix -> AppProfileConfigMiuix( modifier = modifier, fixedName = fixedName, enabled = enabled, profile = profile, onProfileChange = onProfileChange ) UiMode.Material -> AppProfileConfigMaterial( modifier = modifier, fixedName = fixedName, enabled = enabled, profile = profile, onProfileChange = onProfileChange ) } } @Composable fun RootProfileConfig( modifier: Modifier = Modifier, fixedName: Boolean, enabled: Boolean = true, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { when (LocalUiMode.current) { UiMode.Miuix -> RootProfileConfigMiuix( modifier = modifier, fixedName = fixedName, enabled = enabled, profile = profile, onProfileChange = onProfileChange ) UiMode.Material -> RootProfileConfigMaterial( modifier = modifier, enabled = enabled, profile = profile, onProfileChange = onProfileChange ) } } @Composable fun TemplateConfig( modifier: Modifier = Modifier, profile: Natives.Profile, onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit ) { when (LocalUiMode.current) { UiMode.Miuix -> TemplateConfigMiuix( modifier = modifier, profile = profile, onViewTemplate = onViewTemplate, onManageTemplate = onManageTemplate, onProfileChange = onProfileChange ) UiMode.Material -> TemplateConfigMaterial( profile = profile, onViewTemplate = onViewTemplate, onManageTemplate = onManageTemplate, onProfileChange = onProfileChange ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfigMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.material.SegmentedTextField import me.weishu.kernelsu.ui.component.profile.dialogs.MultiSelectDialog import me.weishu.kernelsu.ui.component.profile.dialogs.SingleSelectDialog import me.weishu.kernelsu.ui.util.isSepolicyValid @Composable fun RootProfileConfigMaterial( modifier: Modifier = Modifier, enabled: Boolean = true, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit ) { Column( modifier = modifier.padding(vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { UidGidPanel( enabled = enabled, uid = profile.uid, gid = profile.gid, onUidChange = { onProfileChange(profile.copy(uid = it, rootUseDefault = false)) }, onGidChange = { onProfileChange(profile.copy(gid = it, rootUseDefault = false)) } ) GroupsPanel( enabled = enabled, selected = profile.groups.mapNotNull { gid -> Groups.entries.find { it.gid == gid } }, onSelectionChange = { selection -> onProfileChange( profile.copy( groups = selection.map { it.gid }, rootUseDefault = false ) ) } ) CapsPanel( enabled = enabled, selected = profile.capabilities, onSelectionChange = { selection -> onProfileChange( profile.copy( capabilities = selection.map { it.cap }, rootUseDefault = false ) ) } ) MountNameSpacePanel( enabled = enabled, namespace = profile.namespace, onNamespaceChange = { onProfileChange(profile.copy(namespace = it, rootUseDefault = false)) } ) SELinuxPanel( enabled = enabled, context = profile.context, rules = profile.rules, onContextChange = { domain -> onProfileChange(profile.copy(context = domain, rootUseDefault = false)) }, onRulesChange = { rules -> onProfileChange(profile.copy(rules = rules, rootUseDefault = false)) } ) } } @Composable private fun UidGidPanel( enabled: Boolean, uid: Int, gid: Int, onUidChange: (Int) -> Unit, onGidChange: (Int) -> Unit ) { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf( { SegmentedTextField( enabled = enabled, value = uid.toString(), onValueChange = { onUidChange(it.toIntOrNull() ?: 0) }, label = "UID", singleLine = true ) }, { SegmentedTextField( enabled = enabled, value = gid.toString(), onValueChange = { onGidChange(it.toIntOrNull() ?: 0) }, label = "GID", singleLine = true ) } ) ) } @Composable private fun GroupsPanel( enabled: Boolean, selected: List, onSelectionChange: (Set) -> Unit ) { val showDialog = remember { mutableStateOf(false) } val groups = remember { Groups.entries.sortedWith( compareBy { when (it) { Groups.ROOT -> 0 Groups.SYSTEM -> 1 Groups.SHELL -> 2 else -> Int.MAX_VALUE } } .then(compareBy { it.name }) ) } if (showDialog.value) { MultiSelectDialog( title = "Groups", subtitle = "${selected.size} / 32", items = groups, selectedItems = selected.toSet(), itemTitle = { it.display }, itemSubtitle = { it.desc }, maxSelection = 32, onSelectionChange = { onSelectionChange(it) }, onDismiss = { showDialog.value = false } ) } val tag = if (selected.isEmpty()) { "None" } else { selected.joinToString(", ") { it.display } } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile_groups)) }, supportingContent = { Text(tag) }, onClick = if (enabled) { { showDialog.value = true } } else null ) } ) } @Composable private fun MountNameSpacePanel( enabled: Boolean, namespace: Int, onNamespaceChange: (Int) -> Unit ) { data class NamespaceOption( val value: Int, val label: String ) val showDialog = remember { mutableStateOf(false) } val inheritedLabel = stringResource(R.string.profile_namespace_inherited) val globalLabel = stringResource(R.string.profile_namespace_global) val individualLabel = stringResource(R.string.profile_namespace_individual) val options = remember(inheritedLabel, globalLabel, individualLabel) { listOf( NamespaceOption(0, inheritedLabel), NamespaceOption(1, globalLabel), NamespaceOption(2, individualLabel) ) } val selectedOption = options.find { it.value == namespace } ?: options[0] if (showDialog.value) { SingleSelectDialog( title = stringResource(R.string.profile_namespace), items = options, selectedItem = selectedOption, itemTitle = { it.label }, onConfirm = { onNamespaceChange(it.value) showDialog.value = false }, onDismiss = { showDialog.value = false } ) } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile_namespace)) }, supportingContent = { Text(selectedOption.label) }, onClick = if (enabled) { { showDialog.value = true } } else null ) } ) } @Composable private fun CapsPanel( enabled: Boolean, selected: List, onSelectionChange: (Set) -> Unit ) { val showDialog = remember { mutableStateOf(false) } val selectedCaps = remember(selected) { selected.mapNotNull { cap -> Capabilities.entries.find { it.cap == cap } } } val capabilities = remember { Capabilities.entries.sortedBy { it.display } } if (showDialog.value) { MultiSelectDialog( title = "Capabilities", subtitle = "${selectedCaps.size} / ${Capabilities.entries.size}", items = capabilities, selectedItems = selectedCaps.toSet(), itemTitle = { it.display }, itemSubtitle = { null }, maxSelection = Int.MAX_VALUE, onSelectionChange = { onSelectionChange(it) }, onDismiss = { showDialog.value = false } ) } val tag = if (selectedCaps.isEmpty()) { "None" } else { selectedCaps.joinToString(", ") { it.display } } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile_capabilities)) }, supportingContent = { Text(tag) }, onClick = if (enabled) { { showDialog.value = true } } else null ) } ) } @Composable private fun SELinuxPanel( enabled: Boolean, context: String, rules: String, onContextChange: (String) -> Unit, onRulesChange: (String) -> Unit ) { val showDialog = remember { mutableStateOf(false) } if (showDialog.value) { SELinuxDialog( domain = context, rules = rules, onConfirm = { domain, r -> onContextChange(domain) onRulesChange(r) showDialog.value = false }, onDismiss = { showDialog.value = false } ) } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = listOf { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile_selinux_context)) }, supportingContent = { Text(context.ifEmpty { "—" }) }, onClick = if (enabled) { { showDialog.value = true } } else null ) } ) } @Composable private fun SELinuxDialog( domain: String, rules: String, onConfirm: (String, String) -> Unit, onDismiss: () -> Unit ) { var currentDomain by remember { mutableStateOf(domain) } var currentRules by remember { mutableStateOf(rules) } val isDomainValid = remember(currentDomain) { val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") currentDomain.matches(regex) } val isRulesValid = remember(currentRules) { isSepolicyValid(currentRules) } AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(R.string.profile_selinux_context)) }, text = { Column { OutlinedTextField( value = currentDomain, onValueChange = { currentDomain = it }, label = { Text(stringResource(R.string.profile_selinux_domain)) }, isError = !isDomainValid, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next ), singleLine = true ) Spacer(Modifier.height(8.dp)) OutlinedTextField( value = currentRules, onValueChange = { currentRules = it }, label = { Text(stringResource(R.string.profile_selinux_rules)) }, isError = !isRulesValid, modifier = Modifier.fillMaxWidth(), keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Ascii ), minLines = 3, maxLines = 10 ) } }, confirmButton = { TextButton( onClick = { onConfirm(currentDomain, currentRules) }, enabled = isDomainValid && isRulesValid ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/RootProfileConfigMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups import me.weishu.kernelsu.ui.component.miuix.SuperEditArrow import me.weishu.kernelsu.ui.util.isSepolicyValid import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TextField import top.yukonga.miuix.kmp.extra.CheckboxLocation import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperCheckbox import top.yukonga.miuix.kmp.extra.SuperDialog import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable fun RootProfileConfigMiuix( modifier: Modifier = Modifier, fixedName: Boolean, enabled: Boolean = true, profile: Natives.Profile, onProfileChange: (Natives.Profile) -> Unit, ) { Column( modifier = modifier ) { if (!fixedName) { TextField( enabled = enabled, label = stringResource(R.string.profile_name), value = profile.name, onValueChange = { onProfileChange(profile.copy(name = it)) } ) } SuperEditArrow( enabled = enabled, title = "UID", defaultValue = profile.uid, ) { onProfileChange( profile.copy( uid = it, rootUseDefault = false ) ) } SuperEditArrow( enabled = enabled, title = "GID", defaultValue = profile.gid, ) { onProfileChange( profile.copy( gid = it, rootUseDefault = false ) ) } val selectedGroups = profile.groups.let { e -> e.mapNotNull { g -> Groups.entries.find { it.gid == g } } } GroupsPanel(enabled, selectedGroups) { onProfileChange( profile.copy( groups = it.map { group -> group.gid }, rootUseDefault = false ) ) } val selectedCaps = profile.capabilities.mapNotNull { e -> Capabilities.entries.find { it.cap == e } } CapsPanel(enabled, selectedCaps) { onProfileChange( profile.copy( capabilities = it.map { cap -> cap.cap }, rootUseDefault = false ) ) } MountNameSpacePanel(enabled = enabled, profile = profile) { onProfileChange( profile.copy( namespace = it, rootUseDefault = false ) ) } SELinuxPanel(enabled = enabled, profile = profile, onSELinuxChange = { domain, rules -> onProfileChange( profile.copy( context = domain, rules = rules, rootUseDefault = false ) ) }) } } @Composable private fun GroupsPanel( enabled: Boolean, selected: List, closeSelection: (selection: Set) -> Unit ) { val showDialog = remember { mutableStateOf(false) } val groups = remember { Groups.entries.toTypedArray().sortedWith( compareBy { when (it) { Groups.ROOT -> 0 Groups.SYSTEM -> 1 Groups.SHELL -> 2 else -> Int.MAX_VALUE } } .then(compareBy { it.name }) ) } val currentSelection = remember { mutableStateOf(selected.toSet()) } SuperDialog( show = showDialog.value, title = stringResource(R.string.profile_groups), summary = "${currentSelection.value.size} / 32", onDismissRequest = { showDialog.value = false }, insideMargin = DpSize(0.dp, 24.dp), content = { Column(modifier = Modifier.heightIn(max = 500.dp)) { LazyColumn(modifier = Modifier.weight(1f, fill = false)) { items(groups) { group -> SuperCheckbox( title = group.display, summary = group.desc, insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), checkboxLocation = CheckboxLocation.End, checked = currentSelection.value.contains(group), holdDownState = currentSelection.value.contains(group), onCheckedChange = { isChecked -> val newSelection = currentSelection.value.toMutableSet() if (isChecked) { if (newSelection.size < 32) newSelection.add(group) } else { newSelection.remove(group) } currentSelection.value = newSelection } ) } } Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.padding(horizontal = 24.dp), horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { currentSelection.value = selected.toSet() showDialog.value = false }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f), ) Spacer(modifier = Modifier.width(20.dp)) TextButton( onClick = { closeSelection(currentSelection.value) showDialog.value = false }, text = stringResource(R.string.confirm), modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) val tag = if (selected.isEmpty()) { "None" } else { selected.joinToString(separator = ",", transform = { it.display }) } SuperArrow( enabled = enabled, title = stringResource(R.string.profile_groups), summary = tag, onClick = { showDialog.value = true } ) } @Composable private fun MountNameSpacePanel( enabled: Boolean, profile: Natives.Profile, onMntNamespaceChange: (namespaceType: Int) -> Unit ) { SuperDropdown( enabled = enabled, title = stringResource(id = R.string.profile_namespace), items = listOf( stringResource(id = R.string.profile_namespace_inherited), stringResource(id = R.string.profile_namespace_global), stringResource(id = R.string.profile_namespace_individual), ), selectedIndex = profile.namespace, onSelectedIndexChange = { index -> onMntNamespaceChange(index) } ) } @Composable private fun CapsPanel( enabled: Boolean, selected: Collection, closeSelection: (selection: Set) -> Unit ) { val showDialog = remember { mutableStateOf(false) } val caps = remember { Capabilities.entries.toTypedArray().sortedBy { it.display } } val currentSelection = remember { mutableStateOf(selected.toSet()) } SuperDialog( show = showDialog.value, title = stringResource(R.string.profile_capabilities), onDismissRequest = { showDialog.value = false }, insideMargin = DpSize(0.dp, 24.dp), content = { Column(modifier = Modifier.heightIn(max = 500.dp)) { LazyColumn(modifier = Modifier.weight(1f, fill = false)) { items(caps) { cap -> SuperCheckbox( title = cap.display, summary = cap.desc, insideMargin = PaddingValues(horizontal = 30.dp, vertical = 16.dp), checkboxLocation = CheckboxLocation.End, checked = currentSelection.value.contains(cap), holdDownState = currentSelection.value.contains(cap), onCheckedChange = { isChecked -> val newSelection = currentSelection.value.toMutableSet() if (isChecked) { newSelection.add(cap) } else { newSelection.remove(cap) } currentSelection.value = newSelection } ) } } Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.padding(horizontal = 24.dp), horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { showDialog.value = false currentSelection.value = selected.toSet() }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(20.dp)) TextButton( onClick = { closeSelection(currentSelection.value) showDialog.value = false }, text = stringResource(R.string.confirm), modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) val tag = if (selected.isEmpty()) { "None" } else { selected.joinToString(separator = ",", transform = { it.display }) } SuperArrow( enabled = enabled, title = stringResource(R.string.profile_capabilities), summary = tag, onClick = { showDialog.value = true } ) } @Composable private fun SELinuxPanel( enabled: Boolean, profile: Natives.Profile, onSELinuxChange: (domain: String, rules: String) -> Unit ) { val showDialog = remember { mutableStateOf(false) } var domain by remember { mutableStateOf(profile.context) } var rules by remember { mutableStateOf(profile.rules) } val isDomainValid = remember(domain) { val regex = Regex("^[a-z_]+:[a-z0-9_]+:[a-z0-9_]+(:[a-z0-9_]+)?$") domain.matches(regex) } val isRulesValid = remember(rules) { isSepolicyValid(rules) } SuperDialog( show = showDialog.value, title = stringResource(R.string.profile_selinux_context), onDismissRequest = { showDialog.value = false }, content = { Column(modifier = Modifier.heightIn(max = 500.dp)) { Column(modifier = Modifier.weight(1f, fill = false)) { TextField( value = domain, onValueChange = { domain = it }, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), label = stringResource(id = R.string.profile_selinux_domain), borderColor = if (isDomainValid) { colorScheme.primary } else { Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Ascii, imeAction = ImeAction.Next ), singleLine = true ) TextField( value = rules, onValueChange = { rules = it }, modifier = Modifier .fillMaxWidth() .padding(vertical = 8.dp), label = stringResource(id = R.string.profile_selinux_rules), borderColor = if (isRulesValid) { colorScheme.primary } else { Color.Red.copy(alpha = if (isSystemInDarkTheme()) 0.3f else 0.6f) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Ascii, ), singleLine = false ) } Spacer(Modifier.height(12.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { showDialog.value = false }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f) ) Spacer(modifier = Modifier.width(20.dp)) TextButton( onClick = { onSELinuxChange(domain, rules) showDialog.value = false }, text = stringResource(R.string.confirm), enabled = isDomainValid && isRulesValid, modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) SuperArrow( enabled = enabled, title = stringResource(R.string.profile_selinux_context), summary = profile.context, onClick = { showDialog.value = true } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfigMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ReadMore import androidx.compose.material.icons.filled.Create import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.profile.dialogs.SingleSelectDialog import me.weishu.kernelsu.ui.util.listAppProfileTemplates import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById private data class TemplateOption( val id: String, val name: String ) @Composable fun TemplateConfigMaterial( profile: Natives.Profile, onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit ) { val showDialog = remember { mutableStateOf(false) } val template = rememberSaveable { mutableStateOf(profile.rootTemplate ?: "") } val profileTemplates = listAppProfileTemplates() val noTemplates = profileTemplates.isEmpty() val templateOptions = remember(profileTemplates) { profileTemplates.map { tid -> TemplateOption(tid, tid) } } val selectedTemplate = remember(template.value, templateOptions) { templateOptions.find { it.id == template.value } ?: templateOptions.firstOrNull() } if (showDialog.value && !noTemplates) { SingleSelectDialog( title = stringResource(R.string.profile_template), items = templateOptions, selectedItem = selectedTemplate ?: templateOptions.first(), itemTitle = { it.name }, onConfirm = { selected -> val tid = selected.id val templateInfo = getTemplateInfoById(tid) if (templateInfo != null && setSepolicy(tid, templateInfo.rules.joinToString("\n"))) { onProfileChange( profile.copy( rootTemplate = tid, rootUseDefault = false, uid = templateInfo.uid, gid = templateInfo.gid, groups = templateInfo.groups, capabilities = templateInfo.capabilities, context = templateInfo.context, namespace = templateInfo.namespace, ) ) template.value = tid } showDialog.value = false }, onDismiss = { showDialog.value = false } ) } val selectedTemplateName = template.value.ifEmpty { "None" } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp), content = buildList { add { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile_template)) }, supportingContent = { Text(selectedTemplateName) }, trailingContent = { if (noTemplates) { IconButton(onClick = onManageTemplate) { Icon(Icons.Filled.Create, contentDescription = null) } } }, onClick = { if (!noTemplates) { showDialog.value = true } } ) } if (template.value.isNotEmpty()) add { SegmentedListItem( headlineContent = { Text(stringResource(R.string.app_profile_template_view)) }, trailingContent = { Icon(Icons.AutoMirrored.Filled.ReadMore, contentDescription = null) }, onClick = { onViewTemplate(template.value) } ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/TemplateConfigMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.profile import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Create import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.util.listAppProfileTemplates import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.theme.MiuixTheme /** * @author weishu * @date 2023/10/21. */ @Composable fun TemplateConfigMiuix( modifier: Modifier = Modifier, profile: Natives.Profile, onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit ) { val profileTemplates = listAppProfileTemplates() val noTemplates = profileTemplates.isEmpty() if (noTemplates) { SuperArrow( modifier = modifier, title = stringResource(R.string.app_profile_template_create), startAction = { Icon( Icons.Rounded.Create, null, modifier = Modifier.padding(end = 16.dp), tint = MiuixTheme.colorScheme.onBackground ) }, onClick = onManageTemplate, ) } else { var template by rememberSaveable { mutableStateOf(profile.rootTemplate ?: profileTemplates[0]) } Column(modifier = modifier) { SuperDropdown( title = stringResource(R.string.profile_template), items = profileTemplates, selectedIndex = profileTemplates.indexOf(template).takeIf { it >= 0 } ?: 0, onSelectedIndexChange = { index -> if (index < 0 || index >= profileTemplates.size) return@SuperDropdown template = profileTemplates[index] val templateInfo = getTemplateInfoById(template) if (templateInfo != null && setSepolicy(template, templateInfo.rules.joinToString("\n"))) { onProfileChange( profile.copy( rootTemplate = template, rootUseDefault = false, uid = templateInfo.uid, gid = templateInfo.gid, groups = templateInfo.groups, capabilities = templateInfo.capabilities, context = templateInfo.context, namespace = templateInfo.namespace, ) ) } }, maxHeight = 280.dp ) SuperArrow( title = stringResource(R.string.app_profile_template_view), onClick = { onViewTemplate(template) } ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/dialogs/MultiSelectDialog.kt ================================================ package me.weishu.kernelsu.ui.component.profile.dialogs import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Search import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import me.weishu.kernelsu.ui.component.material.SegmentedCheckboxItem import me.weishu.kernelsu.ui.component.material.SegmentedColumn @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun MultiSelectDialog( title: String, subtitle: String? = null, items: List, selectedItems: Set, itemTitle: (T) -> String, itemSubtitle: (T) -> String?, maxSelection: Int = Int.MAX_VALUE, onSelectionChange: (Set) -> Unit, onDismiss: () -> Unit ) { var searchQuery by remember { mutableStateOf("") } val filteredItems = remember(items, searchQuery) { if (searchQuery.isBlank()) { items } else { items.filter { item -> itemTitle(item).contains(searchQuery, ignoreCase = true) || itemSubtitle(item)?.contains(searchQuery, ignoreCase = true) == true } } } ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 24.dp) .padding(bottom = 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.weight(1f)) { Text( text = title, style = MaterialTheme.typography.titleLarge ) if (subtitle != null) { Text( text = subtitle, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } } IconButton(onClick = onDismiss) { Icon(Icons.Filled.Close, contentDescription = null) } } OutlinedTextField( value = searchQuery, onValueChange = { searchQuery = it }, modifier = Modifier.fillMaxWidth(), leadingIcon = { Icon(Icons.Filled.Search, contentDescription = null) }, trailingIcon = { if (searchQuery.isNotEmpty()) { IconButton(onClick = { searchQuery = "" }) { Icon(Icons.Filled.Close, contentDescription = null) } } }, singleLine = true ) SegmentedColumn( modifier = Modifier .fillMaxWidth() .weight(1f, fill = false) .verticalScroll(rememberScrollState()), content = filteredItems.map { item -> { SegmentedCheckboxItem( title = itemTitle(item), summary = itemSubtitle(item), colors = ListItemDefaults.segmentedColors().copy( containerColor = MaterialTheme.colorScheme.surface, contentColor = Color.Unspecified ), checked = selectedItems.contains(item), onCheckedChange = { isChecked -> val newSelection = selectedItems.toMutableSet() if (isChecked && newSelection.size < maxSelection) { newSelection.add(item) } else if (!isChecked) { newSelection.remove(item) } onSelectionChange(newSelection) } ) } } ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/profile/dialogs/SingleSelectDialog.kt ================================================ package me.weishu.kernelsu.ui.component.profile.dialogs import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material3.AlertDialog import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedRadioItem @Composable fun SingleSelectDialog( title: String, items: List, selectedItem: T, itemTitle: (T) -> String, onConfirm: (T) -> Unit, onDismiss: () -> Unit ) { var selected by remember { mutableStateOf(selectedItem) } AlertDialog( onDismissRequest = onDismiss, title = { Text(title) }, text = { SegmentedColumn( modifier = Modifier.verticalScroll(rememberScrollState()), content = items.map { item -> { SegmentedRadioItem( title = itemTitle(item), selected = selected == item, onClick = { selected = item } ) } } ) }, confirmButton = { TextButton( onClick = { onConfirm(selected) onDismiss() } ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopup.kt ================================================ package me.weishu.kernelsu.ui.component.rebootlistpopup import androidx.compose.runtime.Composable import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun RebootListPopup() { when (LocalUiMode.current) { UiMode.Miuix -> RebootListPopupMiuix() UiMode.Material -> RebootListPopupMaterial() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopupMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.rebootlistpopup import android.content.Context import android.os.PowerManager import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.PowerSettingsNew import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KsuIsValid import me.weishu.kernelsu.ui.util.reboot @Composable fun RebootDropdownItems(onItemClick: (String) -> Unit) { val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? @Suppress("DEPRECATION") val isRebootingUserspaceSupported = pm?.isRebootingUserspaceSupported == true val rebootOptions = mutableListOf( Pair(R.string.reboot, ""), Pair(R.string.reboot_soft, "soft_reboot"), Pair(R.string.reboot_recovery, "recovery"), Pair(R.string.reboot_bootloader, "bootloader"), Pair(R.string.reboot_download, "download"), Pair(R.string.reboot_edl, "edl") ) if (isRebootingUserspaceSupported) { rebootOptions.add(1, Pair(R.string.reboot_userspace, "userspace")) } rebootOptions.forEach { (id, reason) -> DropdownMenuItem( text = { Text(" " + stringResource(id)) }, onClick = { onItemClick(reason) } ) } } @Composable fun RebootListPopupMaterial() { var expanded by remember { mutableStateOf(false) } KsuIsValid { IconButton(onClick = { expanded = true }) { Icon( imageVector = Icons.Filled.PowerSettingsNew, contentDescription = stringResource(id = R.string.reboot) ) } DropdownMenu( expanded = expanded, onDismissRequest = { expanded = false } ) { RebootDropdownItems { reason -> expanded = false reboot(reason) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/rebootlistpopup/RebootListPopupMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.rebootlistpopup import android.content.Context import android.os.PowerManager import androidx.compose.runtime.Composable import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KsuIsValid import me.weishu.kernelsu.ui.util.reboot import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Close2 import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme @Composable fun RebootListPopupMiuix( modifier: Modifier = Modifier, alignment: PopupPositionProvider.Align = PopupPositionProvider.Align.TopEnd ) { val showTopPopup = remember { mutableStateOf(false) } KsuIsValid { IconButton( modifier = modifier, onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.Close2, contentDescription = stringResource(id = R.string.reboot), tint = colorScheme.onBackground ) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = alignment, onDismissRequest = { showTopPopup.value = false }, content = { val pm = LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? @Suppress("DEPRECATION") val isRebootingUserspaceSupported = pm?.isRebootingUserspaceSupported == true ListPopupColumn { val rebootOptions = mutableListOf( Pair(R.string.reboot, ""), Pair(R.string.reboot_soft, "soft_reboot"), Pair(R.string.reboot_recovery, "recovery"), Pair(R.string.reboot_bootloader, "bootloader"), Pair(R.string.reboot_download, "download"), Pair(R.string.reboot_edl, "edl") ) if (isRebootingUserspaceSupported) { rebootOptions.add(1, Pair(R.string.reboot_userspace, "userspace")) } rebootOptions.forEachIndexed { idx, (id, reason) -> RebootDropdownItem( id = id, reason = reason, showTopPopup = showTopPopup, optionSize = rebootOptions.size, index = idx ) } } } ) } } @Composable fun RebootDropdownItem( id: Int, reason: String = "", showTopPopup: MutableState, optionSize: Int, index: Int, ) { me.weishu.kernelsu.ui.component.miuix.DropdownItem( text = stringResource(id), optionSize = optionSize, onSelectedIndexChange = { reboot(reason) showTopPopup.value = false }, index = index ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTag.kt ================================================ package me.weishu.kernelsu.ui.component.statustag import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun StatusTag( label: String, modifier: Modifier = Modifier, backgroundColor: Color, contentColor: Color ) { when (LocalUiMode.current) { UiMode.Miuix -> StatusTagMiuix(label, backgroundColor, contentColor) UiMode.Material -> StatusTagMaterial(label, modifier, backgroundColor, contentColor) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTagMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.statustag import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp @Composable fun StatusTagMaterial( label: String, modifier: Modifier = Modifier, backgroundColor: Color, contentColor: Color ) { Box( modifier = modifier .padding(end = 4.dp) .background( color = backgroundColor, shape = RoundedCornerShape(4.dp) ) ) { Text( text = label, modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), style = TextStyle( fontSize = MaterialTheme.typography.labelSmall.fontSize, fontWeight = FontWeight.SemiBold, color = contentColor, ) ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/statustag/StatusTagMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.statustag import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.capsule.ContinuousRoundedRectangle import top.yukonga.miuix.kmp.basic.Text @Composable fun StatusTagMiuix( label: String, backgroundColor: Color, contentColor: Color ) { Box( modifier = Modifier .background( color = backgroundColor, shape = ContinuousRoundedRectangle(6.dp) ) ) { Text( modifier = Modifier.padding(horizontal = 4.dp, vertical = 2.dp), text = label, color = contentColor, fontSize = 9.sp, fontWeight = FontWeight(750), maxLines = 1, softWrap = false ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialog.kt ================================================ package me.weishu.kernelsu.ui.component.uninstalldialog import androidx.compose.runtime.Composable import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun UninstallDialog( show: Boolean, onDismissRequest: () -> Unit ) { when (LocalUiMode.current) { UiMode.Miuix -> UninstallDialogMiuix(show, onDismissRequest) UiMode.Material -> UninstallDialogMaterial(show, onDismissRequest) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialogMaterial.kt ================================================ package me.weishu.kernelsu.ui.component.uninstalldialog import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.screen.flash.UninstallType import me.weishu.kernelsu.ui.screen.flash.UninstallType.PERMANENT import me.weishu.kernelsu.ui.screen.flash.UninstallType.RESTORE_STOCK_IMAGE @Composable fun UninstallDialogMaterial( show: Boolean, onDismissRequest: () -> Unit ) { val navigator = LocalNavigator.current val options = listOf( // TEMPORARY, PERMANENT, RESTORE_STOCK_IMAGE ) val showConfirmDialog = remember { mutableStateOf(false) } val runType = remember { mutableStateOf(null) } val run = { type: UninstallType -> when (type) { PERMANENT -> navigator.push(Route.Flash(FlashIt.FlashUninstall)) RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash(FlashIt.FlashRestore)) else -> Unit } } if (show) { AlertDialog( onDismissRequest = onDismissRequest, title = { Text(stringResource(R.string.settings_uninstall)) }, text = { SegmentedColumn( modifier = Modifier, content = options.map { type -> { SegmentedListItem( onClick = { showConfirmDialog.value = true runType.value = type }, headlineContent = { Text(stringResource(type.title)) }, supportingContent = { Text(stringResource(type.message)) }, leadingContent = { Icon( imageVector = type.icon, contentDescription = null ) } ) } } ) }, confirmButton = { TextButton(onClick = onDismissRequest) { Text(stringResource(android.R.string.cancel)) } } ) } val confirmDialog = rememberConfirmDialog( onConfirm = { showConfirmDialog.value = false onDismissRequest() runType.value?.let { type -> run(type) } }, onDismiss = { showConfirmDialog.value = false } ) val dialogTitle = runType.value?.let { type -> options.find { it == type }?.let { stringResource(it.title) } } ?: "" val dialogContent = runType.value?.let { type -> options.find { it == type }?.let { stringResource(it.message) } } if (showConfirmDialog.value) { confirmDialog.showConfirm(title = dialogTitle, content = dialogContent) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/component/uninstalldialog/UninstallDialogMiuix.kt ================================================ package me.weishu.kernelsu.ui.component.uninstalldialog import android.widget.Toast import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.DpSize import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.screen.flash.UninstallType import me.weishu.kernelsu.ui.screen.flash.UninstallType.NONE import me.weishu.kernelsu.ui.screen.flash.UninstallType.PERMANENT import me.weishu.kernelsu.ui.screen.flash.UninstallType.RESTORE_STOCK_IMAGE import me.weishu.kernelsu.ui.screen.flash.UninstallType.TEMPORARY import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDialog import top.yukonga.miuix.kmp.theme.MiuixTheme @Composable fun UninstallDialogMiuix( show: Boolean, onDismissRequest: () -> Unit ) { val context = LocalContext.current val navigator = LocalNavigator.current val options = listOf( // TEMPORARY, PERMANENT, RESTORE_STOCK_IMAGE ) val showTodo = { Toast.makeText(context, "TODO", Toast.LENGTH_SHORT).show() } val showConfirmDialog = remember(show) { mutableStateOf(false) } val runType = remember(show) { mutableStateOf(null) } val run = { type: UninstallType -> when (type) { PERMANENT -> navigator.push(Route.Flash(FlashIt.FlashUninstall)) RESTORE_STOCK_IMAGE -> navigator.push(Route.Flash(FlashIt.FlashRestore)) TEMPORARY -> showTodo() NONE -> Unit } } SuperDialog( show = show, onDismissRequest = onDismissRequest, insideMargin = DpSize(0.dp, 0.dp), content = { Text( modifier = Modifier .fillMaxWidth() .padding(top = 24.dp, bottom = 12.dp), text = stringResource(R.string.uninstall), fontSize = MiuixTheme.textStyles.title4.fontSize, fontWeight = FontWeight.Medium, textAlign = TextAlign.Center, color = MiuixTheme.colorScheme.onSurface ) options.forEach { type -> SuperArrow( onClick = { showConfirmDialog.value = true runType.value = type }, title = stringResource(type.title), startAction = { Icon( imageVector = type.icon, contentDescription = null, modifier = Modifier.padding(end = 16.dp), tint = MiuixTheme.colorScheme.onSurface ) }, insideMargin = PaddingValues(horizontal = 24.dp, vertical = 12.dp) ) } TextButton( text = stringResource(id = android.R.string.cancel), onClick = onDismissRequest, modifier = Modifier .fillMaxWidth() .padding(top = 12.dp, bottom = 24.dp) .padding(horizontal = 24.dp) ) } ) val confirmDialog = rememberConfirmDialog( onConfirm = { showConfirmDialog.value = false onDismissRequest() runType.value?.let { type -> run(type) } }, onDismiss = { showConfirmDialog.value = false } ) val dialogTitle = runType.value?.let { type -> options.find { it == type }?.let { stringResource(it.title) } } ?: "" val dialogContent = runType.value?.let { type -> options.find { it == type }?.let { stringResource(it.message) } } if (showConfirmDialog.value) { confirmDialog.showConfirm(title = dialogTitle, content = dialogContent) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/modifier/DragGestureInspector.kt ================================================ package me.weishu.kernelsu.ui.modifier import androidx.compose.foundation.gestures.awaitEachGesture import androidx.compose.foundation.gestures.awaitFirstDown import androidx.compose.ui.geometry.Offset import androidx.compose.ui.input.pointer.AwaitPointerEventScope import androidx.compose.ui.input.pointer.PointerEventPass import androidx.compose.ui.input.pointer.PointerId import androidx.compose.ui.input.pointer.PointerInputChange import androidx.compose.ui.input.pointer.PointerInputScope import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed import androidx.compose.ui.input.pointer.positionChange import androidx.compose.ui.util.fastFirstOrNull suspend fun PointerInputScope.inspectDragGestures( onDragStart: (down: PointerInputChange) -> Unit = {}, onDragEnd: (change: PointerInputChange) -> Unit = {}, onDragCancel: () -> Unit = {}, onDrag: (change: PointerInputChange, dragAmount: Offset) -> Unit ) { awaitEachGesture { val initialDown = awaitFirstDown(false, PointerEventPass.Initial) val down = awaitFirstDown(false) onDragStart(down) onDrag(initialDown, Offset.Zero) val upEvent = drag( pointerId = initialDown.id, onDrag = { onDrag(it, it.positionChange()) } ) if (upEvent == null) { onDragCancel() } else { onDragEnd(upEvent) } } } private suspend inline fun AwaitPointerEventScope.drag( pointerId: PointerId, onDrag: (PointerInputChange) -> Unit ): PointerInputChange? { val isPointerUp = currentEvent.changes.fastFirstOrNull { it.id == pointerId }?.pressed != true if (isPointerUp) { return null } var pointer = pointerId while (true) { val change = awaitDragOrUp(pointer) ?: return null if (change.isConsumed) { return null } if (change.changedToUpIgnoreConsumed()) { return change } onDrag(change) pointer = change.id } } private suspend inline fun AwaitPointerEventScope.awaitDragOrUp( pointerId: PointerId ): PointerInputChange? { var pointer = pointerId while (true) { val event = awaitPointerEvent() val dragEvent = event.changes.fastFirstOrNull { it.id == pointer } ?: return null if (dragEvent.changedToUpIgnoreConsumed()) { val otherDown = event.changes.fastFirstOrNull { it.pressed } if (otherDown == null) { return dragEvent } else { pointer = otherDown.id } } else { val hasDragged = dragEvent.previousPosition != dragEvent.position if (hasDragged) { return dragEvent } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/DeepLinkResolver.kt ================================================ package me.weishu.kernelsu.ui.navigation3 import android.app.Activity import android.content.Intent import android.net.Uri import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.State import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext /** * Deep link resolution: maps external Intent/Uri to an initial back stack. * Call resolve(intent) at Activity start to seed the back stack. */ object DeepLinkResolver { fun resolve(intent: Intent?): List { if (intent == null) return emptyList() val shortcutType = intent.getStringExtra("shortcut_type") return when (shortcutType) { "module_action" -> { val moduleId = intent.getStringExtra("module_id") ?: return emptyList() listOf(Route.Main, Route.ExecuteModuleAction(moduleId, fromShortcut = true)) } else -> emptyList() } } fun resolve(uri: Uri?): List { return emptyList() } } /** * Composable that handles deep link intents and updates the back stack accordingly. * Should be placed at the root of the NavHost. */ @Composable fun HandleDeepLink( intentState: State, ) { val context = LocalContext.current val activity = context as? Activity val currentIntentId by intentState val navigator = LocalNavigator.current var lastHandledIntentId by rememberSaveable { mutableIntStateOf(-1) } LaunchedEffect(currentIntentId) { if (currentIntentId != lastHandledIntentId) { val intent = activity?.intent val initialStack = DeepLinkResolver.resolve(intent) if (initialStack.isNotEmpty()) { navigator.replaceAll(initialStack) intent?.removeExtra("shortcut_type") intent?.removeExtra("module_id") } lastHandledIntentId = currentIntentId } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/Navigator.kt ================================================ package me.weishu.kernelsu.ui.navigation3 import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.staticCompositionLocalOf import androidx.navigation3.runtime.NavKey import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow /** * Simple navigation helper that owns a back stack and result channels. * Supports push/replace/pop/popUntil and result APIs: navigateForResult/setResult/observeResult/clearResult. */ class Navigator( initialKey: NavKey ) { val backStack: SnapshotStateList = mutableStateListOf(initialKey) private val resultBus = mutableMapOf>() /** * Push a key onto the back stack. */ fun push(key: NavKey) { backStack.add(key) } /** * Replace the top key, or push if the stack is empty. */ fun replace(key: NavKey) { if (backStack.isNotEmpty()) { backStack[backStack.lastIndex] = key } else { backStack.add(key) } } /** * Replace the backstack with a new list of keys if the stack is not empty. */ fun replaceAll(keys: List) { if (keys.isEmpty()) { return } if (backStack.isNotEmpty()) { backStack.clear() backStack.addAll(keys) } } /** * Pop the top key if present. */ fun pop() { backStack.removeLastOrNull() } /** * Pop until predicate matches the top key. */ fun popUntil(predicate: (NavKey) -> Boolean) { while (backStack.isNotEmpty() && !predicate(backStack.last())) { backStack.removeAt(backStack.lastIndex) } } /** * Navigate expecting a result. Caller should subscribe via observeResult(requestKey). */ fun navigateForResult(route: Route, requestKey: String) { ensureChannel(requestKey) push(route) } /** * Set a result for the given request and then pop. */ fun setResult(requestKey: String, value: T) { ensureChannel(requestKey).tryEmit(value) pop() } /** * Observe results for a given request key as a SharedFlow. */ @Suppress("UNCHECKED_CAST") fun observeResult(requestKey: String): SharedFlow { return ensureChannel(requestKey) as SharedFlow } /** * Clear the last emitted result for the request key. */ @OptIn(ExperimentalCoroutinesApi::class) fun clearResult(requestKey: String) { ensureChannel(requestKey).resetReplayCache() } /** * Get current NavKey on the back stack. */ fun current(): NavKey? { return backStack.lastOrNull() } /** * Get current size of back stack. */ fun backStackSize(): Int { return backStack.size } private fun ensureChannel(key: String): MutableSharedFlow { return resultBus.getOrPut(key) { MutableSharedFlow(replay = 1, extraBufferCapacity = 0) } } companion object { val Saver: Saver = listSaver(save = { navigator -> navigator.backStack.toList() }, restore = { savedList -> val initialKey = savedList.firstOrNull() ?: Route.Home val navigator = Navigator(initialKey) navigator.backStack.clear() navigator.backStack.addAll(savedList) navigator }) } } @Composable fun rememberNavigator(startRoute: NavKey): Navigator { return rememberSaveable(startRoute, saver = Navigator.Saver) { Navigator(startRoute) } } val LocalNavigator = staticCompositionLocalOf { error("LocalNavigator not provided") } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/navigation3/Routes.kt ================================================ package me.weishu.kernelsu.ui.navigation3 import android.os.Parcelable import androidx.navigation3.runtime.NavKey import kotlinx.parcelize.Parcelize import kotlinx.serialization.Serializable import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.screen.modulerepo.RepoModuleArg import me.weishu.kernelsu.ui.util.FlashItSerializer import me.weishu.kernelsu.ui.util.RepoModuleArgSerializer import me.weishu.kernelsu.ui.util.TemplateInfoSerializer import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel /** * Type-safe navigation keys for Navigation3. * Each destination is a NavKey (data object/data class) and can be saved/restored in the back stack. */ sealed interface Route : NavKey, Parcelable { @Parcelize @Serializable data object Main : Route @Parcelize @Serializable data object Home : Route @Parcelize @Serializable data object SuperUser : Route @Parcelize @Serializable data object Module : Route @Parcelize @Serializable data object Settings : Route @Parcelize @Serializable data object About : Route @Parcelize @Serializable data object ColorPalette : Route @Parcelize @Serializable data object AppProfileTemplate : Route @Parcelize @Serializable data class TemplateEditor( @Serializable(with = TemplateInfoSerializer::class) val template: TemplateViewModel.TemplateInfo, val readOnly: Boolean ) : Route @Parcelize @Serializable data class AppProfile(val uid: Int) : Route @Parcelize @Serializable data object Install : Route @Parcelize @Serializable data class ModuleRepoDetail(@Serializable(with = RepoModuleArgSerializer::class) val module: RepoModuleArg) : Route @Parcelize @Serializable data object ModuleRepo : Route @Parcelize @Serializable data class Flash(@Serializable(with = FlashItSerializer::class) val flashIt: FlashIt) : Route @Parcelize @Serializable data class ExecuteModuleAction(val moduleId: String, val fromShortcut: Boolean = false) : Route } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.about import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.FixedScale import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AboutScreenMaterial( state: AboutUiState, actions: AboutScreenActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) Scaffold( topBar = { LargeFlexibleTopAppBar( title = { Text(state.title) }, navigationIcon = { IconButton( onClick = actions.onBack ) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null ) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), scrollBehavior = scrollBehavior ) }, ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxSize() .padding(innerPadding) .nestedScroll(scrollBehavior.nestedScrollConnection) ) { item { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(vertical = 48.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .size(80.dp) .clip(RoundedCornerShape(16.dp)) .background(Color.White) ) { Image( painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = null, contentScale = FixedScale(1f) ) } Text( modifier = Modifier.padding(top = 12.dp), text = state.appName, fontWeight = FontWeight.Medium, fontSize = MaterialTheme.typography.headlineMedium.fontSize ) Text( text = state.versionName, fontSize = MaterialTheme.typography.bodyMedium.fontSize ) } } item { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = state.links.map { linkInfo -> { SegmentedListItem( onClick = { actions.onOpenLink(linkInfo.url) }, headlineContent = { Text(linkInfo.fullText) } ) } } ) Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.about import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.FixedScale import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.capsule.ContinuousRoundedRectangle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect 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.Text import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical @Composable fun AboutScreenMiuix( state: AboutUiState, actions: AboutScreenActions, ) { val scrollBehavior = MiuixScrollBehavior() val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = state.title, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = actions.onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, scrollBehavior = scrollBehavior ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding(horizontal = 12.dp), contentPadding = innerPadding, overscrollEffect = null, ) { item { Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(vertical = 48.dp), horizontalAlignment = Alignment.CenterHorizontally ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .size(80.dp) .clip(ContinuousRoundedRectangle(16.dp)) .background(Color.White) ) { Image( painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = null, contentScale = FixedScale(1f) ) } Text( modifier = Modifier.padding(top = 12.dp), text = state.appName, fontWeight = FontWeight.Medium, fontSize = 26.sp ) Text( text = state.versionName, fontSize = 14.sp ) } } item { Card( modifier = Modifier.padding(bottom = 12.dp) ) { state.links.forEach { SuperArrow( title = it.fullText, onClick = { actions.onOpenLink(it.url) } ) } } Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.about import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator @Composable fun AboutScreen() { val navigator = LocalNavigator.current val uriHandler = LocalUriHandler.current val htmlString = stringResource( id = R.string.about_source_code, "GitHub", "Telegram" ) val state = AboutUiState( title = stringResource(R.string.about), appName = stringResource(R.string.app_name), versionName = BuildConfig.VERSION_NAME, links = extractLinks(htmlString), ) val actions = AboutScreenActions( onBack = dropUnlessResumed { navigator.pop() }, onOpenLink = uriHandler::openUri, ) when (LocalUiMode.current) { UiMode.Miuix -> AboutScreenMiuix(state, actions) UiMode.Material -> AboutScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.about import androidx.compose.runtime.Immutable @Immutable data class AboutUiState( val title: String, val appName: String, val versionName: String, val links: List, ) @Immutable data class AboutScreenActions( val onBack: () -> Unit, val onOpenLink: (String) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/about/AboutUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.about import android.util.Log import androidx.compose.runtime.Immutable @Immutable data class LinkInfo( val fullText: String, val url: String ) fun extractLinks(html: String): List { val regex = Regex( """([^<>\n\r]+?)\s*\s*]*\bhref\s*=\s*(['"]?)([^'"\s>]+)\2[^>]*>([^<]+)\s*\s*(.*?)\s*(?= try { val before = match.groupValues[1].trim() val url = match.groupValues[3].trim() val title = match.groupValues[4].trim() val after = match.groupValues[5].trim() val fullText = "$before $title $after" LinkInfo(fullText, url) } catch (e: Exception) { Log.e("AboutState", "extractLinks failed: ${e.message}") null } }.toList() } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.appprofile import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade 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.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.AccountCircle import androidx.compose.material.icons.filled.Android import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Security import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.ToggleButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.AppIconImage import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem import me.weishu.kernelsu.ui.component.profile.AppProfileConfig import me.weishu.kernelsu.ui.component.profile.RootProfileConfig import me.weishu.kernelsu.ui.component.profile.TemplateConfig import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.ownerNameForUid import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel /** * @author weishu * @date 2023/5/16. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun AppProfileScreenMaterial( state: AppProfileUiState, actions: AppProfileActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val snackBarHost = LocalSnackbarHost.current LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } Scaffold( topBar = { TopBar( onBack = actions.onBack, scrollBehavior = scrollBehavior, isUidGroup = state.isUidGroup, packageName = state.packageName, userId = state.uid / 100000, onLaunchApp = actions.onLaunchApp, onForceStopApp = actions.onForceStopApp, onRestartApp = actions.onRestartApp, ) }, snackbarHost = { SnackbarHost(hostState = snackBarHost) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { paddingValues -> AppProfileInner( modifier = Modifier .padding(paddingValues) .fillMaxHeight() .imePadding() .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()), packageName = if (state.isUidGroup) "" else state.appGroup.primary.packageName, appLabel = if (state.isUidGroup) ownerNameForUid(state.appGroup.primary.uid) else state.appGroup.primary.label, appIcon = { AppIconImage( packageInfo = state.appGroup.primary.packageInfo, label = state.appGroup.primary.label, modifier = Modifier .padding(top = 4.dp) .size(48.dp) ) }, appUid = state.uid, sharedUserId = if (state.isUidGroup) state.sharedUserId else "", appVersionName = if (state.isUidGroup) "" else (state.appGroup.primary.packageInfo.versionName ?: ""), appVersionCode = if (state.isUidGroup) 0L else state.appGroup.primary.packageInfo.longVersionCode, profile = state.profile, isUidGroup = state.isUidGroup, affectedApps = state.appGroup.apps, onViewTemplate = actions.onViewTemplate, onManageTemplate = actions.onManageTemplate, onProfileChange = actions.onProfileChange, ) } } @Composable private fun AppProfileInner( modifier: Modifier = Modifier, packageName: String, appLabel: String, appIcon: @Composable (() -> Unit), appUid: Int, sharedUserId: String = "", appVersionName: String, appVersionCode: Long, profile: Natives.Profile, isUidGroup: Boolean = false, affectedApps: List = emptyList(), onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit, ) { val isRootGranted = profile.allowSu val userId = appUid / 100000 val appId = appUid % 100000 val initialRootMode = if (profile.rootUseDefault) { Mode.Default } else if (profile.rootTemplate != null) { Mode.Template } else { Mode.Custom } var rootMode by rememberSaveable(profile) { mutableStateOf(initialRootMode) } val nonRootMode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom val mode = if (isRootGranted) rootMode else nonRootMode Column(modifier = modifier) { SegmentedColumn( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), content = listOf( { SegmentedListItem( headlineContent = { Text(appLabel) }, supportingContent = { Column { if (!isUidGroup) { Text("$appVersionName ($appVersionCode)", color = MaterialTheme.colorScheme.outline) Text(packageName, color = MaterialTheme.colorScheme.outline) } else { if (sharedUserId.isNotEmpty()) { Text(text = sharedUserId, color = MaterialTheme.colorScheme.outline) } Text( text = stringResource(R.string.group_contains_apps, affectedApps.size), color = MaterialTheme.colorScheme.outline ) } } }, leadingContent = appIcon, trailingContent = { Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { if (userId != 0) { StatusTag( label = "USER $userId", contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) StatusTag( label = "UID $appId", contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) } else { StatusTag( label = "UID $appUid", contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) } } } ) }, { SegmentedSwitchItem( icon = Icons.Filled.Security, title = stringResource(id = R.string.superuser), checked = isRootGranted, onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, ) }, { SegmentedListItem( headlineContent = { Text(stringResource(R.string.profile)) }, supportingContent = { Text(mode.text, color = MaterialTheme.colorScheme.outline) }, leadingContent = { Icon(Icons.Filled.AccountCircle, null) }, ) } ) ) Crossfade(targetState = isRootGranted, label = "") { current -> Column( modifier = Modifier.padding(bottom = 6.dp + 48.dp + 6.dp /* SnackBar height */) ) { if (current) { ProfileBox(mode, true) { // template mode shouldn't change profile here! if (it == Mode.Default || it == Mode.Custom) { onProfileChange( profile.copy( rootUseDefault = it == Mode.Default, rootTemplate = null ) ) } rootMode = it } AnimatedVisibility( visible = mode == Mode.Template, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { TemplateConfig( profile = profile, onViewTemplate = onViewTemplate, onManageTemplate = onManageTemplate, onProfileChange = onProfileChange ) } AnimatedVisibility( visible = mode == Mode.Custom, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { RootProfileConfig( fixedName = true, enabled = mode == Mode.Custom, profile = profile, onProfileChange = onProfileChange ) } } else { ProfileBox(mode, false) { onProfileChange(profile.copy(nonRootUseDefault = (it == Mode.Default))) } AnimatedVisibility( visible = true, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { AppProfileConfig( fixedName = true, profile = profile, enabled = mode == Mode.Custom, onProfileChange = onProfileChange ) } } if (isUidGroup) { val appItems = affectedApps.map Unit> { app -> { SegmentedListItem( headlineContent = { Text(app.label) }, supportingContent = { Text(app.packageName) }, leadingContent = { AppIconImage( packageInfo = app.packageInfo, label = app.label, modifier = Modifier.size(36.dp) ) } ) } } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), title = stringResource(R.string.app_profile_affects_following_apps), content = appItems ) } } } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( onBack: () -> Unit, scrollBehavior: TopAppBarScrollBehavior? = null, isUidGroup: Boolean = false, packageName: String = "", userId: Int = 0, onLaunchApp: (String, Int) -> Unit, onForceStopApp: (String, Int) -> Unit, onRestartApp: (String, Int) -> Unit, ) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.profile)) }, navigationIcon = { IconButton( onClick = onBack ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, actions = { if (!isUidGroup) { var showDropdown by remember { mutableStateOf(false) } IconButton( onClick = { showDropdown = true }, ) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings) ) DropdownMenu( expanded = showDropdown, onDismissRequest = { showDropdown = false } ) { DropdownMenuItem( text = { Text(stringResource(id = R.string.launch_app)) }, onClick = { showDropdown = false onLaunchApp(packageName, userId) }, ) DropdownMenuItem( text = { Text(stringResource(id = R.string.force_stop_app)) }, onClick = { showDropdown = false onForceStopApp(packageName, userId) }, ) DropdownMenuItem( text = { Text(stringResource(id = R.string.restart_app)) }, onClick = { showDropdown = false onRestartApp(packageName, userId) }, ) } } } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ProfileBox( mode: Mode, hasTemplate: Boolean, onModeChange: (Mode) -> Unit, ) { Row( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 8.dp), horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), ) { val options = listOf( Mode.Default to stringResource(R.string.profile_default), Mode.Template to stringResource(R.string.profile_template), Mode.Custom to stringResource(R.string.profile_custom), ) options.forEachIndexed { index, (m, label) -> ToggleButton( checked = mode == m, onCheckedChange = { if (m != Mode.Template || hasTemplate) onModeChange(m) }, enabled = if (m == Mode.Template) hasTemplate else true, modifier = Modifier .weight(1f) .semantics { role = Role.RadioButton }, shapes = when (index) { 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() else -> ButtonGroupDefaults.connectedMiddleButtonShapes() }, ) { Text(label, maxLines = 1, overflow = TextOverflow.Ellipsis) } } } } @Preview @Composable private fun AppProfilePreview() { var profile by remember { mutableStateOf(Natives.Profile("")) } AppProfileInner( packageName = "icu.nullptr.test", appLabel = "Test", appIcon = { Icon(Icons.Filled.Android, null) }, appUid = 1, appVersionName = "v1.0.0", appVersionCode = 12345, profile = profile, onProfileChange = { profile = it }, ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.appprofile 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.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.AccountCircle import androidx.compose.material.icons.rounded.Security import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.AppIconImage import me.weishu.kernelsu.ui.component.miuix.DropdownItem import me.weishu.kernelsu.ui.component.profile.AppProfileConfig import me.weishu.kernelsu.ui.component.profile.RootProfileConfig import me.weishu.kernelsu.ui.component.profile.TemplateConfig import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect import me.weishu.kernelsu.ui.util.listAppProfileTemplates import me.weishu.kernelsu.ui.util.ownerNameForUid import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import top.yukonga.miuix.kmp.basic.BasicComponent 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.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.SmallTitle import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.MoreCircle import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/5/16. */ @Composable fun AppProfileScreenMiuix( state: AppProfileUiState, actions: AppProfileActions, ) { val enableBlur = LocalEnableBlur.current val scrollBehavior = MiuixScrollBehavior() val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopBar( onBack = actions.onBack, showActions = !state.isUidGroup, packageName = state.packageName, userId = state.uid / 100000, onLaunchApp = actions.onLaunchApp, onForceStopApp = actions.onForceStopApp, onRestartApp = actions.onRestartApp, scrollBehavior = scrollBehavior, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .padding(top = 16.dp) .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, contentPadding = innerPadding, overscrollEffect = null ) { item { AppProfileInner( packageName = if (state.isUidGroup) "" else state.appGroup.primary.packageName, appLabel = if (state.isUidGroup) ownerNameForUid(state.appGroup.primary.uid) else state.appGroup.primary.label, appIcon = { AppIconImage( packageInfo = state.appGroup.primary.packageInfo, label = state.appGroup.primary.label, modifier = Modifier.size(54.dp) ) }, appUid = state.uid, sharedUserId = if (state.isUidGroup) state.sharedUserId else "", appVersionName = if (state.isUidGroup) "" else (state.appGroup.primary.packageInfo.versionName ?: ""), appVersionCode = if (state.isUidGroup) 0L else state.appGroup.primary.packageInfo.longVersionCode, profile = state.profile, isUidGroup = state.isUidGroup, affectedApps = state.appGroup.apps, onViewTemplate = actions.onViewTemplate, onManageTemplate = actions.onManageTemplate, onProfileChange = actions.onProfileChange, ) Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } @Composable private fun AppProfileInner( modifier: Modifier = Modifier, packageName: String, appLabel: String, appIcon: @Composable (() -> Unit), appUid: Int, sharedUserId: String = "", appVersionName: String, appVersionCode: Long, profile: Natives.Profile, isUidGroup: Boolean = false, affectedApps: List = emptyList(), onViewTemplate: (id: String) -> Unit = {}, onManageTemplate: () -> Unit = {}, onProfileChange: (Natives.Profile) -> Unit, ) { val isRootGranted = profile.allowSu val userId = appUid / 100000 val appId = appUid % 100000 val templates = remember { listAppProfileTemplates() } Column( modifier = modifier ) { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), insideMargin = PaddingValues(horizontal = 16.dp, vertical = 14.dp) ) { Row( verticalAlignment = Alignment.CenterVertically ) { appIcon() Column( modifier = Modifier .padding(start = 16.dp, end = 8.dp) .weight(1f), ) { Text( text = appLabel, color = colorScheme.onSurface, fontWeight = FontWeight(550), modifier = Modifier .basicMarquee(), maxLines = 1, softWrap = false ) if (!isUidGroup) { Text( text = "$appVersionName ($appVersionCode)", fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, modifier = Modifier .basicMarquee(), maxLines = 1, softWrap = false ) Text( text = packageName, fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, modifier = Modifier .basicMarquee(), maxLines = 1, softWrap = false ) } else { if (sharedUserId.isNotEmpty()) { Text( text = sharedUserId, fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, modifier = Modifier .basicMarquee(), maxLines = 1, softWrap = false ) } Text( text = stringResource(R.string.group_contains_apps, affectedApps.size), fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, fontWeight = FontWeight.Medium, modifier = Modifier .basicMarquee(), maxLines = 1, softWrap = false ) } } Column( modifier = Modifier, horizontalAlignment = Alignment.End, verticalArrangement = Arrangement.spacedBy(6.dp) ) { if (userId != 0) { StatusTag( label = "USER $userId", backgroundColor = colorScheme.primary.copy(alpha = 0.8f), contentColor = colorScheme.onPrimary ) StatusTag( label = "UID $appId", backgroundColor = colorScheme.primary.copy(alpha = 0.8f), contentColor = colorScheme.onPrimary ) } else { StatusTag( label = "UID $appUid", backgroundColor = colorScheme.primary.copy(alpha = 0.8f), contentColor = colorScheme.onPrimary ) } } } } Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), ) { SuperSwitch( startAction = { Icon( imageVector = Icons.Rounded.Security, contentDescription = null, modifier = Modifier.padding(end = 16.dp), tint = colorScheme.onBackground ) }, title = stringResource(id = R.string.superuser), checked = isRootGranted, onCheckedChange = { onProfileChange(profile.copy(allowSu = it)) }, ) } val initialRootMode = if (profile.rootUseDefault) { Mode.Default } else if (profile.rootTemplate != null) { Mode.Template } else { Mode.Custom } var rootMode by rememberSaveable { mutableStateOf(initialRootMode) } val nonRootMode = if (profile.nonRootUseDefault) Mode.Default else Mode.Custom val dropdownMode = if (isRootGranted) rootMode else nonRootMode ProfileBox(dropdownMode, isRootGranted) { mode -> if (isRootGranted) { when (mode) { Mode.Default, Mode.Custom -> { onProfileChange( profile.copy( rootUseDefault = mode == Mode.Default, rootTemplate = null ) ) rootMode = mode } Mode.Template -> { if (templates.isNotEmpty()) { val selected = profile.rootTemplate ?: templates[0] val info = me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById(selected) if (info != null && setSepolicy(selected, info.rules.joinToString("\n"))) { onProfileChange( profile.copy( rootUseDefault = false, rootTemplate = selected, uid = info.uid, gid = info.gid, groups = info.groups, capabilities = info.capabilities, context = info.context, namespace = info.namespace, ) ) } else if (profile.rootTemplate != selected || profile.rootUseDefault) { onProfileChange( profile.copy( rootUseDefault = false, rootTemplate = selected ) ) } rootMode = Mode.Template } } } } else { onProfileChange(profile.copy(nonRootUseDefault = (mode == Mode.Default))) } } Spacer(Modifier.height(12.dp)) AnimatedVisibility( visible = isRootGranted, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = if (rootMode != Mode.Default) 12.dp else 0.dp), ) { AnimatedVisibility( visible = rootMode == Mode.Template, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { TemplateConfig( profile = profile, onViewTemplate = onViewTemplate, onManageTemplate = onManageTemplate, onProfileChange = onProfileChange ) } AnimatedVisibility( visible = rootMode == Mode.Custom, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { RootProfileConfig( fixedName = true, enabled = rootMode == Mode.Custom, profile = profile, onProfileChange = onProfileChange ) } } } AnimatedVisibility( visible = !isRootGranted, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = if (nonRootMode != Mode.Default) 12.dp else 0.dp), ) { AnimatedVisibility( visible = nonRootMode == Mode.Custom, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { AppProfileConfig( fixedName = true, profile = profile, enabled = true, onProfileChange = onProfileChange ) } } } if (isUidGroup) { SmallTitle( text = stringResource(R.string.app_profile_affects_following_apps), modifier = Modifier.padding(top = 4.dp) ) Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), ) { Spacer(Modifier.height(3.dp)) affectedApps.forEach { app -> BasicComponent( startAction = { AppIconImage( packageInfo = app.packageInfo, label = app.label, modifier = Modifier .padding(end = 12.dp) .size(40.dp) ) }, title = app.label, summary = app.packageName, insideMargin = PaddingValues(horizontal = 16.dp, vertical = 12.dp) ) } Spacer(Modifier.height(3.dp)) } } } } @Composable private fun TopBar( onBack: () -> Unit, showActions: Boolean = true, packageName: String, userId: Int, onLaunchApp: (String, Int) -> Unit = { _, _ -> }, onForceStopApp: (String, Int) -> Unit = { _, _ -> }, onRestartApp: (String, Int) -> Unit = { _, _ -> }, scrollBehavior: ScrollBehavior, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.profile), navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, actions = { if (showActions) { val showTopPopup = remember { mutableStateOf(false) } IconButton( modifier = Modifier.padding(end = 16.dp), onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.MoreCircle, tint = colorScheme.onSurface, contentDescription = stringResource(id = R.string.settings) ) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = { showTopPopup.value = false }, content = { ListPopupColumn { val items = listOf( stringResource(id = R.string.launch_app), stringResource(id = R.string.force_stop_app), stringResource(id = R.string.restart_app) ) items.forEachIndexed { index, text -> DropdownItem( text = text, optionSize = items.size, index = index, onSelectedIndexChange = { selectedIndex -> when (selectedIndex) { 0 -> onLaunchApp(packageName, userId) 1 -> onForceStopApp(packageName, userId) 2 -> onRestartApp(packageName, userId) } showTopPopup.value = false } ) } } } ) } }, scrollBehavior = scrollBehavior ) } @Composable private fun ProfileBox( mode: Mode, hasTemplate: Boolean, onModeChange: (Mode) -> Unit, ) { val defaultText = stringResource(R.string.profile_default) val templateText = stringResource(R.string.profile_template) val customText = stringResource(R.string.profile_custom) val list = remember(hasTemplate, defaultText, templateText, customText) { buildList { add(defaultText) if (hasTemplate) { add(templateText) } add(customText) } } val modesAndTitles = remember(hasTemplate, defaultText, templateText, customText) { buildList { add(Mode.Default to defaultText) if (hasTemplate) { add(Mode.Template to templateText) } add(Mode.Custom to customText) } } val selectedIndex = modesAndTitles.indexOfFirst { it.first == mode } Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp), ) { SuperDropdown( title = stringResource(R.string.profile), items = list, startAction = { Icon( Icons.Rounded.AccountCircle, modifier = Modifier.padding(end = 16.dp), contentDescription = null, tint = colorScheme.onBackground ) }, selectedIndex = if (selectedIndex == -1) 0 else selectedIndex, ) { onModeChange(modesAndTitles[it].first) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.appprofile import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.forceStopApp import me.weishu.kernelsu.ui.util.getSepolicy import me.weishu.kernelsu.ui.util.launchApp import me.weishu.kernelsu.ui.util.restartApp import me.weishu.kernelsu.ui.util.setSepolicy import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import me.weishu.kernelsu.ui.viewmodel.getTemplateInfoById @Composable fun AppProfileScreen(uid: Int) { val uiMode = LocalUiMode.current val navigator = LocalNavigator.current val context = LocalContext.current val snackbarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val viewModel: SuperUserViewModel = viewModel() val appGroupState = remember(uid) { derivedStateOf { viewModel.uiState.value.groupedApps.find { it.uid == uid } ?: SuperUserViewModel.getGroupedApp(uid) } } val appGroup = appGroupState.value val primaryAppInfo = appGroup?.primary if (primaryAppInfo == null) { LaunchedEffect(Unit) { navigator.pop() } return } val packageName = primaryAppInfo.packageName val sharedUserId = remember(uid) { primaryAppInfo.packageInfo.sharedUserId ?: appGroup.apps.firstOrNull { it.packageInfo.sharedUserId != null }?.packageInfo?.sharedUserId ?: "" } val initialProfile = remember(uid, packageName) { Natives.getAppProfile(packageName, uid).also { if (it.allowSu) { it.rules = getSepolicy(packageName) } } } var profile by rememberSaveable(uid, packageName) { mutableStateOf(initialProfile) } val failToUpdateAppProfile = stringResource(R.string.failed_to_update_app_profile).format(primaryAppInfo.label) val failToUpdateSepolicy = stringResource(R.string.failed_to_update_sepolicy).format(primaryAppInfo.label) val suNotAllowed = stringResource(R.string.su_not_allowed).format(primaryAppInfo.label) fun showMessage(message: String) { scope.launch { if (uiMode == UiMode.Material) { snackbarHost.showSnackbar(message) } else { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } } val state = AppProfileUiState( uid = uid, packageName = packageName, profile = profile, appGroup = appGroup, sharedUserId = sharedUserId, ) val actions = AppProfileActions( onBack = dropUnlessResumed { navigator.pop() }, onLaunchApp = ::launchApp, onForceStopApp = ::forceStopApp, onRestartApp = ::restartApp, onViewTemplate = { templateId -> getTemplateInfoById(templateId)?.let { info -> navigator.push(Route.TemplateEditor(info, true)) } }, onManageTemplate = { navigator.push(Route.AppProfileTemplate) }, onProfileChange = { updatedProfile -> scope.launch { if (updatedProfile.allowSu) { if (uid < 2000 && uid != 1000) { showMessage(suNotAllowed) return@launch } if (!updatedProfile.rootUseDefault && updatedProfile.rules.isNotEmpty() && !setSepolicy(profile.name, updatedProfile.rules) ) { showMessage(failToUpdateSepolicy) return@launch } } if (!Natives.setAppProfile(updatedProfile)) { showMessage(failToUpdateAppProfile) } else { profile = updatedProfile if (uiMode == UiMode.Material) { viewModel.loadAppList() } } } }, ) when (uiMode) { UiMode.Miuix -> AppProfileScreenMiuix( state = state, actions = actions, ) UiMode.Material -> AppProfileScreenMaterial( state = state, actions = actions, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.appprofile import androidx.compose.runtime.Immutable import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.screen.superuser.GroupedApps @Immutable data class AppProfileUiState( val uid: Int, val packageName: String, val profile: Natives.Profile, val appGroup: GroupedApps, val sharedUserId: String, ) { val isUidGroup get() = appGroup.apps.size > 1 } @Immutable data class AppProfileActions( val onBack: () -> Unit, val onLaunchApp: (String, Int) -> Unit, val onForceStopApp: (String, Int) -> Unit, val onRestartApp: (String, Int) -> Unit, val onViewTemplate: (String) -> Unit, val onManageTemplate: () -> Unit, val onProfileChange: (Natives.Profile) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/appprofile/AppProfileUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.appprofile import androidx.annotation.StringRes import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R enum class Mode(@field:StringRes private val res: Int) { Default(R.string.profile_default), Template(R.string.profile_template), Custom(R.string.profile_custom); val text: String @Composable get() = stringResource(res) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.colorpalette import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.KernelSUApplication import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.theme.ColorMode import me.weishu.kernelsu.ui.viewmodel.SettingsViewModel @Composable fun ColorPaletteScreen() { val navigator = LocalNavigator.current val context = LocalContext.current val activity = LocalActivity.current val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() val currentPaletteStyle = try { PaletteStyle.valueOf(uiState.colorStyle) } catch (_: Exception) { PaletteStyle.TonalSpot } val currentColorSpec = try { ColorSpec.SpecVersion.valueOf(uiState.colorSpec) } catch (_: Exception) { ColorSpec.SpecVersion.Default } val state = ColorPaletteUiState( uiState = uiState, currentColorMode = ColorMode.fromValue(uiState.themeMode), currentPaletteStyle = currentPaletteStyle, currentColorSpec = currentColorSpec, ) val actions = ColorPaletteScreenActions( onBack = dropUnlessResumed { navigator.pop() }, onSetThemeMode = viewModel::setThemeMode, onSetMiuixMonet = viewModel::setMiuixMonet, onSetKeyColor = viewModel::setKeyColor, onSetColorMode = viewModel::setColorMode, onSetColorStyle = viewModel::setColorStyle, onSetColorSpec = viewModel::setColorSpec, onSetEnableBlur = viewModel::setEnableBlur, onSetEnableFloatingBottomBar = viewModel::setEnableFloatingBottomBar, onSetEnableFloatingBottomBarBlur = viewModel::setEnableFloatingBottomBarBlur, onSetEnablePredictiveBack = { viewModel.setEnablePredictiveBack(it) KernelSUApplication.setEnableOnBackInvokedCallback(context.applicationInfo, it) activity?.recreate() }, onSetPageScale = viewModel::setPageScale, ) when (LocalUiMode.current) { UiMode.Miuix -> ColorPaletteScreenMiuix(state, actions) UiMode.Material -> ColorPaletteScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreenMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.colorpalette import android.annotation.SuppressLint import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Brightness1 import androidx.compose.material.icons.filled.Brightness3 import androidx.compose.material.icons.filled.Brightness4 import androidx.compose.material.icons.filled.Brightness7 import androidx.compose.material.icons.filled.Home import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.AspectRatio import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.DesignServices import androidx.compose.material.icons.rounded.Style import androidx.compose.material3.ButtonGroupDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Slider import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ToggleButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import com.materialkolor.rememberDynamicColorScheme import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem import me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem import me.weishu.kernelsu.ui.screen.home.TonalCard import me.weishu.kernelsu.ui.theme.ColorMode import me.weishu.kernelsu.ui.theme.keyColorOptions @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ColorPaletteScreenMaterial( state: ColorPaletteUiState, actions: ColorPaletteScreenActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val uiState = state.uiState val currentColorMode = state.currentColorMode val currentKeyColor = uiState.keyColor val colorStyle = state.currentPaletteStyle val colorSpec = state.currentColorSpec LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } Scaffold( topBar = { LargeFlexibleTopAppBar( navigationIcon = { IconButton( onClick = actions.onBack ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, title = { Text(stringResource(R.string.settings_theme)) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { paddingValues -> val navBars = WindowInsets.navigationBars.asPaddingValues() val captionBar = WindowInsets.captionBar.asPaddingValues() Column( modifier = Modifier .padding(paddingValues) .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(8.dp) ) { val isDark = currentColorMode.isDark || currentColorMode.isSystem && isSystemInDarkTheme() ThemePreviewCard( keyColor = currentKeyColor, isDark = isDark, paletteStyle = colorStyle, colorSpec = colorSpec, ) Spacer(modifier = Modifier.height(8.dp)) LazyRow( modifier = Modifier.fillMaxWidth(), contentPadding = PaddingValues(horizontal = 16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), ) { item { ColorButtonMaterial( color = Color.Unspecified, isSelected = currentKeyColor == 0, isDark = isDark, paletteStyle = colorStyle, colorSpec = colorSpec, onClick = { actions.onSetKeyColor(0) } ) } items(keyColorOptions) { color -> ColorButtonMaterial( color = Color(color), isSelected = currentKeyColor == color, isDark = isDark, paletteStyle = colorStyle, colorSpec = colorSpec, onClick = { actions.onSetKeyColor(color) } ) } } Column( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { val options = listOf( listOf(ColorMode.SYSTEM) to stringResource(R.string.settings_theme_mode_system), listOf(ColorMode.LIGHT) to stringResource(R.string.settings_theme_mode_light), listOf(ColorMode.DARK) to stringResource(R.string.settings_theme_mode_dark), listOf(ColorMode.DARK_AMOLED) to stringResource(R.string.settings_theme_mode_dark) ) options.chunked(4).forEach { rowOptions -> Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween) ) { rowOptions.forEachIndexed { index, (modes, label) -> ToggleButton( checked = currentColorMode in modes, onCheckedChange = { if (it) { actions.onSetColorMode(modes.first()) } }, modifier = Modifier .weight(1f) .semantics { role = Role.RadioButton }, shapes = when (index) { 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() rowOptions.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() else -> ButtonGroupDefaults.connectedMiddleButtonShapes() }, ) { Icon( imageVector = when (modes.first()) { ColorMode.SYSTEM -> Icons.Filled.Brightness4 ColorMode.LIGHT -> Icons.Filled.Brightness7 ColorMode.DARK -> Icons.Filled.Brightness3 ColorMode.DARK_AMOLED -> Icons.Filled.Brightness1 else -> Icons.Filled.Brightness4 }, contentDescription = label ) } } } } SegmentedColumn( modifier = Modifier.padding(top = 4.dp), content = listOf( { val styles = PaletteStyle.entries SegmentedDropdownItem( icon = Icons.Rounded.Style, title = stringResource(R.string.settings_color_style), items = styles.map { it.name }, selectedIndex = styles.indexOf(colorStyle), onItemSelected = { index -> actions.onSetColorStyle(styles[index].name) } ) }, { val specs = ColorSpec.SpecVersion.entries SegmentedDropdownItem( icon = Icons.Rounded.DesignServices, title = stringResource(R.string.settings_color_spec), items = specs.map { it.name }, selectedIndex = specs.indexOf(colorSpec).coerceAtLeast(0), onItemSelected = { index -> actions.onSetColorSpec(specs[index].name) } ) } ) ) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { SegmentedColumn( modifier = Modifier.padding(top = 4.dp), content = listOf( { SegmentedSwitchItem( icon = Icons.Rounded.Adb, title = stringResource(id = R.string.settings_enable_predictive_back), summary = stringResource(id = R.string.settings_enable_predictive_back_summary), checked = uiState.enablePredictiveBack, onCheckedChange = actions.onSetEnablePredictiveBack ) } ) ) } TonalCard(modifier = Modifier.padding(top = 4.dp)) { var sliderValue by remember(uiState.pageScale) { mutableFloatStateOf(uiState.pageScale) } Column( modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Icon( Icons.Rounded.AspectRatio, contentDescription = stringResource(id = R.string.settings_page_scale), tint = MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier = Modifier.width(12.dp)) Column( modifier = Modifier.weight(1f) ) { Text( text = stringResource(R.string.settings_page_scale), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface ) Text( text = stringResource(id = R.string.settings_page_scale_summary), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } Text( text = "${(sliderValue * 100).toInt()}%", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) } Slider( value = sliderValue, onValueChange = { sliderValue = it }, onValueChangeFinished = { actions.onSetPageScale(sliderValue) }, valueRange = 0.8f..1.1f, modifier = Modifier.fillMaxWidth() ) } } } Spacer(modifier = Modifier.height(16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding())) } } } @SuppressLint("ConfigurationScreenWidthHeight") @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ThemePreviewCard( keyColor: Int, isDark: Boolean, paletteStyle: PaletteStyle = PaletteStyle.TonalSpot, colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021, ) { val context = LocalContext.current val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.toFloat() val screenHeight = configuration.screenHeightDp.toFloat() val screenRatio = screenWidth / screenHeight val dynamicColor = keyColor == 0 val colorScheme = if (dynamicColor) { val baseScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) rememberDynamicColorScheme( seedColor = Color.Unspecified, isDark = isDark, style = paletteStyle, specVersion = colorSpec, primary = baseScheme.primary, secondary = baseScheme.secondary, tertiary = baseScheme.tertiary, neutral = baseScheme.surface, neutralVariant = baseScheme.surfaceVariant, error = baseScheme.error ) } else { rememberDynamicColorScheme( seedColor = Color(keyColor), isDark = isDark, style = paletteStyle, specVersion = colorSpec, ) } Box(modifier = Modifier.fillMaxWidth(), contentAlignment = Alignment.TopCenter) { Surface( modifier = Modifier .fillMaxWidth(0.4f) .aspectRatio(screenRatio), color = colorScheme.background, shape = RoundedCornerShape(20.dp), border = BorderStroke(1.dp, color = MaterialTheme.colorScheme.outlineVariant) ) { Column { // top bar Box( modifier = Modifier .height(48.dp) .fillMaxWidth(), contentAlignment = Alignment.TopStart ) { Row( modifier = Modifier .fillMaxSize() .padding(start = 12.dp, top = 16.dp, bottom = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = stringResource(id = R.string.app_name), style = MaterialTheme.typography.bodyMedium, color = colorScheme.onSurface ) } } Box( modifier = Modifier .fillMaxWidth() .weight(1f), contentAlignment = Alignment.TopStart ) { Column( modifier = Modifier.padding(horizontal = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { TonalCard( containerColor = MaterialTheme.colorScheme.secondaryContainer, modifier = Modifier .fillMaxWidth() .height(40.dp), shape = RoundedCornerShape(12.dp), content = { } ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { TonalCard( modifier = Modifier .weight(1f) .height(32.dp), shape = RoundedCornerShape(12.dp), content = { } ) TonalCard( modifier = Modifier .weight(1f) .height(32.dp), shape = RoundedCornerShape(12.dp), content = { } ) } TonalCard( modifier = Modifier .fillMaxWidth() .height(96.dp), shape = RoundedCornerShape(12.dp), content = { } ) } } // bottom bar Surface( color = colorScheme.surfaceContainer, modifier = Modifier.fillMaxWidth() ) { Row( modifier = Modifier .height(40.dp) .fillMaxWidth() .padding(horizontal = 8.dp), verticalAlignment = Alignment.CenterVertically ) { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) { Icon(Icons.Filled.Home, null, tint = colorScheme.primary) } } } } } } } @Composable private fun ColorButtonMaterial( color: Color, isSelected: Boolean, isDark: Boolean, paletteStyle: PaletteStyle = PaletteStyle.TonalSpot, colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021, onClick: () -> Unit ) { val context = LocalContext.current val colorScheme = if (color == Color.Unspecified) { val baseScheme = if (isDark) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) rememberDynamicColorScheme( seedColor = Color.Unspecified, isDark = isDark, style = paletteStyle, specVersion = colorSpec, primary = baseScheme.primary, secondary = baseScheme.secondary, tertiary = baseScheme.tertiary, neutral = baseScheme.surface, neutralVariant = baseScheme.surfaceVariant, error = baseScheme.error ) } else { rememberDynamicColorScheme( seedColor = color, isDark = isDark, style = paletteStyle, specVersion = colorSpec, ) } Surface( onClick = onClick, shape = RoundedCornerShape(20.dp), color = colorScheme.surfaceContainer, modifier = Modifier.size(72.dp) ) { Box(contentAlignment = Alignment.Center) { Canvas(modifier = Modifier.size(48.dp)) { drawArc( color = colorScheme.primaryContainer, startAngle = 180f, sweepAngle = 180f, useCenter = true ) drawArc( color = colorScheme.tertiaryContainer, startAngle = 0f, sweepAngle = 180f, useCenter = true ) } val scale by animateFloatAsState(targetValue = if (isSelected) 1.1f else 1.0f) Box( modifier = Modifier.graphicsLayer { scaleX = scale scaleY = scale }, contentAlignment = Alignment.Center ) { AnimatedVisibility( visible = isSelected, enter = fadeIn() + scaleIn(initialScale = 0.8f), exit = fadeOut() + scaleOut(targetScale = 0.8f) ) { Box( modifier = Modifier .size(56.dp) .border(2.dp, colorScheme.primary, CircleShape), contentAlignment = Alignment.Center ) { Box( modifier = Modifier .size(24.dp) .clip(CircleShape) .background(colorScheme.primary, CircleShape) ) { Icon( imageVector = Icons.Rounded.Check, contentDescription = null, tint = colorScheme.onPrimary, modifier = Modifier .align(Alignment.Center) .size(16.dp) ) } } } AnimatedVisibility( visible = !isSelected, enter = fadeIn() + scaleIn(initialScale = 0.8f), exit = fadeOut() + scaleOut(targetScale = 0.8f) ) { Box( modifier = Modifier .size(20.dp) .background(colorScheme.primary, CircleShape) ) } } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteScreenMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.colorpalette import android.annotation.SuppressLint import android.os.Build import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.AspectRatio import androidx.compose.material.icons.rounded.BlurOn import androidx.compose.material.icons.rounded.CallToAction import androidx.compose.material.icons.rounded.Colorize import androidx.compose.material.icons.rounded.DesignServices import androidx.compose.material.icons.rounded.Style import androidx.compose.material.icons.rounded.Wallpaper import androidx.compose.material.icons.rounded.WaterDrop import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.capsule.ContinuousRoundedRectangle import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import com.materialkolor.rememberDynamicColorScheme import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.miuix.ScaleDialog import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.keyColorOptions import me.weishu.kernelsu.ui.util.defaultHazeEffect 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.Slider import top.yukonga.miuix.kmp.basic.SliderDefaults import top.yukonga.miuix.kmp.basic.TabRow import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical @Composable fun ColorPaletteScreenMiuix( state: ColorPaletteUiState, actions: ColorPaletteScreenActions, ) { val scrollBehavior = MiuixScrollBehavior() val enableBlurState = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlurState) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } val uiState = state.uiState val currentColorMode = state.currentColorMode val isDark = currentColorMode.isDark || currentColorMode.isSystem && isSystemInDarkTheme() Scaffold( topBar = { TopAppBar( modifier = if (enableBlurState) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlurState) Color.Transparent else colorScheme.surface, title = stringResource(R.string.settings_theme), navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = actions.onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, scrollBehavior = scrollBehavior ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val showScaleDialog = rememberSaveable { mutableStateOf(false) } LazyColumn( modifier = Modifier .fillMaxHeight() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlurState) it.hazeSource(state = hazeState) else it } .padding(horizontal = 12.dp), contentPadding = innerPadding, overscrollEffect = null, ) { item { Spacer(modifier = Modifier.height(32.dp)) ThemePreviewCardMiuix( keyColor = uiState.keyColor, isDark = isDark, miuixMonet = uiState.miuixMonet, enableFloatingBottomBar = uiState.enableFloatingBottomBar, enableFloatingBottomBarBlur = uiState.enableFloatingBottomBarBlur, paletteStyle = state.currentPaletteStyle, colorSpec = state.currentColorSpec, ) Spacer(modifier = Modifier.height(72.dp)) val themeItems = listOf( stringResource(id = R.string.settings_theme_mode_system), stringResource(id = R.string.settings_theme_mode_light), stringResource(id = R.string.settings_theme_mode_dark), ) TabRow( tabs = themeItems, selectedTabIndex = (if (uiState.themeMode >= 3) uiState.themeMode - 3 else uiState.themeMode).coerceIn(0, 2), onTabSelected = { index -> actions.onSetThemeMode(index) }, height = 48.dp, ) Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { SuperSwitch( title = stringResource(id = R.string.settings_monet), startAction = { Icon( Icons.Rounded.Wallpaper, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_monet), tint = colorScheme.onBackground ) }, checked = uiState.miuixMonet, onCheckedChange = { actions.onSetMiuixMonet(it) } ) AnimatedVisibility( visible = uiState.miuixMonet ) { Column { val colorItems = listOf( stringResource(id = R.string.settings_key_color_default), stringResource(id = R.string.color_red), stringResource(id = R.string.color_pink), stringResource(id = R.string.color_purple), stringResource(id = R.string.color_deep_purple), stringResource(id = R.string.color_indigo), stringResource(id = R.string.color_blue), stringResource(id = R.string.color_cyan), stringResource(id = R.string.color_teal), stringResource(id = R.string.color_green), stringResource(id = R.string.color_yellow), stringResource(id = R.string.color_amber), stringResource(id = R.string.color_orange), stringResource(id = R.string.color_brown), stringResource(id = R.string.color_blue_grey), stringResource(id = R.string.color_sakura), ) val colorValues = listOf(0) + keyColorOptions SuperDropdown( title = stringResource(id = R.string.settings_key_color), items = colorItems, startAction = { Icon( Icons.Rounded.Colorize, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_key_color), tint = colorScheme.onBackground ) }, selectedIndex = colorValues.indexOf(uiState.keyColor).takeIf { it >= 0 } ?: 0, onSelectedIndexChange = { index -> actions.onSetKeyColor(colorValues[index]) } ) AnimatedVisibility( visible = uiState.keyColor != 0 ) { Column { val styles = PaletteStyle.entries SuperDropdown( title = stringResource(R.string.settings_color_style), startAction = { Icon( Icons.Rounded.Style, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_color_style), tint = colorScheme.onBackground ) }, items = styles.map { it.name }, selectedIndex = styles.indexOfFirst { it.name == uiState.colorStyle }.coerceAtLeast(0), onSelectedIndexChange = { index -> actions.onSetColorStyle(styles[index].name) } ) val specs = ColorSpec.SpecVersion.entries SuperDropdown( title = stringResource(R.string.settings_color_spec), startAction = { Icon( Icons.Rounded.DesignServices, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_color_spec), tint = colorScheme.onBackground ) }, items = specs.map { it.name }, selectedIndex = specs.indexOfFirst { it.name == uiState.colorSpec }.coerceAtLeast(0), onSelectedIndexChange = { index -> actions.onSetColorSpec(specs[index].name) } ) } } } } } Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { SuperSwitch( title = stringResource(id = R.string.settings_enable_blur), summary = stringResource(id = R.string.settings_enable_blur_summary), startAction = { Icon( Icons.Rounded.BlurOn, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_enable_blur), tint = colorScheme.onBackground ) }, checked = uiState.enableBlur, onCheckedChange = { actions.onSetEnableBlur(it) } ) } SuperSwitch( title = stringResource(id = R.string.settings_floating_bottom_bar), summary = stringResource(id = R.string.settings_floating_bottom_bar_summary), startAction = { Icon( Icons.Rounded.CallToAction, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_floating_bottom_bar), tint = colorScheme.onBackground ) }, checked = uiState.enableFloatingBottomBar, onCheckedChange = { actions.onSetEnableFloatingBottomBar(it) } ) AnimatedVisibility(visible = uiState.enableFloatingBottomBar && Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { SuperSwitch( title = stringResource(id = R.string.settings_enable_glass), summary = stringResource(id = R.string.settings_enable_glass_summary), startAction = { Icon( Icons.Rounded.WaterDrop, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_enable_glass), tint = colorScheme.onBackground ) }, checked = uiState.enableFloatingBottomBarBlur, onCheckedChange = { actions.onSetEnableFloatingBottomBarBlur(it) } ) } } Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { SuperSwitch( title = stringResource(id = R.string.settings_enable_predictive_back), summary = stringResource(id = R.string.settings_enable_predictive_back_summary), startAction = { Icon( Icons.Rounded.Adb, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_enable_predictive_back), tint = colorScheme.onBackground ) }, checked = uiState.enablePredictiveBack, onCheckedChange = { actions.onSetEnablePredictiveBack(it) } ) } var sliderValue by remember(uiState.pageScale) { mutableFloatStateOf(uiState.pageScale) } SuperArrow( title = stringResource(id = R.string.settings_page_scale), summary = stringResource(id = R.string.settings_page_scale_summary), startAction = { Icon( Icons.Rounded.AspectRatio, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_page_scale), tint = colorScheme.onBackground ) }, endActions = { Text( text = "${(sliderValue * 100).toInt()}%", color = colorScheme.onSurfaceVariantActions, ) }, onClick = { showScaleDialog.value = !showScaleDialog.value }, holdDownState = showScaleDialog.value, bottomAction = { Slider( value = sliderValue, onValueChange = { sliderValue = it }, onValueChangeFinished = { actions.onSetPageScale(sliderValue) }, valueRange = 0.8f..1.1f, showKeyPoints = true, keyPoints = listOf(0.8f, 0.9f, 1f, 1.1f), magnetThreshold = 0.01f, hapticEffect = SliderDefaults.SliderHapticEffect.Step, ) }, ) ScaleDialog( show = showScaleDialog.value, onDismissRequest = { showScaleDialog.value = false }, volumeState = { uiState.pageScale }, onVolumeChange = { actions.onSetPageScale(it) } ) } } item { Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 12.dp ) ) } } } } @SuppressLint("ConfigurationScreenWidthHeight") @Composable private fun ThemePreviewCardMiuix( keyColor: Int, isDark: Boolean, miuixMonet: Boolean, enableFloatingBottomBar: Boolean = false, enableFloatingBottomBarBlur: Boolean = false, paletteStyle: PaletteStyle = PaletteStyle.TonalSpot, colorSpec: ColorSpec.SpecVersion = ColorSpec.SpecVersion.SPEC_2021, ) { val configuration = LocalConfiguration.current val screenWidth = configuration.screenWidthDp.toFloat() val screenHeight = configuration.screenHeightDp.toFloat() val screenRatio = screenWidth / screenHeight val seedColor = if (keyColor == 0) colorScheme.primary else Color(keyColor) val effectiveStyle = if (keyColor == 0) PaletteStyle.TonalSpot else paletteStyle val effectiveSpec = if (keyColor == 0) ColorSpec.SpecVersion.Default else colorSpec val dynamicCs = rememberDynamicColorScheme( seedColor = seedColor, isDark = isDark, style = effectiveStyle, specVersion = effectiveSpec, ) val bgColor = if (miuixMonet) dynamicCs.background else colorScheme.surface val textColor = if (miuixMonet) dynamicCs.onSurface else colorScheme.onBackground val accentCardColor = when { miuixMonet -> dynamicCs.secondaryContainer isDark -> Color(0xFF1A3825) else -> Color(0xFFDFFAE4) } val cardColor = if (miuixMonet) dynamicCs.surfaceContainerHighest else colorScheme.surfaceVariant val navBarColor = if (miuixMonet) dynamicCs.surfaceContainer else colorScheme.surface val iconColor = if (miuixMonet) dynamicCs.primary else colorScheme.primary val navSelectedColor = colorScheme.onSurfaceContainer val navUnselectedColor = colorScheme.onSurfaceContainer.copy(alpha = 0.5f) Box( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp), contentAlignment = Alignment.TopCenter ) { Box( modifier = Modifier .fillMaxWidth(0.4f) .aspectRatio(screenRatio) .clip(ContinuousRoundedRectangle(20.dp)) .background(bgColor) .border(1.dp, colorScheme.outline, ContinuousRoundedRectangle(20.dp)) ) { Column { Row( modifier = Modifier .height(48.dp) .fillMaxWidth() .padding(start = 12.dp, top = 24.dp), verticalAlignment = Alignment.CenterVertically ) { Text( text = stringResource(id = R.string.app_name), fontSize = 12.sp, color = textColor ) } Row( modifier = Modifier .fillMaxWidth() .height(65.dp) .padding(horizontal = 8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp) ) { Box( modifier = Modifier .weight(1f) .fillMaxHeight() .clip(RoundedCornerShape(6.dp)) .background(accentCardColor) ) Column( modifier = Modifier .weight(1f) .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(6.dp) ) { Box( modifier = Modifier .fillMaxWidth() .weight(1f) .clip(RoundedCornerShape(6.dp)) .background(cardColor) ) Box( modifier = Modifier .fillMaxWidth() .weight(1f) .clip(RoundedCornerShape(6.dp)) .background(cardColor) ) } } Column( modifier = Modifier .weight(1f) .padding(horizontal = 8.dp, vertical = 6.dp), verticalArrangement = Arrangement.spacedBy(6.dp) ) { Box( modifier = Modifier .fillMaxWidth() .weight(0.8f) .clip(RoundedCornerShape(6.dp)) .background(cardColor) ) Box( modifier = Modifier .fillMaxWidth() .weight(.1f) .clip(RoundedCornerShape(6.dp)) .background(cardColor) ) Box( modifier = Modifier .fillMaxWidth() .weight(.1f) .clip(RoundedCornerShape(6.dp)) .background(cardColor) ) } } if (enableFloatingBottomBar) { Box( modifier = Modifier .align(Alignment.BottomCenter) .padding(bottom = 8.dp), ) { Row( modifier = Modifier .height(28.dp) .clip(RoundedCornerShape(14.dp)) .background( if (enableFloatingBottomBarBlur) navBarColor.copy(alpha = 0.5f) else navBarColor ) .border(0.5.dp, textColor.copy(alpha = 0.1f), RoundedCornerShape(14.dp)) .padding(horizontal = 12.dp), horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically ) { repeat(4) { Box( modifier = Modifier .size(13.dp) .clip(RoundedCornerShape(2.dp)) .background(if (it == 0) iconColor else textColor) ) } } } } else { Column( modifier = Modifier .align(Alignment.BottomCenter) .fillMaxWidth() ) { Box( modifier = Modifier .fillMaxWidth() .height(0.5.dp) .background(textColor.copy(alpha = 0.1f)) ) Row( modifier = Modifier .height(36.dp) .fillMaxWidth() .background(navBarColor) .padding(top = 2.dp, bottom = 8.dp), horizontalArrangement = Arrangement.SpaceEvenly, verticalAlignment = Alignment.CenterVertically ) { repeat(4) { Box( modifier = Modifier .size(15.dp) .clip(RoundedCornerShape(3.dp)) .background(if (it == 0) navSelectedColor else navUnselectedColor) ) } } } } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/colorpalette/ColorPaletteUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.colorpalette import androidx.compose.runtime.Immutable import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.ui.screen.settings.SettingsUiState import me.weishu.kernelsu.ui.theme.ColorMode @Immutable data class ColorPaletteUiState( val uiState: SettingsUiState, val currentColorMode: ColorMode, val currentPaletteStyle: PaletteStyle, val currentColorSpec: ColorSpec.SpecVersion, ) @Immutable data class ColorPaletteScreenActions( val onBack: () -> Unit, val onSetThemeMode: (Int) -> Unit, val onSetMiuixMonet: (Boolean) -> Unit, val onSetKeyColor: (Int) -> Unit, val onSetColorMode: (ColorMode) -> Unit, val onSetColorStyle: (String) -> Unit, val onSetColorSpec: (String) -> Unit, val onSetEnableBlur: (Boolean) -> Unit, val onSetEnableFloatingBottomBar: (Boolean) -> Unit, val onSetEnableFloatingBottomBarBlur: (Boolean) -> Unit, val onSetEnablePredictiveBack: (Boolean) -> Unit, val onSetPageScale: (Float) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.executemoduleaction import android.annotation.SuppressLint import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Save import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker @SuppressLint("LocalContextGetResourceValueCall") @OptIn(ExperimentalMaterial3Api::class) @Composable fun ExecuteModuleActionScreenMaterial( state: ExecuteModuleActionUiState, actions: ExecuteModuleActionScreenActions, ) { val scrollState = rememberScrollState() BackHandler { } Scaffold( topBar = { TopAppBar( title = { Text(stringResource(R.string.action)) }, navigationIcon = { IconButton(onClick = actions.onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null ) } }, actions = { IconButton(onClick = actions.onSaveLog) { Icon( imageVector = Icons.Filled.Save, contentDescription = stringResource(R.string.save_log) ) } } ) }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout) .only(WindowInsetsSides.Horizontal) ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current val navBars = WindowInsets.navigationBars.asPaddingValues() val captionBar = WindowInsets.captionBar.asPaddingValues() KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize() .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), ) .verticalScroll(scrollState) ) { LaunchedEffect(state.text) { scrollState.animateScrollTo(scrollState.maxValue) } Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = state.text, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, ) Spacer( Modifier.height( 16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding() ) ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.executemoduleaction import androidx.activity.compose.BackHandler import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.SmallTopAppBar import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Download import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.scrollEndHaptic @Composable fun ExecuteModuleActionScreenMiuix( state: ExecuteModuleActionUiState, actions: ExecuteModuleActionScreenActions, ) { val scrollState = rememberScrollState() val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } BackHandler { } Scaffold( topBar = { TopBar( onBack = actions.onBack, onSave = actions.onSaveLog, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout) .only(WindowInsetsSides.Horizontal) ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize(1f) .scrollEndHaptic() .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), ) .verticalScroll(scrollState), ) { LaunchedEffect(state.text) { scrollState.animateScrollTo(scrollState.maxValue) } Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = state.text, fontSize = 12.sp, fontFamily = FontFamily.Monospace, ) Spacer( Modifier.height( 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } @Composable private fun TopBar( onBack: () -> Unit = {}, onSave: () -> Unit = {}, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { SmallTopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, title = stringResource(R.string.action), navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, actions = { IconButton( modifier = Modifier.padding(end = 16.dp), onClick = onSave ) { Icon( imageVector = MiuixIcons.Download, contentDescription = stringResource(id = R.string.save_log), tint = colorScheme.onBackground ) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.executemoduleaction import androidx.activity.compose.LocalActivity import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.dropUnlessResumed import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator @Composable fun ExecuteModuleActionScreen(moduleId: String, fromShortcut: Boolean = false) { val navigator = LocalNavigator.current val context = LocalContext.current val activity = LocalActivity.current val scope = rememberCoroutineScope() var text by rememberSaveable { mutableStateOf("") } val logContent = rememberSaveable { StringBuilder() } val exitExecute = { if (fromShortcut && activity != null) { activity.finishAndRemoveTask() } else { navigator.pop() } } ExecuteModuleActionEffect( moduleId = moduleId, text = text, logContent = logContent, fromShortcut = fromShortcut, onTextUpdate = { text = it }, onExit = exitExecute ) val state = ExecuteModuleActionUiState( text = text, ) val actions = ExecuteModuleActionScreenActions( onBack = dropUnlessResumed { navigator.pop() }, onSaveLog = saveLog(logContent, context, scope), ) when (LocalUiMode.current) { UiMode.Miuix -> ExecuteModuleActionScreenMiuix(state, actions) UiMode.Material -> ExecuteModuleActionScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.executemoduleaction import androidx.compose.runtime.Immutable @Immutable data class ExecuteModuleActionUiState( val text: String, ) @Immutable data class ExecuteModuleActionScreenActions( val onBack: () -> Unit, val onSaveLog: () -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/executemoduleaction/ExecuteModuleActionUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.executemoduleaction import android.content.Context import android.os.Environment import android.os.Handler import android.os.Looper import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.data.repository.ModuleRepositoryImpl import me.weishu.kernelsu.ui.util.runModuleAction import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale @Composable fun ExecuteModuleActionEffect( moduleId: String, text: String, logContent: StringBuilder, fromShortcut: Boolean, onTextUpdate: (String) -> Unit, onExit: () -> Unit ) { val context = LocalContext.current val noModule = stringResource(R.string.no_such_module) val moduleUnavailable = stringResource(R.string.module_unavailable) val moduleActionSuccess = stringResource(R.string.module_action_success) LaunchedEffect(Unit) { if (text.isNotEmpty()) { return@LaunchedEffect } val repo = ModuleRepositoryImpl() val modules = repo.getModules().getOrDefault(emptyList()) val moduleInfo = modules.find { info -> info.id == moduleId } if (moduleInfo == null) { Toast.makeText(context, noModule.format(moduleId), Toast.LENGTH_SHORT).show() onExit() return@LaunchedEffect } if (!moduleInfo.hasActionScript) { onExit() return@LaunchedEffect } if (!moduleInfo.enabled || moduleInfo.update || moduleInfo.remove) { Toast.makeText(context, moduleUnavailable.format(moduleInfo.name), Toast.LENGTH_SHORT).show() onExit() return@LaunchedEffect } var actionResult: Boolean var currentText = text val mainHandler = Handler(Looper.getMainLooper()) withContext(Dispatchers.IO) { runModuleAction( moduleId = moduleId, onStdout = { val tempText = "$it\n" if (tempText.startsWith("")) { // clear command currentText = tempText.substring(6) } else { currentText += tempText } mainHandler.post { onTextUpdate(currentText) } logContent.append(it).append("\n") }, onStderr = { logContent.append(it).append("\n") } ).let { actionResult = it } } if (actionResult) { if (fromShortcut) { Toast.makeText( context, moduleActionSuccess, Toast.LENGTH_SHORT ).show() } onExit() } } } fun saveLog( logContent: StringBuilder, context: Context, scope: CoroutineScope ): () -> Unit { return { scope.launch { val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) val date = format.format(Date()) val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "KernelSU_module_action_log_${date}.log" ) file.writeText(logContent.toString()) Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.flash import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Save import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Modifier import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker @OptIn(ExperimentalMaterial3Api::class) @Composable fun FlashScreenMaterial( state: FlashUiState, actions: FlashScreenActions, ) { val scrollState = rememberScrollState() if (state.showJailbreakWarning) { JailbreakFlashWarningDialog( onConfirm = actions.onConfirmJailbreakWarning, onDismiss = actions.onDismissJailbreakWarning, ) } Scaffold( topBar = { TopAppBar( title = { Text( stringResource( when (state.flashingStatus) { FlashingStatus.FLASHING -> R.string.flashing FlashingStatus.SUCCESS -> R.string.flash_success FlashingStatus.FAILED -> R.string.flash_failed } ) ) }, navigationIcon = { IconButton(onClick = actions.onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, null) } }, actions = { IconButton(onClick = actions.onSaveLog) { Icon(Icons.Filled.Save, stringResource(R.string.save_log)) } } ) }, floatingActionButton = { if (state.showRebootAction) { ExtendedFloatingActionButton( onClick = actions.onReboot, icon = { Icon(Icons.Filled.Refresh, null) }, text = { Text(stringResource(R.string.reboot)) }, modifier = Modifier.padding( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding(), ) ) } }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current val navBars = WindowInsets.navigationBars.asPaddingValues() val captionBar = WindowInsets.captionBar.asPaddingValues() KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize() .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), ) .verticalScroll(scrollState) ) { LaunchedEffect(state.text) { scrollState.animateScrollTo(scrollState.maxValue) } Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = state.text, style = MaterialTheme.typography.bodySmall, fontFamily = FontFamily.Monospace, ) Spacer( Modifier.height( 16.dp + 54.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding() ) ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.flash import androidx.compose.foundation.border import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Refresh import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.key import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.KeyEventBlocker import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect 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.Scaffold import top.yukonga.miuix.kmp.basic.SmallTopAppBar import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Share import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/1/1. */ // Lets you flash modules sequentially when mutiple zipUris are selected @Composable fun FlashScreenMiuix( state: FlashUiState, actions: FlashScreenActions, ) { val enableBlur = LocalEnableBlur.current val scrollState = rememberScrollState() val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } if (state.showJailbreakWarning) { JailbreakFlashWarningDialog( onConfirm = actions.onConfirmJailbreakWarning, onDismiss = actions.onDismissJailbreakWarning, ) } Scaffold( topBar = { TopBar( state.flashingStatus, onBack = actions.onBack, onSave = actions.onSaveLog, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, floatingActionButton = { if (state.showRebootAction) { val reboot = stringResource(id = R.string.reboot) FloatingActionButton( modifier = Modifier .padding( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, end = 20.dp ) .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), onClick = actions.onReboot, shadowElevation = 0.dp, content = { Icon( Icons.Rounded.Refresh, reboot, Modifier.size(40.dp), tint = colorScheme.onPrimary ) }, ) } }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current KeyEventBlocker { it.key == Key.VolumeDown || it.key == Key.VolumeUp } Column( modifier = Modifier .fillMaxSize(1f) .scrollEndHaptic() .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), ) .verticalScroll(scrollState), ) { LaunchedEffect(state.text) { scrollState.animateScrollTo(scrollState.maxValue) } Spacer(Modifier.height(innerPadding.calculateTopPadding())) Text( modifier = Modifier.padding(8.dp), text = state.text, fontSize = 12.sp, fontFamily = FontFamily.Monospace, ) Spacer( Modifier.height( 12.dp + WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } @Composable private fun TopBar( status: FlashingStatus, onBack: () -> Unit = {}, onSave: () -> Unit = {}, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { SmallTopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, title = stringResource( when (status) { FlashingStatus.FLASHING -> R.string.flashing FlashingStatus.SUCCESS -> R.string.flash_success FlashingStatus.FAILED -> R.string.flash_failed } ), color = if (enableBlur) Color.Transparent else colorScheme.surface, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, actions = { IconButton( modifier = Modifier.padding(end = 16.dp), onClick = onSave ) { Icon( imageVector = MiuixIcons.Share, contentDescription = stringResource(id = R.string.save_log), tint = colorScheme.onBackground ) } }, ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.flash import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.dropUnlessResumed import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.util.reboot @Composable fun FlashScreen(flashIt: FlashIt) { val navigator = LocalNavigator.current val context = LocalContext.current val scope = rememberCoroutineScope() var text by rememberSaveable { mutableStateOf("") } val logContent = rememberSaveable { StringBuilder() } var showRebootAction by rememberSaveable { mutableStateOf(false) } var flashingStatus by rememberSaveable { mutableStateOf(FlashingStatus.FLASHING) } val needJailbreakWarning = flashIt is FlashIt.FlashBoot && Natives.isLateLoadMode var flashingEnabled by rememberSaveable { mutableStateOf(!needJailbreakWarning) } FlashEffect( flashIt = flashIt, text = text, logContent = logContent, onTextUpdate = { text = it }, onShowRebootChange = { showRebootAction = it }, onFlashingStatusChange = { flashingStatus = it }, enabled = flashingEnabled, ) val state = FlashUiState( text = text, showRebootAction = showRebootAction, flashingStatus = flashingStatus, showJailbreakWarning = needJailbreakWarning && !flashingEnabled, ) val actions = FlashScreenActions( onBack = dropUnlessResumed { navigator.pop() }, onSaveLog = saveLog(logContent, context, scope), onReboot = { scope.launch { withContext(Dispatchers.IO) { reboot() } } }, onConfirmJailbreakWarning = { flashingEnabled = true }, onDismissJailbreakWarning = dropUnlessResumed { navigator.pop() }, ) when (LocalUiMode.current) { UiMode.Miuix -> FlashScreenMiuix(state, actions) UiMode.Material -> FlashScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.flash import androidx.compose.runtime.Immutable @Immutable data class FlashUiState( val text: String, val showRebootAction: Boolean, val flashingStatus: FlashingStatus, val showJailbreakWarning: Boolean, ) @Immutable data class FlashScreenActions( val onBack: () -> Unit, val onSaveLog: () -> Unit, val onReboot: () -> Unit, val onConfirmJailbreakWarning: () -> Unit, val onDismissJailbreakWarning: () -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/flash/FlashUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.flash import android.content.Context import android.net.Uri import android.os.Environment import android.os.Handler import android.os.Looper import android.os.Parcelable import android.widget.Toast import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Adb import androidx.compose.material.icons.rounded.DeleteForever import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material3.AlertDialog import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.stringResource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.util.FlashResult import me.weishu.kernelsu.ui.util.LkmSelection import me.weishu.kernelsu.ui.util.flashModule import me.weishu.kernelsu.ui.util.installBoot import me.weishu.kernelsu.ui.util.restoreBoot import me.weishu.kernelsu.ui.util.uninstallPermanently import java.io.File import java.text.SimpleDateFormat import java.util.Date import java.util.Locale enum class FlashingStatus { FLASHING, SUCCESS, FAILED } enum class UninstallType(val icon: ImageVector, val title: Int, val message: Int) { TEMPORARY( Icons.Rounded.RemoveModerator, R.string.settings_uninstall_temporary, R.string.settings_uninstall_temporary_message ), PERMANENT( Icons.Rounded.DeleteForever, R.string.settings_uninstall_permanent, R.string.settings_uninstall_permanent_message ), RESTORE_STOCK_IMAGE( Icons.Rounded.RestartAlt, R.string.settings_restore_stock_image, R.string.settings_restore_stock_image_message ), NONE(Icons.Rounded.Adb, 0, 0) } @Parcelize sealed class FlashIt : Parcelable { @Parcelize data class FlashBoot( val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null, val allowShell: Boolean = false, val enableAdb: Boolean = false, ) : FlashIt() @Parcelize data class FlashModules(val uris: List) : FlashIt() @Parcelize data object FlashRestore : FlashIt() @Parcelize data object FlashUninstall : FlashIt() } fun flashModulesSequentially( uris: List, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): FlashResult { for (uri in uris) { flashModule(uri, onStdout, onStderr).apply { if (code != 0) { return FlashResult(code, err, showReboot) } } } return FlashResult(0, "", true) } fun flashIt( flashIt: FlashIt, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): FlashResult { return when (flashIt) { is FlashIt.FlashBoot -> installBoot( flashIt.boot, flashIt.lkm, flashIt.ota, flashIt.partition, flashIt.allowShell, flashIt.enableAdb, onStdout, onStderr ) is FlashIt.FlashModules -> { flashModulesSequentially(flashIt.uris, onStdout, onStderr) } FlashIt.FlashRestore -> restoreBoot(onStdout, onStderr) FlashIt.FlashUninstall -> uninstallPermanently(onStdout, onStderr) } } @Composable fun FlashEffect( flashIt: FlashIt, text: String, logContent: StringBuilder, onTextUpdate: (String) -> Unit, onShowRebootChange: (Boolean) -> Unit, onFlashingStatusChange: (FlashingStatus) -> Unit, enabled: Boolean = true ) { LaunchedEffect(enabled) { if (!enabled || text.isNotEmpty()) { return@LaunchedEffect } var currentText = text val mainHandler = Handler(Looper.getMainLooper()) withContext(Dispatchers.IO) { flashIt(flashIt, onStdout = { val tempText = "$it\n" if (tempText.startsWith("")) { // clear command currentText = tempText.substring(6) } else { currentText += tempText } mainHandler.post { onTextUpdate(currentText) } logContent.append(it).append("\n") }, onStderr = { logContent.append(it).append("\n") }).apply { if (code != 0) { currentText += "Error code: $code.\n $err Please save and check the log.\n" mainHandler.post { onTextUpdate(currentText) } } if (showReboot) { currentText += "\n\n\n" mainHandler.post { onTextUpdate(currentText) onShowRebootChange(true) } } mainHandler.post { onFlashingStatusChange(if (code == 0) FlashingStatus.SUCCESS else FlashingStatus.FAILED) } } } } } fun saveLog( logContent: StringBuilder, context: Context, scope: CoroutineScope ): () -> Unit { return { scope.launch { val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.getDefault()) val date = format.format(Date()) val file = File( Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "KernelSU_install_log_${date}.log" ) file.writeText(logContent.toString()) Toast.makeText(context, "Log saved to ${file.absolutePath}", Toast.LENGTH_SHORT).show() } } } private const val JAILBREAK_WARNING_COUNTDOWN = 10 @Composable fun JailbreakFlashWarningDialog( onConfirm: () -> Unit, onDismiss: () -> Unit ) { var countdown by remember { mutableIntStateOf(JAILBREAK_WARNING_COUNTDOWN) } LaunchedEffect(Unit) { while (countdown > 0) { delay(1000) countdown-- } } AlertDialog( onDismissRequest = onDismiss, title = { Text(stringResource(android.R.string.dialog_alert_title)) }, text = { Text( stringResource(R.string.jailbreak_flash_warning), style = MaterialTheme.typography.bodyMedium ) }, confirmButton = { TextButton( onClick = onConfirm, enabled = countdown == 0 ) { Text( if (countdown > 0) stringResource(R.string.jailbreak_flash_warning_countdown, countdown) else stringResource(R.string.install_next) ) } }, dismissButton = { TextButton(onClick = onDismiss) { Text(stringResource(android.R.string.cancel)) } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.home 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.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides 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.safeDrawing import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Block import androidx.compose.material.icons.outlined.CheckCircle import androidx.compose.material.icons.outlined.Warning import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import me.weishu.kernelsu.KernelVersion import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopup import me.weishu.kernelsu.ui.component.statustag.StatusTag @OptIn(ExperimentalMaterial3Api::class) @Composable fun HomePagerMaterial( state: HomeUiState, actions: HomeActions, bottomInnerPadding: Dp, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) Scaffold( topBar = { TopBar(scrollBehavior = scrollBehavior) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { StatusCard( state = state, actions = actions, ) if (state.showManagerPrBuildWarning) { WarningCard(stringResource(id = R.string.home_pr_build_warning)) } else if (state.showKernelPrBuildWarning) { WarningCard(stringResource(id = R.string.home_pr_kernel_warning)) } if (state.showVersionMismatchWarning) { WarningCard( stringResource(id = R.string.home_version_mismatch).format( state.currentManagerVersionCode, state.ksuVersion ) ) } if (state.showGkiWarning) { WarningCard(stringResource(id = R.string.home_gki_warning)) } if (state.showRequireKernelWarning) { WarningCard( stringResource(id = R.string.require_kernel_version).format( state.ksuVersion, me.weishu.kernelsu.Natives.MINIMAL_SUPPORTED_KERNEL ) ) } if (state.showRootWarning) { WarningCard(stringResource(id = R.string.grant_root_failed)) } if (state.checkUpdateEnabled) { UpdateCard(state = state, actions = actions) } InfoCard(systemInfo = state.systemInfo) DonateCard(onOpenUrl = actions.onOpenUrl) LearnMoreCard(onOpenUrl = actions.onOpenUrl) Spacer(Modifier.height(bottomInnerPadding)) } } } @Composable private fun UpdateCard( state: HomeUiState, actions: HomeActions, ) { val newVersion = state.latestVersionInfo val title = stringResource(id = R.string.module_changelog) val updateText = stringResource(id = R.string.module_update) AnimatedVisibility( visible = state.hasUpdate, enter = fadeIn() + expandVertically(), exit = shrinkVertically() + fadeOut() ) { val updateDialog = rememberConfirmDialog(onConfirm = { actions.onOpenUrl(newVersion.downloadUrl) }) WarningCard( message = stringResource(id = R.string.new_version_available).format(newVersion.versionCode), MaterialTheme.colorScheme.outlineVariant ) { if (newVersion.changelog.isEmpty()) { actions.onOpenUrl(newVersion.downloadUrl) } else { updateDialog.showConfirm( title = title, content = newVersion.changelog, markdown = true, confirm = updateText ) } } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( scrollBehavior: TopAppBarScrollBehavior? = null ) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.app_name)) }, actions = { RebootListPopup() }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } @Composable private fun StatusCard( state: HomeUiState, actions: HomeActions, ) { Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { TonalCard( containerColor = if (state.ksuVersion != null) { MaterialTheme.colorScheme.secondaryContainer } else { MaterialTheme.colorScheme.errorContainer } ) { Row( modifier = Modifier .fillMaxWidth() .clickable(enabled = !state.isLateLoadMode) { actions.onInstallClick() } .padding(24.dp), verticalAlignment = Alignment.CenterVertically ) { when { state.ksuVersion != null -> { val workingMode = when (state.lkmMode) { null -> "" true -> "LKM" else -> "GKI" } Icon(Icons.Outlined.CheckCircle, stringResource(R.string.home_working)) Column(Modifier.padding(start = 20.dp)) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = stringResource(id = R.string.home_working), style = MaterialTheme.typography.titleMedium ) if (workingMode.isNotEmpty()) { Spacer(Modifier.width(8.dp)) StatusTag( label = workingMode, contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } if (state.isSafeMode) { Spacer(Modifier.width(8.dp)) StatusTag( label = stringResource(id = R.string.safe_mode), contentColor = MaterialTheme.colorScheme.onErrorContainer, backgroundColor = MaterialTheme.colorScheme.errorContainer ) } if (state.isLateLoadMode) { Spacer(Modifier.width(8.dp)) StatusTag( label = stringResource(id = R.string.jailbreak_mode), contentColor = MaterialTheme.colorScheme.onErrorContainer, backgroundColor = MaterialTheme.colorScheme.errorContainer ) } } Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_working_version, state.ksuVersion), style = MaterialTheme.typography.bodyMedium ) } } state.kernelVersion.isGKI() -> { Icon(Icons.Outlined.Warning, stringResource(R.string.home_not_installed)) Column( modifier = Modifier .padding(start = 20.dp) .weight(1f) ) { Text( text = stringResource(R.string.home_not_installed), style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_click_to_install), style = MaterialTheme.typography.bodyMedium ) } if (state.isSELinuxPermissive) { Button( onClick = actions.onJailbreakClick, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.error, contentColor = MaterialTheme.colorScheme.onError ) ) { Text(stringResource(R.string.home_jailbreak)) } } } else -> { Icon(Icons.Outlined.Block, stringResource(R.string.home_unsupported)) Column(Modifier.padding(start = 20.dp)) { Text( text = stringResource(R.string.home_unsupported), style = MaterialTheme.typography.titleMedium ) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_unsupported_reason), style = MaterialTheme.typography.bodyMedium ) } } } } } if (state.isFullFeatured) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) ) { TonalCard(modifier = Modifier.weight(1f)) { Column( modifier = Modifier .fillMaxWidth() .clickable { actions.onSuperuserClick() } .padding(horizontal = 24.dp, vertical = 16.dp) ) { Text( text = stringResource(R.string.superuser), style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(Modifier.height(4.dp)) Text( text = state.superuserCount.toString(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } } TonalCard(modifier = Modifier.weight(1f)) { Column( modifier = Modifier .fillMaxWidth() .clickable { actions.onModuleClick() } .padding(horizontal = 24.dp, vertical = 16.dp) ) { Text( text = stringResource(R.string.module), style = MaterialTheme.typography.bodyLarge, maxLines = 1, overflow = TextOverflow.Ellipsis ) Spacer(Modifier.height(4.dp)) Text( text = state.moduleCount.toString(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } } } } } } @Composable private fun WarningCard( message: String, color: Color = MaterialTheme.colorScheme.error, onClick: (() -> Unit)? = null ) { TonalCard(containerColor = color) { Row( modifier = Modifier .fillMaxWidth() .then(onClick?.let { Modifier.clickable { it() } } ?: Modifier) .padding(24.dp) ) { Text(text = message, style = MaterialTheme.typography.bodyMedium) } } } @Composable fun TonalCard( modifier: Modifier = Modifier, containerColor: Color = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp), shape: Shape = MaterialTheme.shapes.large, content: @Composable () -> Unit ) { Card( modifier = modifier, colors = CardDefaults.cardColors(containerColor = containerColor), shape = shape ) { content() } } @Composable private fun LearnMoreCard(onOpenUrl: (String) -> Unit) { val url = stringResource(R.string.home_learn_kernelsu_url) TonalCard { Row( modifier = Modifier .fillMaxWidth() .clickable { onOpenUrl(url) } .padding(24.dp), verticalAlignment = Alignment.CenterVertically ) { Column { Text(text = stringResource(R.string.home_learn_kernelsu), style = MaterialTheme.typography.titleSmall) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_click_to_learn_kernelsu), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } } } } @Composable private fun DonateCard(onOpenUrl: (String) -> Unit) { TonalCard { Row( modifier = Modifier .fillMaxWidth() .clickable { onOpenUrl("https://patreon.com/weishu") } .padding(24.dp), verticalAlignment = Alignment.CenterVertically ) { Column { Text(text = stringResource(R.string.home_support_title), style = MaterialTheme.typography.titleSmall) Spacer(Modifier.height(4.dp)) Text( text = stringResource(R.string.home_support_content), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } } } } @Composable private fun InfoCard(systemInfo: SystemInfo) { TonalCard { Column( modifier = Modifier .fillMaxWidth() .padding(start = 24.dp, top = 24.dp, end = 24.dp, bottom = 16.dp) ) { @Composable fun InfoCardItem(label: String, content: String) { Text(text = label, style = MaterialTheme.typography.bodyLarge) Text( text = content, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline ) } InfoCardItem(stringResource(R.string.home_kernel), systemInfo.kernelVersion) Spacer(Modifier.height(16.dp)) InfoCardItem(stringResource(R.string.home_manager_version), systemInfo.managerVersion) Spacer(Modifier.height(16.dp)) InfoCardItem(stringResource(R.string.home_fingerprint), systemInfo.fingerprint) Spacer(Modifier.height(16.dp)) InfoCardItem(stringResource(R.string.home_selinux_status), systemInfo.selinuxStatus) } } } @Preview(name = "Activated") @Composable private fun StatusCardActivatedPreview() { StatusCard( state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10), actions = HomeActions({}, {}, {}, {}) ) } @Preview(name = "Not Activated") @Composable private fun StatusCardNotActivatedPreview() { StatusCard(state = previewHomeScreenState(ksuVersion = null, lkmMode = null), actions = HomeActions({}, {}, {}, {})) } @Preview(name = "Permissive") @Composable private fun StatusCardPermissivePreview() { StatusCard( state = previewHomeScreenState(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true), actions = HomeActions({}, {}, {}, {}) ) } @Preview(name = "Jailbreak") @Composable private fun StatusCardJailbreakPreview() { StatusCard( state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10), actions = HomeActions({}, {}, {}, {}) ) } private val previewSystemInfo = SystemInfo( kernelVersion = "6.1.0-android14-0-g1234567", managerVersion = "1.0.0 (10000)", fingerprint = "google/raven/raven:14/AP1A.240305.019:user/release-keys", selinuxStatus = "Enforcing" ) private val previewUriHandler = object : UriHandler { override fun openUri(uri: String) {} } @Composable private fun HomeScreenPreviewContent( ksuVersion: Int?, lkmMode: Boolean?, isSafeMode: Boolean = false, isLateLoadMode: Boolean = false, isSELinuxPermissive: Boolean = false, superuserCount: Int = 0, moduleCount: Int = 0, selinuxStatus: String = "Enforcing", ) { CompositionLocalProvider(LocalUriHandler provides previewUriHandler) { Column( modifier = Modifier.padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(16.dp) ) { val actions = HomeActions({}, {}, {}, {}) StatusCard( state = previewHomeScreenState( ksuVersion = ksuVersion, lkmMode = lkmMode, isSafeMode = isSafeMode, isLateLoadMode = isLateLoadMode, isSELinuxPermissive = isSELinuxPermissive, superuserCount = superuserCount, moduleCount = moduleCount, selinuxStatus = selinuxStatus, ), actions = actions ) InfoCard(previewSystemInfo.copy(selinuxStatus = selinuxStatus)) DonateCard(onOpenUrl = {}) LearnMoreCard(onOpenUrl = {}) } } } @Preview(name = "Home Activated", showBackground = true) @Composable private fun HomeScreenActivatedPreview() { HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10) } @Preview(name = "Home Not Activated", showBackground = true) @Composable private fun HomeScreenNotActivatedPreview() { HomeScreenPreviewContent(ksuVersion = null, lkmMode = null) } @Preview(name = "Home Permissive", showBackground = true) @Composable private fun HomeScreenPermissivePreview() { HomeScreenPreviewContent(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true, selinuxStatus = "Permissive") } @Preview(name = "Home Jailbreak", showBackground = true) @Composable private fun HomeScreenJailbreakPreview() { HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10) } private fun previewHomeScreenState( ksuVersion: Int?, lkmMode: Boolean?, isSafeMode: Boolean = false, isLateLoadMode: Boolean = false, isSELinuxPermissive: Boolean = false, superuserCount: Int = 0, moduleCount: Int = 0, selinuxStatus: String = "Enforcing", ) = HomeUiState( kernelVersion = KernelVersion(6, 1, 0), ksuVersion = ksuVersion, lkmMode = lkmMode, isManager = true, isManagerPrBuild = false, isKernelPrBuild = false, requiresNewKernel = false, isRootAvailable = ksuVersion != null, isSafeMode = isSafeMode, isLateLoadMode = isLateLoadMode, isSELinuxPermissive = isSELinuxPermissive, checkUpdateEnabled = false, latestVersionInfo = me.weishu.kernelsu.ui.util.module.LatestVersionInfo(), currentManagerVersionCode = 10000, superuserCount = superuserCount, moduleCount = moduleCount, systemInfo = previewSystemInfo.copy(selinuxStatus = selinuxStatus), ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.home 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.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer 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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.CheckCircleOutline import androidx.compose.material.icons.rounded.ErrorOutline import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember 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.LocalUriHandler import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.KernelVersion import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopupMiuix import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.isInDarkTheme import me.weishu.kernelsu.ui.util.defaultHazeEffect import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CardDefaults 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.ScrollBehavior import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Link import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.theme.MiuixTheme.isDynamicColor import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic @Composable fun HomePagerMiuix( state: HomeUiState, actions: HomeActions, bottomInnerPadding: Dp, ) { val scrollBehavior = MiuixScrollBehavior() val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopBar( scrollBehavior = scrollBehavior, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .padding(horizontal = 12.dp) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, contentPadding = innerPadding, overscrollEffect = null, ) { item { Column( modifier = Modifier.padding(vertical = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { if (state.showManagerPrBuildWarning) { WarningCard(stringResource(id = R.string.home_pr_build_warning)) } else if (state.showKernelPrBuildWarning) { WarningCard(stringResource(id = R.string.home_pr_kernel_warning)) } if (state.showVersionMismatchWarning) { WarningCard( stringResource(id = R.string.home_version_mismatch).format( state.currentManagerVersionCode, state.ksuVersion ) ) } if (state.showGkiWarning) { WarningCard(stringResource(id = R.string.home_gki_warning)) } if (state.showRequireKernelWarning) { WarningCard( stringResource(id = R.string.require_kernel_version) .format(state.ksuVersion, me.weishu.kernelsu.Natives.MINIMAL_SUPPORTED_KERNEL), ) } if (state.showRootWarning) { WarningCard(stringResource(id = R.string.grant_root_failed)) } StatusCard( state = state, actions = actions, ) if (state.checkUpdateEnabled) { UpdateCard(state = state, actions = actions) } InfoCard(systemInfo = state.systemInfo) DonateCard(onOpenUrl = actions.onOpenUrl) LearnMoreCard(onOpenUrl = actions.onOpenUrl) } Spacer(Modifier.height(bottomInnerPadding)) } } } } @Composable private fun UpdateCard( state: HomeUiState, actions: HomeActions, ) { val newVersion = state.latestVersionInfo val title = stringResource(id = R.string.module_changelog) val updateText = stringResource(id = R.string.module_update) AnimatedVisibility( visible = state.hasUpdate, enter = fadeIn() + expandVertically(), exit = shrinkVertically() + fadeOut() ) { val updateDialog = rememberConfirmDialog(onConfirm = { actions.onOpenUrl(newVersion.downloadUrl) }) WarningCard( message = stringResource(id = R.string.new_version_available).format(newVersion.versionCode), colorScheme.outline ) { if (newVersion.changelog.isEmpty()) { actions.onOpenUrl(newVersion.downloadUrl) } else { updateDialog.showConfirm( title = title, content = newVersion.changelog, markdown = true, confirm = updateText ) } } } } @Composable private fun TopBar( scrollBehavior: ScrollBehavior, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean, ) { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.app_name), actions = { RebootListPopupMiuix(modifier = Modifier.padding(end = 16.dp)) }, scrollBehavior = scrollBehavior ) } @Composable private fun StatusCard( state: HomeUiState, actions: HomeActions, ) { Column { when { state.ksuVersion != null -> { val workingState = buildString { if (state.isSafeMode) { append(" [${stringResource(id = R.string.safe_mode)}]") } if (state.isLateLoadMode) { append(" [${stringResource(id = R.string.jailbreak_mode)}]") } } val workingMode = when (state.lkmMode) { null -> "" true -> " " else -> " " } val workingText = "${stringResource(id = R.string.home_working)}$workingMode$workingState" Row( modifier = Modifier .fillMaxWidth() .height(IntrinsicSize.Min), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically, ) { Card( modifier = Modifier .weight(1f) .fillMaxHeight(), colors = CardDefaults.defaultColors( color = when { isDynamicColor -> colorScheme.secondaryContainer isInDarkTheme() -> Color(0xFF1A3825) else -> Color(0xFFDFFAE4) } ), onClick = { if (!state.isLateLoadMode) { actions.onInstallClick() } }, showIndication = !state.isLateLoadMode, pressFeedbackType = PressFeedbackType.Tilt ) { Box(modifier = Modifier.fillMaxSize()) { Box( modifier = Modifier .fillMaxSize() .offset(38.dp, 45.dp), contentAlignment = Alignment.BottomEnd ) { Icon( modifier = Modifier.size(170.dp), imageVector = Icons.Rounded.CheckCircleOutline, tint = if (isDynamicColor) { colorScheme.primary.copy(alpha = 0.8f) } else { Color(0xFF36D167) }, contentDescription = null ) } Column( modifier = Modifier .fillMaxSize() .padding(all = 16.dp) ) { Text( modifier = Modifier.fillMaxWidth(), text = workingText, fontSize = 20.sp, fontWeight = FontWeight.SemiBold, ) Spacer(Modifier.height(2.dp)) Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.home_working_version, state.ksuVersion), fontSize = 14.sp, fontWeight = FontWeight.Medium, ) } } } Column( modifier = Modifier .weight(1f) .fillMaxHeight() ) { Card( modifier = Modifier .fillMaxWidth() .weight(1f), insideMargin = PaddingValues(16.dp), onClick = { actions.onSuperuserClick() }, showIndication = true, pressFeedbackType = PressFeedbackType.Tilt ) { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.superuser), fontWeight = FontWeight.Medium, fontSize = 15.sp, color = colorScheme.onSurfaceVariantSummary, ) Text( modifier = Modifier.fillMaxWidth(), text = state.superuserCount.toString(), fontSize = 26.sp, fontWeight = FontWeight.SemiBold, color = colorScheme.onSurface, ) } } Spacer(Modifier.height(12.dp)) Card( modifier = Modifier .fillMaxWidth() .weight(1f), insideMargin = PaddingValues(16.dp), onClick = { actions.onModuleClick() }, showIndication = true, pressFeedbackType = PressFeedbackType.Tilt ) { Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.Start) { Text( modifier = Modifier.fillMaxWidth(), text = stringResource(R.string.module), fontWeight = FontWeight.Medium, fontSize = 15.sp, color = colorScheme.onSurfaceVariantSummary, ) Text( modifier = Modifier.fillMaxWidth(), text = state.moduleCount.toString(), fontSize = 26.sp, fontWeight = FontWeight.SemiBold, color = colorScheme.onSurface, ) } } } } } state.kernelVersion.isGKI() -> { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { Card( modifier = Modifier.weight(1f), onClick = { if (!state.isLateLoadMode) { actions.onInstallClick() } }, showIndication = !state.isLateLoadMode, pressFeedbackType = PressFeedbackType.Sink ) { BasicComponent( title = stringResource(R.string.home_not_installed), summary = stringResource(R.string.home_click_to_install), startAction = { Icon( Icons.Rounded.ErrorOutline, stringResource(R.string.home_not_installed), modifier = Modifier.padding(end = 16.dp), tint = colorScheme.onBackground, ) }, endActions = { if (state.isSELinuxPermissive) { TextButton( text = stringResource(R.string.home_jailbreak), insideMargin = PaddingValues(12.dp), onClick = actions.onJailbreakClick, colors = ButtonDefaults.textButtonColorsPrimary() ) } } ) } } } else -> { Card( onClick = { if (!state.isLateLoadMode) { actions.onInstallClick() } }, showIndication = !state.isLateLoadMode, pressFeedbackType = PressFeedbackType.Sink ) { BasicComponent( title = stringResource(R.string.home_unsupported), summary = stringResource(R.string.home_unsupported_reason), startAction = { Icon( Icons.Rounded.ErrorOutline, stringResource(R.string.home_unsupported), modifier = Modifier.padding(end = 16.dp), tint = colorScheme.onBackground, ) } ) } } } } } @Composable private fun WarningCard( message: String, color: Color? = null, onClick: (() -> Unit)? = null, ) { Card( onClick = { onClick?.invoke() }, colors = CardDefaults.defaultColors( color = color ?: when { isDynamicColor -> colorScheme.errorContainer isInDarkTheme() -> Color(0XFF310808) else -> Color(0xFFF8E2E2) } ), showIndication = onClick != null, pressFeedbackType = PressFeedbackType.Tilt ) { Row( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Text( text = message, color = if (isDynamicColor) colorScheme.onErrorContainer else Color(0xFFF72727), fontSize = 14.sp ) } } } @Composable private fun LearnMoreCard( onOpenUrl: (String) -> Unit, ) { val url = stringResource(R.string.home_learn_kernelsu_url) Card(modifier = Modifier.fillMaxWidth()) { BasicComponent( title = stringResource(R.string.home_learn_kernelsu), summary = stringResource(R.string.home_click_to_learn_kernelsu), endActions = { Icon( imageVector = MiuixIcons.Link, tint = colorScheme.onSurface, contentDescription = null ) }, onClick = { onOpenUrl(url) } ) } } @Composable private fun DonateCard(onOpenUrl: (String) -> Unit) { Card(modifier = Modifier.fillMaxWidth()) { BasicComponent( title = stringResource(R.string.home_support_title), summary = stringResource(R.string.home_support_content), endActions = { Icon( imageVector = MiuixIcons.Link, tint = colorScheme.onSurface, contentDescription = null ) }, onClick = { onOpenUrl("https://patreon.com/weishu") }, insideMargin = PaddingValues(18.dp) ) } } @Composable private fun InfoCard(systemInfo: SystemInfo) { @Composable fun InfoText( title: String, content: String, bottomPadding: Dp = 24.dp ) { Text( text = title, fontSize = MiuixTheme.textStyles.headline1.fontSize, fontWeight = FontWeight.Medium, color = colorScheme.onSurface ) Text( text = content, fontSize = MiuixTheme.textStyles.body2.fontSize, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(top = 2.dp, bottom = bottomPadding) ) } Card { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { InfoText(title = stringResource(R.string.home_kernel), content = systemInfo.kernelVersion) InfoText(title = stringResource(R.string.home_manager_version), content = systemInfo.managerVersion) InfoText(title = stringResource(R.string.home_fingerprint), content = systemInfo.fingerprint) InfoText( title = stringResource(R.string.home_selinux_status), content = systemInfo.selinuxStatus, bottomPadding = 0.dp ) } } } @Preview(name = "Activated") @Composable private fun StatusCardActivatedPreview() { StatusCard( state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10), actions = HomeActions({}, {}, {}, {}) ) } @Preview(name = "Not Activated") @Composable private fun StatusCardNotActivatedPreview() { StatusCard(state = previewHomeScreenState(ksuVersion = null, lkmMode = null), actions = HomeActions({}, {}, {}, {})) } @Preview(name = "Permissive") @Composable private fun StatusCardPermissivePreview() { StatusCard( state = previewHomeScreenState(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true), actions = HomeActions({}, {}, {}, {}) ) } @Preview(name = "Jailbreak") @Composable private fun StatusCardJailbreakPreview() { StatusCard( state = previewHomeScreenState(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10), actions = HomeActions({}, {}, {}, {}) ) } private val previewSystemInfo = SystemInfo( kernelVersion = "6.1.0-android14-0-g1234567", managerVersion = "1.0.0 (10000)", fingerprint = "google/raven/raven:14/AP1A.240305.019:user/release-keys", selinuxStatus = "Enforcing" ) private val previewUriHandler = object : UriHandler { override fun openUri(uri: String) {} } @Composable private fun HomeScreenPreviewContent( ksuVersion: Int?, lkmMode: Boolean?, isSafeMode: Boolean = false, isLateLoadMode: Boolean = false, isSELinuxPermissive: Boolean = false, superuserCount: Int = 0, moduleCount: Int = 0, selinuxStatus: String = "Enforcing", ) { CompositionLocalProvider(LocalUriHandler provides previewUriHandler) { Column( modifier = Modifier.padding(horizontal = 12.dp, vertical = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp), ) { val actions = HomeActions({}, {}, {}, {}) StatusCard( state = previewHomeScreenState( ksuVersion = ksuVersion, lkmMode = lkmMode, isSafeMode = isSafeMode, isLateLoadMode = isLateLoadMode, isSELinuxPermissive = isSELinuxPermissive, superuserCount = superuserCount, moduleCount = moduleCount, selinuxStatus = selinuxStatus, ), actions = actions ) InfoCard(previewSystemInfo.copy(selinuxStatus = selinuxStatus)) DonateCard(onOpenUrl = {}) LearnMoreCard(onOpenUrl = {}) } } } @Preview(name = "Home Activated", showBackground = true) @Composable private fun HomeScreenActivatedPreview() { HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, superuserCount = 5, moduleCount = 10) } @Preview(name = "Home Not Activated", showBackground = true) @Composable private fun HomeScreenNotActivatedPreview() { HomeScreenPreviewContent(ksuVersion = null, lkmMode = null) } @Preview(name = "Home Permissive", showBackground = true) @Composable private fun HomeScreenPermissivePreview() { HomeScreenPreviewContent(ksuVersion = null, lkmMode = null, isSELinuxPermissive = true, selinuxStatus = "Permissive") } @Preview(name = "Home Jailbreak", showBackground = true) @Composable private fun HomeScreenJailbreakPreview() { HomeScreenPreviewContent(ksuVersion = 12345, lkmMode = true, isLateLoadMode = true, superuserCount = 5, moduleCount = 10) } private fun previewHomeScreenState( ksuVersion: Int?, lkmMode: Boolean?, isSafeMode: Boolean = false, isLateLoadMode: Boolean = false, isSELinuxPermissive: Boolean = false, superuserCount: Int = 0, moduleCount: Int = 0, selinuxStatus: String = "Enforcing", ) = HomeUiState( kernelVersion = KernelVersion(6, 1, 0), ksuVersion = ksuVersion, lkmMode = lkmMode, isManager = true, isManagerPrBuild = false, isKernelPrBuild = false, requiresNewKernel = false, isRootAvailable = ksuVersion != null, isSafeMode = isSafeMode, isLateLoadMode = isLateLoadMode, isSELinuxPermissive = isSELinuxPermissive, checkUpdateEnabled = false, latestVersionInfo = me.weishu.kernelsu.ui.util.module.LatestVersionInfo(), currentManagerVersionCode = 10000, superuserCount = superuserCount, moduleCount = moduleCount, systemInfo = previewSystemInfo.copy(selinuxStatus = selinuxStatus), ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.home import android.content.Intent import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.unit.Dp import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.magica.MagicaService import me.weishu.kernelsu.ui.LocalMainPagerState import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog import me.weishu.kernelsu.ui.navigation3.Navigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.viewmodel.HomeViewModel @Composable fun HomePager( navigator: Navigator, bottomInnerPadding: Dp ) { val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() val mainState = LocalMainPagerState.current val uriHandler = LocalUriHandler.current val context = LocalContext.current val loadingDialog = rememberLoadingDialog() val scope = rememberCoroutineScope() val systemInfo = getSystemInfo() LaunchedEffect(Unit) { viewModel.refresh() viewModel.updateSystemInfo(systemInfo) } val actions = HomeActions( onInstallClick = { navigator.push(Route.Install) }, onSuperuserClick = { mainState.animateToPage(1) }, onModuleClick = { mainState.animateToPage(2) }, onOpenUrl = uriHandler::openUri, onJailbreakClick = { loadingDialog.showLoading() context.startService(Intent(context, MagicaService::class.java)) // Manager will be force-stopped and restarted by late-load on success. // If that doesn't happen within timeout, jailbreak likely failed. scope.launch(Dispatchers.IO) { delay(30_000) withContext(Dispatchers.Main) { loadingDialog.hide() Toast.makeText(context, R.string.jailbreak_timeout, Toast.LENGTH_LONG).show() } } }, ) when (LocalUiMode.current) { UiMode.Miuix -> HomePagerMiuix( state = uiState, actions = actions, bottomInnerPadding = bottomInnerPadding, ) UiMode.Material -> HomePagerMaterial( state = uiState, actions = actions, bottomInnerPadding = bottomInnerPadding, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.home import androidx.compose.runtime.Immutable import me.weishu.kernelsu.KernelVersion import me.weishu.kernelsu.ui.util.module.LatestVersionInfo @Immutable data class HomeUiState( val kernelVersion: KernelVersion, val ksuVersion: Int?, val lkmMode: Boolean?, val isManager: Boolean, val isManagerPrBuild: Boolean, val isKernelPrBuild: Boolean, val requiresNewKernel: Boolean, val isRootAvailable: Boolean, val isSafeMode: Boolean, val isLateLoadMode: Boolean, val isSELinuxPermissive: Boolean, val checkUpdateEnabled: Boolean, val latestVersionInfo: LatestVersionInfo, val currentManagerVersionCode: Long, val superuserCount: Int, val moduleCount: Int, val systemInfo: SystemInfo, ) { val isFullFeatured: Boolean get() = isManager && !requiresNewKernel && isRootAvailable val showGkiWarning: Boolean get() = ksuVersion != null && lkmMode == false val showRequireKernelWarning: Boolean get() = isManager && requiresNewKernel val showRootWarning: Boolean get() = ksuVersion != null && !isRootAvailable val showManagerPrBuildWarning: Boolean get() = isManager && isManagerPrBuild val showKernelPrBuildWarning: Boolean get() = isManager && !isManagerPrBuild && isKernelPrBuild val showVersionMismatchWarning: Boolean get() = ksuVersion != null && ksuVersion.toLong() != currentManagerVersionCode val hasUpdate: Boolean get() = latestVersionInfo.versionCode > currentManagerVersionCode } @Immutable data class HomeActions( val onInstallClick: () -> Unit, val onSuperuserClick: () -> Unit, val onModuleClick: () -> Unit, val onOpenUrl: (String) -> Unit, val onJailbreakClick: () -> Unit = {}, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/home/HomeUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.home import android.content.Context import android.os.Build import android.system.Os import androidx.compose.runtime.Composable import androidx.compose.ui.platform.LocalContext import androidx.core.content.pm.PackageInfoCompat import me.weishu.kernelsu.ui.util.getSELinuxStatus data class ManagerVersion( val versionName: String, val versionCode: Long ) data class SystemInfo( val kernelVersion: String, val managerVersion: String, val fingerprint: String, val selinuxStatus: String ) @Composable fun getSystemInfo(): SystemInfo { val context = LocalContext.current val uname = Os.uname() val managerVersion = getManagerVersion(context) return SystemInfo( kernelVersion = uname.release, managerVersion = "${managerVersion.versionName} (${managerVersion.versionCode})", fingerprint = Build.FINGERPRINT, selinuxStatus = getSELinuxStatus() ) } fun getManagerVersion(context: Context): ManagerVersion { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)!! val versionCode = PackageInfoCompat.getLongVersionCode(packageInfo) return ManagerVersion( versionName = packageInfo.versionName!!, versionCode = versionCode ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.install import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateFloatAsState 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.Column import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.DriveFileMove import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.ExpandMore import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.material.SegmentedCheckboxItem import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.material.SegmentedRadioItem import me.weishu.kernelsu.ui.util.LkmSelection /** * @author weishu * @date 2024/3/12. */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun InstallScreenMaterial( uiState: InstallUiState, actions: InstallScreenActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } Scaffold( topBar = { TopBar( onBack = actions.onBack, scrollBehavior = scrollBehavior, ) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .fillMaxHeight() .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) ) { SelectInstallMethod( state = uiState, onSelected = actions.onSelectMethod, onSelectBootImage = actions.onSelectBootImage, ) SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = buildList { if (uiState.displayPartitions.isNotEmpty()) add { SegmentedDropdownItem( enabled = uiState.canSelectPartition, items = uiState.displayPartitions, selectedIndex = uiState.partitionSelectionIndex, title = "${stringResource(R.string.install_select_partition)} (${uiState.slotSuffix})", onItemSelected = actions.onSelectPartition, icon = Icons.Filled.Edit ) } add { SegmentedListItem( leadingContent = { Icon( Icons.AutoMirrored.Filled.DriveFileMove, null ) }, headlineContent = { Text(stringResource(R.string.install_upload_lkm_file)) }, supportingContent = { (uiState.lkmSelection as? LkmSelection.LkmUri)?.let { Text( stringResource( R.string.selected_lkm, it.uri.lastPathSegment ?: "(file)" ) ) } }, trailingContent = { if (uiState.lkmSelection is LkmSelection.LkmUri) { IconButton(onClick = actions.onClearLkm) { Icon( Icons.Filled.Close, contentDescription = stringResource(android.R.string.cancel) ) } } else { Icon(Icons.AutoMirrored.Filled.KeyboardArrowRight, null) } }, onClick = actions.onUploadLkm ) } } ) SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), visibleLen = if (uiState.advancedOptionsShown) 0 else 1, content = buildList { val rotationState by animateFloatAsState( targetValue = if (uiState.advancedOptionsShown) 180f else 0f, label = "RotationAnimation" ) add { SegmentedListItem( headlineContent = { Text(stringResource(R.string.advanced_options)) }, trailingContent = { Icon( imageVector = Icons.Filled.ExpandMore, contentDescription = stringResource(R.string.expand), modifier = Modifier.graphicsLayer { rotationZ = rotationState } ) }, onClick = actions.onAdvancedOptionsClicked ) } add { AnimatedVisibility( uiState.advancedOptionsShown, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { SegmentedCheckboxItem( title = stringResource(id = R.string.allow_shell), summary = stringResource(id = R.string.allow_shell_summary), checked = uiState.allowShell, onCheckedChange = actions.onSelectAllowShell, ) } } add { AnimatedVisibility( uiState.advancedOptionsShown, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { SegmentedCheckboxItem( title = stringResource(id = R.string.enable_adb), summary = stringResource(id = R.string.enable_adb_summary), checked = uiState.enableAdb, onCheckedChange = actions.onSelectEnableAdb, ) } } } ) Button( modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp, vertical = 4.dp), enabled = uiState.installMethod != null, onClick = actions.onNext ) { Text(stringResource(R.string.install_next)) } } } } @Composable private fun SelectInstallMethod( state: InstallUiState, onSelected: (InstallMethod) -> Unit, onSelectBootImage: () -> Unit, ) { val confirmDialog = rememberConfirmDialog( onConfirm = { onSelected(InstallMethod.DirectInstallToInactiveSlot) }, onDismiss = null ) val dialogTitle = stringResource(android.R.string.dialog_alert_title) val dialogContent = stringResource(R.string.install_inactive_slot_warning) val onClick = { option: InstallMethod -> when (option) { is InstallMethod.SelectFile -> onSelectBootImage() is InstallMethod.DirectInstall -> onSelected(option) is InstallMethod.DirectInstallToInactiveSlot -> confirmDialog.showConfirm(dialogTitle, dialogContent) } } key(state.installMethodOptions.size) { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = state.installMethodOptions.map { option -> { SegmentedRadioItem( title = stringResource(option.label), summary = option.summary, selected = option.javaClass == state.installMethod?.javaClass, onClick = { onClick(option) } ) } } ) } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( onBack: () -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null ) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.install)) }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.install 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.LocalIndication import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.selection.toggleable import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.LkmSelection import me.weishu.kernelsu.ui.util.defaultHazeEffect import top.yukonga.miuix.kmp.basic.BasicComponent 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.IconButton import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.extra.SuperCheckbox import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.basic.ArrowRight import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Close import top.yukonga.miuix.kmp.icon.extended.ConvertFile import top.yukonga.miuix.kmp.icon.extended.ExpandLess import top.yukonga.miuix.kmp.icon.extended.ExpandMore import top.yukonga.miuix.kmp.icon.extended.MoveFile import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2024/3/12. */ @Composable internal fun InstallScreenMiuix( uiState: InstallUiState, actions: InstallScreenActions, ) { val enableBlur = LocalEnableBlur.current val scrollBehavior = MiuixScrollBehavior() val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopBar( onBack = actions.onBack, scrollBehavior = scrollBehavior, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding(top = 12.dp) .padding(horizontal = 16.dp), contentPadding = innerPadding, overscrollEffect = null, ) { item { Card( modifier = Modifier.fillMaxWidth(), ) { SelectInstallMethod( state = uiState, onSelected = actions.onSelectMethod, onSelectBootImage = actions.onSelectBootImage, ) } AnimatedVisibility( visible = uiState.canSelectPartition, enter = expandVertically(), exit = shrinkVertically() ) { Card( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp), ) { SuperDropdown( items = uiState.displayPartitions, selectedIndex = uiState.partitionSelectionIndex, title = "${stringResource(R.string.install_select_partition)} (${uiState.slotSuffix})", onSelectedIndexChange = actions.onSelectPartition, startAction = { Icon( MiuixIcons.ConvertFile, tint = colorScheme.onSurface, modifier = Modifier.padding(end = 12.dp), contentDescription = null ) } ) } } Card( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp), ) { BasicComponent( title = stringResource(id = R.string.install_upload_lkm_file), summary = (uiState.lkmSelection as? LkmSelection.LkmUri)?.let { stringResource(id = R.string.selected_lkm, it.uri.lastPathSegment ?: "(file)") }, onClick = actions.onUploadLkm, startAction = { Icon( MiuixIcons.MoveFile, tint = colorScheme.onSurface, modifier = Modifier.padding(end = 12.dp), contentDescription = null ) }, endActions = { if (uiState.lkmSelection is LkmSelection.LkmUri) { IconButton(onClick = actions.onClearLkm) { Icon( MiuixIcons.Close, modifier = Modifier.size(16.dp), contentDescription = stringResource(android.R.string.cancel), tint = colorScheme.onSurfaceVariantActions ) } } else { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier .size(width = 10.dp, height = 16.dp) .graphicsLayer { scaleX = if (layoutDirection == LayoutDirection.Rtl) -1f else 1f } .align(Alignment.CenterVertically), imageVector = MiuixIcons.Basic.ArrowRight, contentDescription = null, tint = colorScheme.onSurfaceVariantActions, ) } } ) } Card( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp), ) { BasicComponent( title = stringResource(id = R.string.advanced_options), onClick = actions.onAdvancedOptionsClicked, endActions = { Icon( if (uiState.advancedOptionsShown) MiuixIcons.ExpandLess else MiuixIcons.ExpandMore, modifier = Modifier.size(16.dp), tint = colorScheme.onSurfaceVariantActions, contentDescription = stringResource(R.string.expand), ) } ) AnimatedVisibility( visible = uiState.advancedOptionsShown, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { SuperCheckbox( title = stringResource(id = R.string.allow_shell), checked = uiState.allowShell, summary = stringResource(id = R.string.allow_shell_summary), onCheckedChange = actions.onSelectAllowShell ) SuperCheckbox( title = stringResource(id = R.string.enable_adb), checked = uiState.enableAdb, summary = stringResource(id = R.string.enable_adb_summary), onCheckedChange = actions.onSelectEnableAdb ) } } } TextButton( modifier = Modifier .fillMaxWidth() .padding(top = 12.dp), text = stringResource(id = R.string.install_next), enabled = uiState.installMethod != null, colors = ButtonDefaults.textButtonColorsPrimary(), onClick = actions.onNext ) Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } @Composable private fun SelectInstallMethod( state: InstallUiState, onSelected: (InstallMethod) -> Unit, onSelectBootImage: () -> Unit, ) { val confirmDialog = rememberConfirmDialog( onConfirm = { onSelected(InstallMethod.DirectInstallToInactiveSlot) } ) val dialogTitle = stringResource(id = android.R.string.dialog_alert_title) val dialogContent = stringResource(id = R.string.install_inactive_slot_warning) val onClick = { option: InstallMethod -> when (option) { is InstallMethod.SelectFile -> onSelectBootImage() is InstallMethod.DirectInstall -> onSelected(option) is InstallMethod.DirectInstallToInactiveSlot -> confirmDialog.showConfirm(dialogTitle, dialogContent) } } Column { state.installMethodOptions.forEach { option -> val interactionSource = remember { MutableInteractionSource() } Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier .fillMaxWidth() .toggleable( value = option.javaClass == state.installMethod?.javaClass, onValueChange = { onClick(option) }, role = Role.RadioButton, indication = LocalIndication.current, interactionSource = interactionSource ) ) { SuperCheckbox( title = stringResource(id = option.label), summary = option.summary, checked = option.javaClass == state.installMethod?.javaClass, onCheckedChange = { onClick(option) }, ) } } } } @Composable private fun TopBar( onBack: () -> Unit = {}, scrollBehavior: ScrollBehavior, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.install), navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, tint = colorScheme.onSurface, contentDescription = null, ) } }, scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.install import android.app.Activity import android.content.Intent import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.produceState import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed import me.weishu.kernelsu.R import me.weishu.kernelsu.getKernelVersion import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.component.choosekmidialog.ChooseKmiDialog import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.util.LkmSelection import me.weishu.kernelsu.ui.util.getAvailablePartitions import me.weishu.kernelsu.ui.util.getCurrentKmi import me.weishu.kernelsu.ui.util.getDefaultPartition import me.weishu.kernelsu.ui.util.getSlotSuffix import me.weishu.kernelsu.ui.util.isAbDevice import me.weishu.kernelsu.ui.util.rootAvailable @Composable fun InstallScreen() { val navigator = LocalNavigator.current val context = LocalContext.current var installMethod by rememberSaveable { mutableStateOf(null) } var lkmSelection by rememberSaveable { mutableStateOf(LkmSelection.KmiNone) } var partitionSelectionIndex by rememberSaveable { mutableIntStateOf(0) } var hasCustomSelected by rememberSaveable { mutableStateOf(false) } val showChooseKmiDialog = rememberSaveable { mutableStateOf(false) } var advancedOptionsShown by rememberSaveable { mutableStateOf(false) } var allowShell by rememberSaveable { mutableStateOf(false) } var enableAdb by rememberSaveable { mutableStateOf(false) } val currentKmi by produceState(initialValue = "") { value = getCurrentKmi() } val partitions by produceState(initialValue = emptyList()) { value = getAvailablePartitions() } val defaultPartition by produceState(initialValue = "") { value = getDefaultPartition() } val rootAvailable by produceState(initialValue = false) { value = rootAvailable() } val isAbDevice by produceState(initialValue = false) { value = isAbDevice() } val isGkiDevice by produceState(initialValue = false) { value = getKernelVersion().isGKI() } val selectFileTip = stringResource(id = R.string.select_file_tip, defaultPartition) val selectFileTipNoGki = stringResource(id = R.string.select_file_tip_nogki) val installMethodOptions = remember(rootAvailable, isAbDevice, isGkiDevice, selectFileTip, selectFileTipNoGki) { buildList { add(InstallMethod.SelectFile(summary = if (isGkiDevice) selectFileTip else selectFileTipNoGki)) if (rootAvailable && isGkiDevice) { add(InstallMethod.DirectInstall) if (isAbDevice) add(InstallMethod.DirectInstallToInactiveSlot) } } } val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot val slotSuffix by produceState(initialValue = "", isOta) { value = getSlotSuffix(isOta) } val defaultIndex = remember(partitions, defaultPartition) { partitions.indexOf(defaultPartition).coerceAtLeast(0) } LaunchedEffect(partitions, defaultIndex, hasCustomSelected) { if (partitions.isEmpty()) return@LaunchedEffect if (!hasCustomSelected) { partitionSelectionIndex = defaultIndex.coerceIn(0, partitions.lastIndex) } else if (partitionSelectionIndex > partitions.lastIndex) { partitionSelectionIndex = partitions.lastIndex } } val displayPartitions = remember(partitions, defaultPartition) { partitions.map { name -> if (defaultPartition == name) "$name (default)" else name } } val onInstall = { installMethod?.let { method -> navigator.push( Route.Flash( FlashIt.FlashBoot( boot = if (method is InstallMethod.SelectFile) method.uri else null, lkm = lkmSelection, ota = method is InstallMethod.DirectInstallToInactiveSlot, partition = partitions.getOrNull(partitionSelectionIndex), allowShell = allowShell, enableAdb = enableAdb, ) ) ) } } ChooseKmiDialog( show = showChooseKmiDialog.value, onDismissRequest = { showChooseKmiDialog.value = false }, onSelected = { kmi -> kmi?.let { lkmSelection = LkmSelection.KmiString(it) onInstall() } } ) val selectLkmLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { it.data?.data?.let { uri -> if (isKoFile(context, uri)) { lkmSelection = LkmSelection.LkmUri(uri) } else { lkmSelection = LkmSelection.KmiNone Toast.makeText(context, R.string.install_only_support_ko_file, Toast.LENGTH_SHORT).show() } } } } val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { if (it.resultCode == Activity.RESULT_OK) { it.data?.data?.let { uri -> installMethod = InstallMethod.SelectFile(uri, summary = if (isGkiDevice) selectFileTip else selectFileTipNoGki) } } } val state = InstallUiState( installMethod = installMethod, lkmSelection = lkmSelection, partitionSelectionIndex = partitionSelectionIndex, displayPartitions = displayPartitions, currentKmi = currentKmi, slotSuffix = slotSuffix, installMethodOptions = installMethodOptions, canSelectPartition = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, advancedOptionsShown = advancedOptionsShown, allowShell = allowShell, enableAdb = enableAdb, ) val actions = InstallScreenActions( onBack = dropUnlessResumed { navigator.pop() }, onSelectMethod = { method -> installMethod = method }, onSelectBootImage = { selectImageLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { type = "application/octet-stream" }) }, onUploadLkm = { selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { type = "application/octet-stream" }) }, onClearLkm = { lkmSelection = LkmSelection.KmiNone }, onSelectPartition = { index -> hasCustomSelected = true partitionSelectionIndex = index }, onNext = { val isLkmSelected = lkmSelection != LkmSelection.KmiNone val isKmiUnknown = currentKmi.isBlank() val isSelectFileMode = installMethod is InstallMethod.SelectFile if (!isLkmSelected && (isKmiUnknown || isSelectFileMode)) { showChooseKmiDialog.value = true } else { onInstall() } }, onAdvancedOptionsClicked = { advancedOptionsShown = !advancedOptionsShown }, onSelectAllowShell = { allowShell = it }, onSelectEnableAdb = { enableAdb = it }, ) when (LocalUiMode.current) { UiMode.Miuix -> InstallScreenMiuix(state, actions) UiMode.Material -> InstallScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.install import androidx.compose.runtime.Immutable import me.weishu.kernelsu.ui.util.LkmSelection @Immutable internal data class InstallUiState( val installMethod: InstallMethod?, val lkmSelection: LkmSelection, val partitionSelectionIndex: Int, val displayPartitions: List, val currentKmi: String, val slotSuffix: String, val installMethodOptions: List, val canSelectPartition: Boolean, val advancedOptionsShown: Boolean, val allowShell: Boolean, val enableAdb: Boolean, ) @Immutable internal data class InstallScreenActions( val onBack: () -> Unit, val onSelectMethod: (InstallMethod) -> Unit, val onSelectBootImage: () -> Unit, val onUploadLkm: () -> Unit, val onClearLkm: () -> Unit, val onSelectPartition: (Int) -> Unit, val onNext: () -> Unit, val onAdvancedOptionsClicked: () -> Unit, val onSelectAllowShell: (Boolean) -> Unit, val onSelectEnableAdb: (Boolean) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/install/InstallUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.install import android.content.Context import android.net.Uri import android.os.Parcelable import android.provider.OpenableColumns import androidx.annotation.StringRes import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.R @Parcelize internal sealed class InstallMethod : Parcelable { data class SelectFile( val uri: Uri? = null, @get:StringRes override val label: Int = R.string.select_file, override val summary: String? ) : InstallMethod() data object DirectInstall : InstallMethod() { override val label: Int get() = R.string.direct_install } data object DirectInstallToInactiveSlot : InstallMethod() { override val label: Int get() = R.string.install_inactive_slot } abstract val label: Int @IgnoredOnParcel open val summary: String? = null } fun isKoFile(context: Context, uri: Uri): Boolean { val seg = uri.lastPathSegment ?: "" if (seg.endsWith(".ko", ignoreCase = true)) return true return try { context.contentResolver.query( uri, arrayOf(OpenableColumns.DISPLAY_NAME), null, null, null )?.use { cursor -> val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) if (idx != -1 && cursor.moveToFirst()) { val name = cursor.getString(idx) name?.endsWith(".ko", ignoreCase = true) == true } else { false } } ?: false } catch (_: Throwable) { false } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.LocalIndication import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxSize 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.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Cloud import androidx.compose.material.icons.outlined.Code import androidx.compose.material.icons.outlined.Delete import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.PlayArrow import androidx.compose.material.icons.outlined.Refresh import androidx.compose.material3.Button import androidx.compose.material3.ButtonColors import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.ProvideTextStyle import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarDuration import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarResult import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.FixedScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.semantics.role import androidx.compose.ui.semantics.semantics import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog import me.weishu.kernelsu.ui.component.material.ExpressiveSwitch import me.weishu.kernelsu.ui.component.material.SearchAppBar import me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopup import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.screen.home.TonalCard import me.weishu.kernelsu.ui.util.LocalSnackbarHost import me.weishu.kernelsu.ui.util.reboot @SuppressLint("StringFormatInvalid") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ModulePagerMaterial( uiState: ModuleUiState, confirmDialogState: ModuleConfirmDialogState?, effect: ModuleEffect?, actions: ModuleActions, bottomInnerPadding: Dp, ) { val snackBarHost = LocalSnackbarHost.current val context = LocalContext.current val resource = LocalResources.current val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val pullToRefreshState = rememberPullToRefreshState() val scaleFraction = { if (uiState.isRefreshing) 1f else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f) } val listState = rememberLazyListState() val searchListState = rememberLazyListState() val threshold = with(LocalDensity.current) { 100.dp.toPx() } val fabExpanded by remember { var lastIndex = 0 var lastOffset = 0 var scrollDelta = 0f var expanded = true derivedStateOf { val currentIndex = listState.firstVisibleItemIndex val currentOffset = listState.firstVisibleItemScrollOffset val delta = if (currentIndex == lastIndex) { (currentOffset - lastOffset).toFloat() } else if (currentIndex > lastIndex) { 100f } else { -100f } scrollDelta = (scrollDelta + delta).coerceIn(-threshold, threshold) lastIndex = currentIndex lastOffset = currentOffset if (currentIndex == 0) { expanded = true scrollDelta = 0f } else if (expanded && scrollDelta >= threshold) { expanded = false scrollDelta = 0f } else if (!expanded && scrollDelta <= -threshold) { expanded = true scrollDelta = 0f } expanded } } val shortcutState = rememberModuleShortcutState(context) val showShortcutDialog = remember { mutableStateOf(false) } val confirmDialog = rememberConfirmDialog( onConfirm = { when (val request = confirmDialogState?.request) { is ModuleConfirmRequest.Uninstall -> actions.onUninstallModule(request.module) is ModuleConfirmRequest.Update -> actions.onConfirmUpdate(request) null -> Unit } }, onDismiss = actions.onDismissConfirmRequest, ) fun openShortcutDialogForType(type: ShortcutType) { shortcutState.selectType(type) showShortcutDialog.value = true } val pickShortcutIconLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri -> shortcutState.updateIconUri(uri?.toString()) } fun onModuleAddShortcut(module: Module, type: ShortcutType) { shortcutState.bindModule(module) openShortcutDialogForType(type) } LaunchedEffect(confirmDialogState) { confirmDialogState?.let { confirmDialog.showConfirm( title = it.title, content = it.content, markdown = it.markdown, html = it.html, confirm = it.confirm, dismiss = it.dismiss, ) } } LaunchedEffect(effect) { when (effect) { is ModuleEffect.Toast -> { Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() actions.onConsumeEffect() } is ModuleEffect.SnackBar -> { snackBarHost.currentSnackbarData?.dismiss() val result = snackBarHost.showSnackbar( message = effect.message, actionLabel = resource.getString(R.string.reboot), duration = SnackbarDuration.Long ) if (result == SnackbarResult.ActionPerformed) { reboot() } actions.onConsumeEffect() } null -> Unit } } Scaffold( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) .pullToRefresh( state = pullToRefreshState, isRefreshing = uiState.isRefreshing, onRefresh = { actions.onRefresh() }, ), topBar = { SearchAppBar( title = { Text(stringResource(R.string.module)) }, searchText = uiState.searchStatus.searchText, onSearchTextChange = actions.onSearchTextChange, onClearClick = actions.onClearSearch, navigationIcon = { IconButton( onClick = actions.onOpenRepo ) { Icon( imageVector = Icons.Outlined.Cloud, contentDescription = stringResource(id = R.string.module_repos) ) } }, actions = { RebootListPopup() var showDropdown by remember { mutableStateOf(false) } IconButton( onClick = { showDropdown = true } ) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings) ) DropdownMenu( expanded = showDropdown, onDismissRequest = { showDropdown = false } ) { DropdownMenuItem( text = { Text(stringResource(R.string.module_sort_action_first)) }, trailingIcon = { Checkbox(uiState.sortActionFirst, null) }, onClick = { actions.onToggleSortActionFirst() } ) DropdownMenuItem( text = { Text(stringResource(R.string.module_sort_enabled_first)) }, trailingIcon = { Checkbox(uiState.sortEnabledFirst, null) }, onClick = { actions.onToggleSortEnabledFirst() } ) } } }, scrollBehavior = scrollBehavior, searchContent = { bottomPadding, closeSearch -> LaunchedEffect(uiState.searchStatus.searchText) { searchListState.scrollToItem(0) } ModuleList( bottomInnerPadding = bottomPadding, modifier = Modifier.fillMaxSize(), listState = searchListState, displayModules = uiState.searchResults, updateInfoMap = uiState.updateInfo, actions = actions, onClickModule = { module -> if (module.hasWebUi) { actions.onOpenWebUi(module) closeSearch() } }, onModuleAddShortcut = { module, type -> onModuleAddShortcut(module, type) }, closeSearch = closeSearch, ) } ) }, floatingActionButton = { if (uiState.installButtonVisible) { val moduleInstall = stringResource(id = R.string.module_install) val selectZipLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { activityResult -> if (activityResult.resultCode != RESULT_OK) { return@rememberLauncherForActivityResult } val data = activityResult.data ?: return@rememberLauncherForActivityResult val clipData = data.clipData val uris = mutableListOf() if (clipData != null) { for (i in 0 until clipData.itemCount) { clipData.getItemAt(i)?.uri?.let { uris.add(it) } } } else { data.data?.let { uris.add(it) } } actions.onOpenFlash(uris) } ExtendedFloatingActionButton( modifier = Modifier.padding(bottom = bottomInnerPadding), expanded = fabExpanded, onClick = { // Select the zip files to install val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "application/zip" putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } selectZipLauncher.launch(intent) }, icon = { Icon(Icons.Filled.Add, moduleInstall) }, text = { Text(text = moduleInstall) }, ) } }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), snackbarHost = { SnackbarHost(hostState = snackBarHost) } ) { innerPadding -> if (uiState.magiskInstalled) { Box( modifier = Modifier .fillMaxSize() .padding(24.dp), contentAlignment = Alignment.Center ) { Text( stringResource(R.string.module_magisk_conflict), textAlign = TextAlign.Center, ) } return@Scaffold } Box(modifier = Modifier.padding(innerPadding)) { if (uiState.moduleList.isEmpty()) { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center, ) { Text( stringResource(R.string.module_empty), textAlign = TextAlign.Center, ) } } else { ModuleList( bottomInnerPadding = bottomInnerPadding, modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), listState = listState, displayModules = uiState.moduleList, updateInfoMap = uiState.updateInfo, actions = actions, onClickModule = { module -> if (module.hasWebUi) { actions.onOpenWebUi(module) } }, onModuleAddShortcut = { module, type -> onModuleAddShortcut(module, type) }, ) } Box( modifier = Modifier .align(Alignment.TopCenter) .graphicsLayer { scaleX = scaleFraction() scaleY = scaleFraction() } ) { PullToRefreshDefaults.LoadingIndicator( state = pullToRefreshState, isRefreshing = uiState.isRefreshing, ) } } } ModuleShortcutSheet( show = showShortcutDialog.value, shortcutState = shortcutState, onDismiss = { showShortcutDialog.value = false }, onPickShortcutIcon = { pickShortcutIconLauncher.launch("image/*") }, onDeleteShortcut = { shortcutState.deleteShortcut(context) showShortcutDialog.value = false }, onConfirmShortcut = { shortcutState.createShortcut(context) showShortcutDialog.value = false }, ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ModuleList( bottomInnerPadding: Dp, modifier: Modifier = Modifier, listState: LazyListState = rememberLazyListState(), displayModules: List, updateInfoMap: Map, actions: ModuleActions, onClickModule: (Module) -> Unit, onModuleAddShortcut: (Module, ShortcutType) -> Unit, closeSearch: () -> Unit? = {}, ) { val loadingDialog = rememberLoadingDialog() LazyColumn( state = listState, modifier = modifier, verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues( start = 16.dp, top = 8.dp, end = 16.dp, bottom = 16.dp + bottomInnerPadding + 56.dp + 16.dp ), ) { items(displayModules, key = { it.id }) { module -> val scope = rememberCoroutineScope() val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleUpdateInfo.Empty ModuleItem( module = module, updateUrl = moduleUpdateInfo.downloadUrl, onUninstallClicked = { if (module.remove) { actions.onUndoUninstallModule(module) } else { actions.onRequestUninstallConfirmation(module) } }, onCheckChanged = { actions.onToggleModule(module) }, onUpdate = { scope.launch { loadingDialog.withLoading { actions.onRequestUpdateConfirmation(module, moduleUpdateInfo) } } }, onAddShortcut = { type -> onModuleAddShortcut(module, type) }, onClick = { onClickModule(module) }, onExecuteAction = { actions.onExecuteModuleAction(module) }, closeSearch = { closeSearch() } ) } } } @OptIn(ExperimentalMaterial3Api::class) @Composable private fun ModuleShortcutSheet( show: Boolean, shortcutState: ModuleShortcutState, onDismiss: () -> Unit, onPickShortcutIcon: () -> Unit, onDeleteShortcut: () -> Unit, onConfirmShortcut: () -> Unit, ) { if (!show) return ModalBottomSheet( onDismissRequest = onDismiss, sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ) { Column( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(24.dp) ) { Text( text = stringResource(R.string.module_shortcut_title), style = MaterialTheme.typography.titleLarge ) Box( contentAlignment = Alignment.Center, modifier = Modifier .padding(vertical = 16.dp) .size(100.dp) .clip(RoundedCornerShape(25.dp)) ) { val preview = shortcutState.previewIcon if (preview != null) { Image( bitmap = preview, modifier = Modifier.size(100.dp), contentDescription = null, ) } else { Box( modifier = Modifier .size(100.dp) .background(Color.White) ) Image( painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = null, contentScale = FixedScale(1.5f) ) } } Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically ) { TextButton(onClick = onPickShortcutIcon) { Text(stringResource(id = R.string.module_shortcut_icon_pick)) } AnimatedVisibility( visible = shortcutState.iconUri != shortcutState.defaultShortcutIconUri, enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }), exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }), ) { IconButton( onClick = shortcutState::resetIconToDefault, modifier = Modifier.padding(start = 12.dp) ) { Icon( imageVector = Icons.Outlined.Refresh, contentDescription = null, modifier = Modifier.size(24.dp), ) } } } OutlinedTextField( value = shortcutState.name, onValueChange = shortcutState::updateName, label = { Text(stringResource(id = R.string.module_shortcut_name_label)) }, modifier = Modifier.fillMaxWidth() ) if (shortcutState.hasExistingShortcut) { TextButton( onClick = onDeleteShortcut, modifier = Modifier.fillMaxWidth(), colors = ButtonDefaults.textButtonColors(contentColor = MaterialTheme.colorScheme.error) ) { Text(stringResource(id = R.string.module_shortcut_delete)) } } Row( horizontalArrangement = Arrangement.spacedBy(12.dp), modifier = Modifier.fillMaxWidth() ) { OutlinedButton( onClick = onDismiss, modifier = Modifier.weight(1f), ) { Text(stringResource(id = android.R.string.cancel)) } Button( onClick = onConfirmShortcut, modifier = Modifier.weight(1f), ) { Text( if (shortcutState.hasExistingShortcut) { stringResource(id = R.string.module_update) } else { stringResource(id = android.R.string.ok) } ) } } Spacer(modifier = Modifier.height(32.dp)) } } } @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ModuleItem( module: Module, updateUrl: String, onUninstallClicked: () -> Unit, onCheckChanged: (Boolean) -> Unit, onUpdate: () -> Unit, onAddShortcut: (ShortcutType) -> Unit, onClick: () -> Unit, onExecuteAction: () -> Unit, closeSearch: () -> Unit ) { TonalCard( modifier = Modifier.fillMaxWidth() ) { val textDecoration = if (!module.remove) null else TextDecoration.LineThrough val interactionSource = remember { MutableInteractionSource() } val indication = LocalIndication.current var expanded by rememberSaveable(module.id) { mutableStateOf(false) } var isOverflowing by remember { mutableStateOf(false) } Column( modifier = Modifier .run { if (module.hasWebUi) { toggleable( value = module.enabled, enabled = !module.remove && module.enabled, interactionSource = interactionSource, role = Role.Button, indication = indication, onValueChange = { onClick() } ) } else { this } } .padding(22.dp, 18.dp, 22.dp, 12.dp) ) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, ) { val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) Column( modifier = Modifier.fillMaxWidth(0.8f) ) { Text( text = module.name, fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.titleMedium, lineHeight = MaterialTheme.typography.bodySmall.lineHeight, textDecoration = textDecoration, ) Text( text = "$moduleVersion: ${module.version}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, textDecoration = textDecoration ) Text( text = "$moduleAuthor: ${module.author}", color = MaterialTheme.colorScheme.onSurfaceVariant, style = MaterialTheme.typography.bodySmall, textDecoration = textDecoration ) } Spacer(modifier = Modifier.weight(1f)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End, ) { ExpressiveSwitch( enabled = !module.update, checked = module.enabled, onCheckedChange = onCheckChanged, interactionSource = if (!module.hasWebUi) interactionSource else remember { MutableInteractionSource() } ) } } Spacer(modifier = Modifier.height(8.dp)) Text( modifier = Modifier .animateContentSize( animationSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing ) ) .then( if (isOverflowing || expanded) { Modifier.clickable( interactionSource = remember { MutableInteractionSource() }, indication = null ) { expanded = !expanded } } else { Modifier } ), text = module.description, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium, overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis, maxLines = if (expanded) Int.MAX_VALUE else 4, textDecoration = textDecoration, onTextLayout = { textLayoutResult -> isOverflowing = if (expanded) { textLayoutResult.lineCount > 4 } else { textLayoutResult.hasVisualOverflow } } ) Row(modifier = Modifier.padding(vertical = 4.dp)) { if (module.metamodule) { StatusTag( "META", modifier = Modifier.padding(bottom = 4.dp), contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } } HorizontalDivider(thickness = Dp.Hairline) Spacer(modifier = Modifier.height(4.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { val hasUpdate by remember(updateUrl) { derivedStateOf { updateUrl.isNotEmpty() } } val actionButtonsEnabled = !module.remove && module.enabled AnimatedVisibility( visible = actionButtonsEnabled, enter = fadeIn(), exit = fadeOut() ) { Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) { if (module.hasActionScript) { CombinedClickableButton( onClick = { onExecuteAction() closeSearch() }, onLongClick = { onAddShortcut(ShortcutType.Action) }, modifier = Modifier.defaultMinSize(52.dp, 32.dp), shape = ButtonDefaults.filledTonalShape, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.PlayArrow, contentDescription = null ) if (!module.hasWebUi && !hasUpdate) { Text( modifier = Modifier.padding(start = 7.dp), text = stringResource(R.string.action), fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize ) } } } if (module.hasWebUi) { CombinedClickableButton( onClick = { onClick() closeSearch() }, onLongClick = { onAddShortcut(ShortcutType.WebUI) }, modifier = Modifier.defaultMinSize(52.dp, 32.dp), shape = ButtonDefaults.filledTonalShape, colors = ButtonDefaults.buttonColors( containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Code, contentDescription = null ) if (!module.hasActionScript && !hasUpdate) { Text( modifier = Modifier.padding(start = 7.dp), fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize, text = stringResource(R.string.open) ) } } } } } Spacer(modifier = Modifier.weight(1f, true)) AnimatedVisibility( visible = hasUpdate, enter = fadeIn(), exit = fadeOut() ) { Row { Button( modifier = Modifier.defaultMinSize(52.dp, 32.dp), enabled = !module.remove, onClick = onUpdate, shape = ButtonDefaults.textShape, contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Download, contentDescription = null ) if (!module.hasActionScript || !module.hasWebUi) { Text( modifier = Modifier.padding(start = 7.dp), fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize, text = stringResource(R.string.module_update) ) } } Spacer(Modifier.width(12.dp)) } } FilledTonalButton( modifier = Modifier.defaultMinSize(52.dp, 32.dp), onClick = onUninstallClicked, contentPadding = ButtonDefaults.TextButtonContentPadding ) { if (!module.remove) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Delete, contentDescription = null, ) } else { Icon( modifier = Modifier .size(20.dp) .rotate(180f), imageVector = Icons.Outlined.Refresh, contentDescription = null, ) } if (!module.hasActionScript && !module.hasWebUi || !hasUpdate) { Text( modifier = Modifier.padding(start = 7.dp), fontFamily = MaterialTheme.typography.labelMedium.fontFamily, fontSize = MaterialTheme.typography.labelMedium.fontSize, text = stringResource(if (module.remove) R.string.undo else R.string.uninstall) ) } } } } } } @Composable fun CombinedClickableButton( onClick: () -> Unit, onLongClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, shape: Shape = ButtonDefaults.shape, colors: ButtonColors = ButtonDefaults.buttonColors(), border: BorderStroke? = null, contentPadding: PaddingValues = ButtonDefaults.ContentPadding, interactionSource: MutableInteractionSource? = null, content: @Composable RowScope.() -> Unit, ) { val interactionSource = interactionSource ?: remember { MutableInteractionSource() } Surface( modifier = modifier .semantics { role = Role.Button } .clip(shape) .combinedClickable( interactionSource = interactionSource, indication = LocalIndication.current, enabled = enabled, onClick = onClick, onLongClick = onLongClick ), shape = shape, color = if (enabled) colors.containerColor else colors.disabledContainerColor, contentColor = if (enabled) colors.contentColor else colors.disabledContentColor, border = border, ) { ProvideTextStyle(MaterialTheme.typography.labelLarge) { Row( Modifier .defaultMinSize( minWidth = ButtonDefaults.MinWidth, minHeight = ButtonDefaults.MinHeight, ) .padding(contentPadding), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, content = content, ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.animation.expandHorizontally import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkHorizontally import androidx.compose.animation.shrinkVertically import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Code import androidx.compose.material.icons.rounded.PlayArrow import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberUpdatedState import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.layout.FixedScale import androidx.compose.ui.layout.SubcomposeLayout import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Constraints import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.capsule.ContinuousRoundedRectangle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.ui.component.ListPopupDefaults import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog import me.weishu.kernelsu.ui.component.miuix.SearchBox import me.weishu.kernelsu.ui.component.miuix.SearchPager import me.weishu.kernelsu.ui.component.rebootlistpopup.RebootListPopupMiuix import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.isInDarkTheme import me.weishu.kernelsu.ui.util.getFileName import top.yukonga.miuix.kmp.basic.ButtonDefaults import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.FloatingActionButton import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.PullToRefresh import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Switch 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.rememberPullToRefreshState import top.yukonga.miuix.kmp.extra.SuperDialog import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Delete import top.yukonga.miuix.kmp.icon.extended.Download import top.yukonga.miuix.kmp.icon.extended.MoreCircle import top.yukonga.miuix.kmp.icon.extended.Undo import top.yukonga.miuix.kmp.icon.extended.UploadCloud import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic @SuppressLint("StringFormatInvalid", "LocalContextGetResourceValueCall") @Composable fun ModulePagerMiuix( uiState: ModuleUiState, confirmDialogState: ModuleConfirmDialogState?, effect: ModuleEffect?, actions: ModuleActions, bottomInnerPadding: Dp, ) { val modules = uiState.moduleList val searchStatus = uiState.searchStatus val context = LocalContext.current val enableBlur = LocalEnableBlur.current val installPromptWithName = stringResource(R.string.module_install_prompt_with_name, "%s") val confirmDialog = rememberConfirmDialog( onConfirm = { when (val request = confirmDialogState?.request) { is ModuleConfirmRequest.Uninstall -> { actions.onUninstallModule(request.module) } is ModuleConfirmRequest.Update -> { actions.onConfirmUpdate(request) } null -> Unit } }, onDismiss = actions.onDismissConfirmRequest, ) val scrollBehavior = MiuixScrollBehavior() var fabVisible by remember { mutableStateOf(true) } var scrollDistance by remember { mutableFloatStateOf(0f) } val dynamicTopPadding by remember { derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) } } val shortcutState = rememberModuleShortcutState(context) val showShortcutDialog = remember { mutableStateOf(false) } val pickShortcutIconLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.GetContent() ) { uri -> shortcutState.updateIconUri(uri?.toString()) } LaunchedEffect(confirmDialogState) { confirmDialogState?.let { confirmDialog.showConfirm( title = it.title, content = it.content, markdown = it.markdown, html = it.html, confirm = it.confirm, dismiss = it.dismiss, ) } } LaunchedEffect(effect) { when (effect) { is ModuleEffect.Toast -> { Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() actions.onConsumeEffect() } is ModuleEffect.SnackBar -> { Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show() actions.onConsumeEffect() } null -> Unit } } fun onModuleAddShortcut(module: Module, type: ShortcutType) { shortcutState.bindModule(module) shortcutState.selectType(type) showShortcutDialog.value = true } val listState = rememberLazyListState() val nestedScrollConnection = remember(uiState.installButtonVisible) { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val isScrolledToEnd = (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size ?: 0) < listState.layoutInfo.viewportEndOffset) val delta = available.y if (!isScrolledToEnd) { scrollDistance += delta if (scrollDistance < -50f) { if (fabVisible) fabVisible = false scrollDistance = 0f } else if (scrollDistance > 50f) { if (!fabVisible) fabVisible = true scrollDistance = 0f } } return Offset.Zero } } } val offsetHeight by animateDpAsState( targetValue = if (fabVisible) 0.dp else 180.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), animationSpec = tween(durationMillis = 350) ) val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) { TopAppBar( color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.module), actions = { Box { val showTopPopup = remember { mutableStateOf(false) } IconButton( modifier = Modifier.padding(end = 8.dp), onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.MoreCircle, tint = colorScheme.onSurface, contentDescription = null ) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.MenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = { showTopPopup.value = false }, content = { ListPopupColumn { DropdownImpl( text = stringResource(R.string.module_sort_action_first), optionSize = 2, isSelected = uiState.sortActionFirst, onSelectedIndexChange = { actions.onToggleSortActionFirst() showTopPopup.value = false }, index = 0 ) DropdownImpl( text = stringResource(R.string.module_sort_enabled_first), optionSize = 2, isSelected = uiState.sortEnabledFirst, onSelectedIndexChange = { actions.onToggleSortEnabledFirst() showTopPopup.value = false }, index = 1 ) } } ) } RebootListPopupMiuix( modifier = Modifier.padding(end = 16.dp), alignment = PopupPositionProvider.Align.TopEnd, ) }, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = actions.onOpenRepo, ) { Icon( imageVector = MiuixIcons.Download, tint = colorScheme.onSurface, contentDescription = null ) } }, scrollBehavior = scrollBehavior ) } }, floatingActionButton = { if (uiState.installButtonVisible) { val moduleInstall = stringResource(id = R.string.module_install) val confirmTitle = stringResource(R.string.module) var zipUris by remember { mutableStateOf>(emptyList()) } val confirmDialog = rememberConfirmDialog( onConfirm = { actions.onOpenFlash(zipUris) } ) val selectZipLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { activityResult -> val uris = mutableListOf() if (activityResult.resultCode != RESULT_OK) { return@rememberLauncherForActivityResult } val data = activityResult.data ?: return@rememberLauncherForActivityResult val clipData = data.clipData if (clipData != null) { for (i in 0 until clipData.itemCount) { clipData.getItemAt(i)?.uri?.let { uris.add(it) } } } else { data.data?.let { uris.add(it) } } if (uris.size == 1) { actions.onOpenFlash(listOf(uris.first())) } else if (uris.size > 1) { // multiple files selected zipUris = uris val moduleNames = uris.mapIndexed { index, uri -> "\n${index + 1}. ${uri.getFileName(context)}" }.joinToString("") val confirmContent = installPromptWithName.format(moduleNames) confirmDialog.showConfirm( title = confirmTitle, content = confirmContent ) } } FloatingActionButton( modifier = Modifier .offset { IntOffset(x = 0, y = offsetHeight.roundToPx()) } .padding(bottom = bottomInnerPadding + 20.dp, end = 20.dp) .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), shadowElevation = 0.dp, onClick = { // Select the zip files to install val intent = Intent(Intent.ACTION_GET_CONTENT).apply { type = "application/zip" putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } selectZipLauncher.launch(intent) }, content = { Icon( Icons.Rounded.Add, moduleInstall, modifier = Modifier.size(40.dp), tint = colorScheme.onPrimary ) }, ) } }, popupHost = { searchStatus.SearchPager( onSearchStatusChange = actions.onSearchStatusChange, defaultResult = {}, searchBarTopPadding = dynamicTopPadding, ) { val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() ModuleList( modifier = Modifier .fillMaxSize() .overScrollVertical(), modules = uiState.searchResults, updateInfoMap = uiState.updateInfo, actions = actions, onModuleAddShortcut = ::onModuleAddShortcut, contentPadding = PaddingValues( top = 6.dp, start = 0.dp, end = 0.dp, bottom = maxOf(bottomInnerPadding, imeBottomPadding), ), animateItems = true, ) } }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> if (uiState.magiskInstalled) { Box( modifier = Modifier .fillMaxSize() .padding(12.dp), contentAlignment = Alignment.Center ) { Text( stringResource(R.string.module_magisk_conflict), textAlign = TextAlign.Center, ) } return@Scaffold } val layoutDirection = LocalLayoutDirection.current val pullToRefreshState = rememberPullToRefreshState() val refreshTexts = listOf( stringResource(R.string.refresh_pulling), stringResource(R.string.refresh_release), stringResource(R.string.refresh_refresh), stringResource(R.string.refresh_complete), ) searchStatus.SearchBox( onSearchStatusChange = actions.onSearchStatusChange, searchBarTopPadding = dynamicTopPadding, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), hazeState = hazeState, hazeStyle = hazeStyle ) { boxHeight -> if (modules.isEmpty()) { Box( modifier = Modifier .fillMaxSize() .padding( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = bottomInnerPadding ), contentAlignment = Alignment.Center ) { Text( stringResource(R.string.module_empty), textAlign = TextAlign.Center, color = Color.Gray, ) } } else { val contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = bottomInnerPadding, ) PullToRefresh( isRefreshing = uiState.isRefreshing, pullToRefreshState = pullToRefreshState, onRefresh = actions.onRefresh, refreshTexts = refreshTexts, contentPadding = contentPadding, ) { ModuleList( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .nestedScroll(nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, modules = modules, updateInfoMap = uiState.updateInfo, actions = actions, onModuleAddShortcut = { module, type -> onModuleAddShortcut(module, type) }, contentPadding = contentPadding, ) } } } } ModuleShortcutDialog( show = showShortcutDialog.value, onDismissRequest = { showShortcutDialog.value = false }, shortcutState = shortcutState, onPickShortcutIcon = { pickShortcutIconLauncher.launch("image/*") }, onDeleteShortcut = { shortcutState.deleteShortcut(context) showShortcutDialog.value = false }, onConfirmShortcut = { shortcutState.createShortcut(context) showShortcutDialog.value = false }, ) } @Composable private fun ModuleShortcutDialog( show: Boolean, onDismissRequest: () -> Unit, shortcutState: ModuleShortcutState, onPickShortcutIcon: () -> Unit, onDeleteShortcut: () -> Unit, onConfirmShortcut: () -> Unit, ) { SuperDialog( show = show, title = stringResource(R.string.module_shortcut_title), onDismissRequest = onDismissRequest, content = { Column( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { Box( contentAlignment = Alignment.Center, modifier = Modifier .padding(vertical = 16.dp) .size(100.dp) .clip(ContinuousRoundedRectangle(25.dp)) ) { val preview = shortcutState.previewIcon if (preview != null) { Image( bitmap = preview, modifier = Modifier.size(100.dp), contentDescription = null, ) } else { Box( modifier = Modifier .size(100.dp) .background(Color.White) ) Image( painter = painterResource(id = R.drawable.ic_launcher_foreground), contentDescription = null, contentScale = FixedScale(1.5f) ) } } Row { TextButton( modifier = Modifier.weight(1f), text = stringResource(id = R.string.module_shortcut_icon_pick), onClick = onPickShortcutIcon, ) AnimatedVisibility( visible = shortcutState.iconUri != shortcutState.defaultShortcutIconUri, enter = expandHorizontally() + slideInHorizontally(initialOffsetX = { it }), exit = shrinkHorizontally() + slideOutHorizontally(targetOffsetX = { it }), modifier = Modifier.align(Alignment.CenterVertically), ) { IconButton( onClick = shortcutState::resetIconToDefault, modifier = Modifier.padding(start = 12.dp) ) { Icon( imageVector = MiuixIcons.Undo, contentDescription = null, tint = colorScheme.onSurface, modifier = Modifier.size(28.dp), ) } } } TextField( value = shortcutState.name, onValueChange = shortcutState::updateName, label = stringResource(id = R.string.module_shortcut_name_label) ) if (shortcutState.hasExistingShortcut) { TextButton( text = stringResource(id = R.string.module_shortcut_delete), onClick = onDeleteShortcut, modifier = Modifier.fillMaxWidth(), ) } Row( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { TextButton( text = stringResource(id = android.R.string.cancel), onClick = onDismissRequest, modifier = Modifier.weight(1f), ) TextButton( text = if (shortcutState.hasExistingShortcut) { stringResource(id = R.string.module_update) } else { stringResource(id = android.R.string.ok) }, onClick = onConfirmShortcut, colors = ButtonDefaults.textButtonColorsPrimary(), modifier = Modifier.weight(1f), ) } } } ) } @Composable private fun ModuleList( modifier: Modifier = Modifier, modules: List, updateInfoMap: Map, actions: ModuleActions, onModuleAddShortcut: (Module, ShortcutType) -> Unit, contentPadding: PaddingValues, animateItems: Boolean = false, ) { val loadingDialog = rememberLoadingDialog() val scope = rememberCoroutineScope() LazyColumn( modifier = modifier.fillMaxHeight(), contentPadding = contentPadding, overscrollEffect = null, ) { items( items = modules, key = { it.id }, contentType = { "module" } ) { module -> val currentModuleState = rememberUpdatedState(module) val moduleUpdateInfo = updateInfoMap[module.id] ?: ModuleUpdateInfo.Empty val content: @Composable () -> Unit = { ModuleItem( module = module, updateUrl = moduleUpdateInfo.downloadUrl, onUninstall = { actions.onRequestUninstallConfirmation(currentModuleState.value) }, onUndoUninstall = { scope.launch { loadingDialog.withLoading { actions.onUndoUninstallModule(module) } } }, onCheckChanged = { _: Boolean -> scope.launch { loadingDialog.withLoading { actions.onToggleModule(module) } } }, onUpdate = { scope.launch { loadingDialog.withLoading { actions.onRequestUpdateConfirmation(currentModuleState.value, moduleUpdateInfo) } } }, onExecuteAction = { actions.onExecuteModuleAction(currentModuleState.value) }, onAddActionShortcut = { type: ShortcutType -> onModuleAddShortcut(currentModuleState.value, type) }, onOpenWebUi = { if (module.hasWebUi) { actions.onOpenWebUi(module) } } ) } if (animateItems) { AnimatedVisibility( visible = true, enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { content() } } else { content() } } } } @OptIn(ExperimentalFoundationApi::class) @Composable fun ModuleItem( module: Module, updateUrl: String, onUndoUninstall: () -> Unit, onUninstall: () -> Unit, onCheckChanged: (Boolean) -> Unit, onUpdate: () -> Unit, onExecuteAction: () -> Unit, onAddActionShortcut: (ShortcutType) -> Unit, onOpenWebUi: () -> Unit ) { val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f) val actionIconTint = colorScheme.onSurface.copy(alpha = if (isInDarkTheme()) 0.7f else 0.9f) val updateBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f) val updateTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f) val hasUpdate by remember(updateUrl) { derivedStateOf { updateUrl.isNotEmpty() } } val textDecoration by remember(module.remove) { mutableStateOf(if (module.remove) TextDecoration.LineThrough else null) } val hasDescription by remember(module.description) { derivedStateOf { module.description.isNotBlank() } } var expanded by rememberSaveable(module.id) { mutableStateOf(false) } Card( modifier = Modifier .padding(horizontal = 12.dp) .padding(bottom = 12.dp), insideMargin = PaddingValues(16.dp), onClick = { if (hasDescription) expanded = !expanded } ) { Row( horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { Column( modifier = Modifier .weight(1f) .padding(end = 4.dp) ) { val moduleVersion = stringResource(id = R.string.module_version) val moduleAuthor = stringResource(id = R.string.module_author) SubcomposeLayout { constraints -> val spacingPx = 6.dp.roundToPx() var nameTextLayout: TextLayoutResult? = null val metaPlaceable = if (module.metamodule) { subcompose("meta") { Text( text = "META", fontSize = 12.sp, color = updateTint, modifier = Modifier .clip(ContinuousRoundedRectangle(6.dp)) .background(updateBg) .padding(horizontal = 6.dp, vertical = 2.dp), fontWeight = FontWeight(750), maxLines = 1, softWrap = false ) }.first().measure(Constraints(0, constraints.maxWidth, 0, constraints.maxHeight)) } else null val reserved = (metaPlaceable?.width ?: 0) + if (metaPlaceable != null) spacingPx else 0 val nameMax = (constraints.maxWidth - reserved).coerceAtLeast(0) val namePlaceable = subcompose("name") { Text( text = module.name, fontSize = 17.sp, fontWeight = FontWeight(550), color = colorScheme.onSurface, textDecoration = textDecoration, onTextLayout = { nameTextLayout = it } ) }.first().measure(Constraints(constraints.minWidth, nameMax, constraints.minHeight, constraints.maxHeight)) val width = (namePlaceable.width + reserved).coerceIn(constraints.minWidth, constraints.maxWidth) val height = maxOf(namePlaceable.height, metaPlaceable?.height ?: 0) layout(width, height) { namePlaceable.placeRelative(0, 0) val endX = nameTextLayout?.let { layoutRes -> val last = (layoutRes.lineCount - 1).coerceAtLeast(0) layoutRes.getLineRight(last).toInt() } ?: namePlaceable.width metaPlaceable?.placeRelative(endX + spacingPx, (height - (metaPlaceable.height)) / 2) } } Text( text = "$moduleVersion: ${module.version}", fontSize = 12.sp, modifier = Modifier.padding(top = 2.dp), fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, textDecoration = textDecoration ) Text( text = "$moduleAuthor: ${module.author}", fontSize = 12.sp, modifier = Modifier.padding(bottom = 1.dp), fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, textDecoration = textDecoration ) } Switch( enabled = !module.update, checked = module.enabled, onCheckedChange = { if (it != module.enabled) onCheckChanged(it) } ) } if (hasDescription) { Box( modifier = Modifier .padding(top = 2.dp) .animateContentSize( animationSpec = tween( durationMillis = 250, easing = FastOutSlowInEasing ) ) ) { Text( text = module.description, fontSize = 14.sp, color = colorScheme.onSurfaceVariantSummary, overflow = if (expanded) TextOverflow.Clip else TextOverflow.Ellipsis, maxLines = if (expanded) Int.MAX_VALUE else 4, textDecoration = textDecoration ) } } HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) Row { AnimatedVisibility( visible = module.enabled && !module.remove && !module.update, enter = fadeIn(), exit = fadeOut() ) { Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { if (module.hasActionScript) { Row( modifier = Modifier .heightIn(min = 35.dp) .widthIn(min = 35.dp) .clip(CircleShape) .background(secondaryContainer) .combinedClickable( onClick = onExecuteAction, onLongClick = { onAddActionShortcut(ShortcutType.Action) } ) .padding( start = if (!module.hasWebUi && !hasUpdate) 6.dp else 0.dp, end = if (!module.hasWebUi && !hasUpdate) 8.dp else 0.dp, ), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Icon( modifier = Modifier.size(24.dp), imageVector = Icons.Rounded.PlayArrow, tint = actionIconTint, contentDescription = stringResource(R.string.action) ) if (!module.hasWebUi && !hasUpdate) { Text( modifier = Modifier.padding(start = 3.dp, end = 4.dp), text = stringResource(R.string.action), color = actionIconTint, fontWeight = FontWeight.Medium, fontSize = 15.sp, ) } } } if (module.hasWebUi) { Row( modifier = Modifier .heightIn(min = 35.dp) .widthIn(min = 35.dp) .clip(CircleShape) .background(secondaryContainer) .combinedClickable( onClick = onOpenWebUi, onLongClick = { onAddActionShortcut(ShortcutType.WebUI) } ) .padding(horizontal = if (!module.hasActionScript && !hasUpdate) 10.dp else 0.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.Center ) { Icon( modifier = Modifier.size(22.dp), imageVector = Icons.Rounded.Code, tint = actionIconTint, contentDescription = stringResource(R.string.open) ) if (!module.hasActionScript && !hasUpdate) { Text( modifier = Modifier.padding(start = 4.dp, end = 2.dp), text = stringResource(R.string.open), color = actionIconTint, fontWeight = FontWeight.Medium, fontSize = 15.sp, ) } } } } } Spacer(Modifier.weight(1f)) AnimatedVisibility( visible = hasUpdate, enter = fadeIn(), exit = fadeOut() ) { IconButton( modifier = Modifier.padding(end = 16.dp), backgroundColor = updateBg, enabled = !module.remove, minHeight = 35.dp, minWidth = 35.dp, onClick = onUpdate, ) { Row( modifier = Modifier.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), ) { Icon( modifier = Modifier.size(20.dp), imageVector = MiuixIcons.UploadCloud, tint = updateTint, contentDescription = stringResource(R.string.module_update), ) Text( modifier = Modifier.padding(start = 4.dp, end = 3.dp), text = stringResource(R.string.module_update), color = updateTint, fontWeight = FontWeight.Medium, fontSize = 15.sp ) } } } IconButton( minHeight = 35.dp, minWidth = 35.dp, onClick = if (module.remove) onUndoUninstall else onUninstall, backgroundColor = if (module.remove) { secondaryContainer.copy(alpha = 0.8f) } else { secondaryContainer }, ) { val animatedPadding by animateDpAsState( targetValue = if (!hasUpdate) 10.dp else 0.dp, animationSpec = tween(durationMillis = 300) ) Row( modifier = Modifier.padding(horizontal = animatedPadding), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(20.dp), imageVector = if (module.remove) { MiuixIcons.Undo } else { MiuixIcons.Delete }, tint = actionIconTint, contentDescription = null ) AnimatedVisibility( visible = !hasUpdate, enter = expandHorizontally(), exit = shrinkHorizontally() ) { Text( modifier = Modifier.padding(start = 4.dp, end = 3.dp), text = stringResource( if (module.remove) R.string.undo else R.string.uninstall ), color = actionIconTint, fontWeight = FontWeight.Medium, fontSize = 15.sp ) } } } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.content.Intent import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.unit.Dp import androidx.core.net.toUri import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.util.download import me.weishu.kernelsu.ui.util.module.fetchModuleDetail import me.weishu.kernelsu.ui.util.module.fetchReleaseDescriptionHtml import me.weishu.kernelsu.ui.util.toggleModule import me.weishu.kernelsu.ui.util.undoUninstallModule import me.weishu.kernelsu.ui.util.uninstallModule import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel import me.weishu.kernelsu.ui.webui.WebUIActivity import okhttp3.Request @Composable fun ModulePager( bottomInnerPadding: Dp ) { val uiMode = LocalUiMode.current val navigator = LocalNavigator.current val context = LocalContext.current val resource = LocalResources.current val viewModel = viewModel() val rawUiState by viewModel.uiState.collectAsState() val scope = rememberCoroutineScope() var confirmDialogState by remember { mutableStateOf(null) } var effect by remember { mutableStateOf(null) } val webUILauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } LaunchedEffect(Unit) { viewModel.refreshEnvironmentState() viewModel.initializePreferences() if (rawUiState.moduleList.isEmpty() || viewModel.isNeedRefresh) { viewModel.fetchModuleList(checkUpdate = true) } } suspend fun buildUpdateConfirmDialogState( module: Module, updateInfo: ModuleUpdateInfo, ): ModuleConfirmDialogState { val changelogUrl = updateInfo.changelog var changelog = "" var html = false if (changelogUrl.isNotBlank()) { withContext(Dispatchers.IO) { if (changelogUrl.startsWith("#") && changelogUrl.contains('@')) { val parts = changelogUrl.substring(1).split('@', limit = 2) if (parts.size == 2) { fetchReleaseDescriptionHtml(parts[0], parts[1])?.let { changelog = it html = true } } } if (changelog.isBlank()) { changelog = runCatching { ksuApp.okhttpClient.newCall( Request.Builder().url(changelogUrl).build() ).execute().body.string() }.getOrDefault("") } } } if (changelog.isBlank()) { withContext(Dispatchers.IO) { runCatching { val latestTag = fetchModuleDetail(module.id)?.latestTag.orEmpty() if (latestTag.isNotBlank()) { fetchReleaseDescriptionHtml(module.id, latestTag)?.let { changelog = it html = true } } } } } return ModuleConfirmDialogState( request = ModuleConfirmRequest.Update( module = module, downloadUrl = updateInfo.downloadUrl, fileName = "${module.name}-${updateInfo.version}.zip", ), title = if (changelog.isNotBlank()) resource.getString(R.string.module_changelog) else resource.getString(R.string.module_update), content = changelog.ifBlank { resource.getString(R.string.module_start_downloading).format(module.name) }, markdown = changelog.isNotBlank() && !html, html = html, confirm = resource.getString(R.string.module_update), ) } val actions = ModuleActions( onRefresh = { viewModel.fetchModuleList(checkUpdate = true) }, onSearchStatusChange = { viewModel.updateSearchStatus(it) }, onSearchTextChange = { text -> viewModel.updateSearchText(text) }, onClearSearch = { viewModel.updateSearchText("") }, onRequestUpdateConfirmation = { module, updateInfo -> scope.launch { confirmDialogState = buildUpdateConfirmDialogState(module, updateInfo) } }, onRequestUninstallConfirmation = { module -> confirmDialogState = ModuleConfirmDialogState( request = ModuleConfirmRequest.Uninstall(module), title = resource.getString(R.string.module), content = (if (module.metamodule) resource.getString(R.string.metamodule_uninstall_confirm) else resource.getString(R.string.module_uninstall_confirm)).format( module.name ), confirm = resource.getString(R.string.uninstall), dismiss = resource.getString(android.R.string.cancel), ) }, onDismissConfirmRequest = { confirmDialogState = null }, onConsumeEffect = { effect = null }, onConfirmUpdate = { request -> scope.launch { withContext(Dispatchers.IO) { download( url = request.downloadUrl, fileName = request.fileName, onDownloaded = { uri -> navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri)))) viewModel.markNeedRefresh() }, onDownloading = { effect = ModuleEffect.Toast(resource.getString(R.string.module_downloading).format(request.module.name)) }, ) } confirmDialogState = null } }, onOpenRepo = { navigator.push(Route.ModuleRepo) }, onToggleSortActionFirst = { viewModel.toggleSortActionFirst() }, onToggleSortEnabledFirst = { viewModel.toggleSortEnabledFirst() }, onOpenWebUi = { module -> webUILauncher.launch( Intent(context, WebUIActivity::class.java) .setData("kernelsu://webui/${module.id}".toUri()) .putExtra("id", module.id) ) }, onToggleModule = { module -> scope.launch { val success = withContext(Dispatchers.IO) { toggleModule(module.id, !module.enabled) } if (success) { viewModel.fetchModuleList(checkUpdate = true) effect = ModuleEffect.SnackBar(resource.getString(R.string.reboot_to_apply)) } else { val message = if (module.enabled) R.string.module_failed_to_disable else R.string.module_failed_to_enable effect = ModuleEffect.SnackBar(resource.getString(message).format(module.name)) } } }, onUninstallModule = { module -> scope.launch { val success = withContext(Dispatchers.IO) { uninstallModule(module.id) } if (success) { viewModel.fetchModuleList(checkUpdate = true) } confirmDialogState = null effect = ModuleEffect.SnackBar( resource.getString( if (success) R.string.module_uninstall_success else R.string.module_uninstall_failed ).format(module.name) ) } }, onUndoUninstallModule = { module -> scope.launch { val success = withContext(Dispatchers.IO) { undoUninstallModule(module.id) } if (success) { viewModel.fetchModuleList(checkUpdate = true) } effect = ModuleEffect.SnackBar( resource.getString( if (success) R.string.module_undo_uninstall_success else R.string.module_undo_uninstall_failed ).format(module.name) ) } }, onOpenFlash = { uris -> if (uris.isNotEmpty()) { navigator.push(Route.Flash(FlashIt.FlashModules(uris))) viewModel.markNeedRefresh() } }, onExecuteModuleAction = { module -> navigator.push(Route.ExecuteModuleAction(module.id)) viewModel.markNeedRefresh() }, ) when (uiMode) { UiMode.Miuix -> ModulePagerMiuix( uiState = rawUiState, confirmDialogState = confirmDialogState, effect = effect, actions = actions, bottomInnerPadding = bottomInnerPadding, ) UiMode.Material -> ModulePagerMaterial( uiState = rawUiState, confirmDialogState = confirmDialogState, effect = effect, actions = actions, bottomInnerPadding = bottomInnerPadding, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleShortcutState.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.content.Context import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.Stable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.listSaver import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.asImageBitmap import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.ui.util.module.Shortcut @Stable class ModuleShortcutState internal constructor( moduleId: String?, name: String, iconUri: String?, selectedTypeName: String?, supportsActionShortcut: Boolean, supportsWebUiShortcut: Boolean, defaultActionIconUri: String?, defaultWebUiIconUri: String?, ) { var moduleId by mutableStateOf(moduleId) private set var name by mutableStateOf(name) var iconUri by mutableStateOf(iconUri) private set var selectedType by mutableStateOf(selectedTypeName?.let(ShortcutType::valueOf)) private set var supportsActionShortcut by mutableStateOf(supportsActionShortcut) private set var supportsWebUiShortcut by mutableStateOf(supportsWebUiShortcut) private set var defaultActionIconUri by mutableStateOf(defaultActionIconUri) private set var defaultWebUiIconUri by mutableStateOf(defaultWebUiIconUri) private set var previewIcon by mutableStateOf(null) internal set var hasExistingShortcut by mutableStateOf(false) internal set val availableTypes: List get() = buildList { if (supportsActionShortcut) add(ShortcutType.Action) if (supportsWebUiShortcut) add(ShortcutType.WebUI) } val defaultShortcutIconUri: String? get() = selectedType?.let(::defaultIconFor) fun bindModule(module: Module) { moduleId = module.id name = module.name iconUri = null selectedType = null supportsActionShortcut = module.hasActionScript supportsWebUiShortcut = module.hasWebUi defaultActionIconUri = module.actionIconPath ?.takeIf { it.isNotBlank() } ?.let { "su:$it" } defaultWebUiIconUri = module.webUiIconPath ?.takeIf { it.isNotBlank() } ?.let { "su:$it" } } fun selectType(type: ShortcutType) { selectedType = type iconUri = defaultIconFor(type) } fun updateName(value: String) { name = value } fun updateIconUri(value: String?) { iconUri = value } fun resetIconToDefault() { iconUri = defaultShortcutIconUri } fun createShortcut(context: Context) { val currentModuleId = moduleId val type = selectedType if (currentModuleId.isNullOrBlank() || name.isBlank() || type == null) { return } createModuleShortcut( context = context, moduleId = currentModuleId, name = name, iconUri = iconUri, type = type, ) hasExistingShortcut = true } fun deleteShortcut(context: Context) { val currentModuleId = moduleId val type = selectedType if (currentModuleId.isNullOrBlank() || type == null) { return } deleteModuleShortcut(context, currentModuleId, type) hasExistingShortcut = false } private fun defaultIconFor(type: ShortcutType): String? { return when (type) { ShortcutType.Action -> defaultActionIconUri ?: defaultWebUiIconUri ShortcutType.WebUI -> defaultWebUiIconUri ?: defaultActionIconUri } } companion object { val Saver = listSaver( save = { listOf( it.moduleId, it.name, it.iconUri, it.selectedType?.name, it.supportsActionShortcut, it.supportsWebUiShortcut, it.defaultActionIconUri, it.defaultWebUiIconUri, ) }, restore = { ModuleShortcutState( moduleId = it[0] as String?, name = it[1] as String, iconUri = it[2] as String?, selectedTypeName = it[3] as String?, supportsActionShortcut = it[4] as Boolean, supportsWebUiShortcut = it[5] as Boolean, defaultActionIconUri = it[6] as String?, defaultWebUiIconUri = it[7] as String?, ) } ) } } @Composable fun rememberModuleShortcutState( context: Context, ): ModuleShortcutState { val state = rememberSaveable(saver = ModuleShortcutState.Saver) { ModuleShortcutState( moduleId = null, name = "", iconUri = null, selectedTypeName = null, supportsActionShortcut = false, supportsWebUiShortcut = false, defaultActionIconUri = null, defaultWebUiIconUri = null, ) } LaunchedEffect(state.iconUri) { val uri = state.iconUri if (uri.isNullOrBlank()) { state.previewIcon = null return@LaunchedEffect } val bitmap = withContext(Dispatchers.IO) { Shortcut.loadShortcutBitmap(context, uri) } state.previewIcon = bitmap?.asImageBitmap() } LaunchedEffect(state.moduleId, state.selectedType) { val moduleId = state.moduleId val type = state.selectedType if (moduleId.isNullOrBlank() || type == null) { state.hasExistingShortcut = false return@LaunchedEffect } state.hasExistingShortcut = withContext(Dispatchers.IO) { hasModuleShortcut(context, moduleId, type) } } return remember(state) { state } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.net.Uri import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.ui.component.SearchStatus sealed interface ModuleConfirmRequest { data class Update( val module: Module, val downloadUrl: String, val fileName: String, ) : ModuleConfirmRequest data class Uninstall( val module: Module, ) : ModuleConfirmRequest } data class ModuleConfirmDialogState( val request: ModuleConfirmRequest, val title: String, val content: String? = null, val markdown: Boolean = false, val html: Boolean = false, val confirm: String? = null, val dismiss: String? = null, ) sealed interface ModuleEffect { data class Toast( val message: String, ) : ModuleEffect data class SnackBar( val message: String, ) : ModuleEffect } data class ModuleUiState( val isRefreshing: Boolean = false, val modules: List = emptyList(), val moduleList: List = emptyList(), val updateInfo: Map = emptyMap(), val searchStatus: SearchStatus = SearchStatus(""), val searchResults: List = emptyList(), val sortEnabledFirst: Boolean = false, val sortActionFirst: Boolean = false, val checkModuleUpdate: Boolean = true, val isSafeMode: Boolean = false, val magiskInstalled: Boolean = false, ) { val installButtonVisible: Boolean get() = !(isSafeMode || magiskInstalled) } data class ModuleActions( val onRefresh: () -> Unit, val onSearchStatusChange: (SearchStatus) -> Unit, val onSearchTextChange: (String) -> Unit, val onClearSearch: () -> Unit, val onRequestUpdateConfirmation: (Module, ModuleUpdateInfo) -> Unit, val onRequestUninstallConfirmation: (Module) -> Unit, val onDismissConfirmRequest: () -> Unit, val onConsumeEffect: () -> Unit, val onConfirmUpdate: (ModuleConfirmRequest.Update) -> Unit, val onOpenRepo: () -> Unit, val onToggleSortActionFirst: () -> Unit, val onToggleSortEnabledFirst: () -> Unit, val onOpenWebUi: (Module) -> Unit, val onToggleModule: (Module) -> Unit, val onUninstallModule: (Module) -> Unit, val onUndoUninstallModule: (Module) -> Unit, val onOpenFlash: (List) -> Unit, val onExecuteModuleAction: (Module) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/module/ModuleUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.module import android.content.Context import me.weishu.kernelsu.ui.util.module.Shortcut enum class ShortcutType { Action, WebUI } fun hasModuleShortcut(context: Context, moduleId: String, type: ShortcutType): Boolean { return when (type) { ShortcutType.Action -> Shortcut.hasModuleActionShortcut(context, moduleId) ShortcutType.WebUI -> Shortcut.hasModuleWebUiShortcut(context, moduleId) } } fun deleteModuleShortcut(context: Context, moduleId: String, type: ShortcutType) { when (type) { ShortcutType.Action -> Shortcut.deleteModuleActionShortcut(context, moduleId) ShortcutType.WebUI -> Shortcut.deleteModuleWebUiShortcut(context, moduleId) } } fun createModuleShortcut( context: Context, moduleId: String, name: String, iconUri: String?, type: ShortcutType ) { when (type) { ShortcutType.Action -> { Shortcut.createModuleActionShortcut( context = context, moduleId = moduleId, name = name, iconUri = iconUri ) } ShortcutType.WebUI -> { Shortcut.createModuleWebUiShortcut( context = context, moduleId = moduleId, name = name, iconUri = iconUri ) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.modulerepo import android.annotation.SuppressLint import android.content.Context import android.net.Uri 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.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ChromeReaderMode import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.outlined.Download import androidx.compose.material.icons.outlined.Link import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Checkbox import androidx.compose.material3.CircularWavyProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.PrimaryTabRow import androidx.compose.material3.Scaffold import androidx.compose.material3.Tab import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.RepoModule import me.weishu.kernelsu.ui.component.GithubMarkdown import me.weishu.kernelsu.ui.component.dialog.ConfirmDialogHandle import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.material.SearchAppBar import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.screen.home.TonalCard import me.weishu.kernelsu.ui.util.download import java.text.Collator @SuppressLint("LocalContextGetResourceValueCall") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ModuleRepoScreenMaterial( state: ModuleRepoUiState, actions: ModuleRepoActions, ) { val listState = rememberLazyListState() val searchListState = rememberLazyListState() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) Scaffold( topBar = { SearchAppBar( title = { Text(text = stringResource(R.string.module_repos)) }, searchText = state.searchStatus.searchText, onSearchTextChange = actions.onSearchTextChange, onClearClick = actions.onClearSearch, scrollBehavior = scrollBehavior, navigationIcon = { IconButton( onClick = actions.onBack, content = { Icon(Icons.AutoMirrored.Outlined.ArrowBack, null) } ) }, actions = { var showDropdown by remember { mutableStateOf(false) } IconButton( onClick = { showDropdown = true } ) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings) ) DropdownMenu(expanded = showDropdown, onDismissRequest = { showDropdown = false }) { DropdownMenuItem( text = { Text(stringResource(R.string.module_repos_sort_name)) }, trailingIcon = { Checkbox(state.sortByName, null) }, onClick = { actions.onToggleSortByName() } ) } } }, searchContent = { _, closeSearch -> LaunchedEffect(state.searchStatus.searchText) { searchListState.scrollToItem(0) } val sortByName = state.sortByName val collator = Collator.getInstance(LocalLocale.current.platformLocale) val searchModules = if (!sortByName) { state.searchResults } else { state.searchResults.sortedWith(compareBy(collator) { it.moduleName }) } RepoModuleList( modules = searchModules, listState = searchListState, modifier = Modifier.fillMaxSize(), onModuleClick = { closeSearch() actions.onOpenRepoDetail(it) } ) } ) }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val isLoading = state.modules.isEmpty() if (isLoading) { Box( modifier = Modifier .fillMaxSize() .padding(innerPadding), contentAlignment = Alignment.Center ) { if (state.offline) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = stringResource(R.string.network_offline), color = MaterialTheme.colorScheme.outline) Spacer(Modifier.height(12.dp)) Button( onClick = actions.onRefresh, ) { Text(stringResource(R.string.network_retry)) } } } else { LoadingIndicator() } } } else { val displayModules = run { val base = state.modules val sortByName = state.sortByName val collator = Collator.getInstance(LocalLocale.current.platformLocale) if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName }) } RepoModuleList( modules = displayModules, listState = listState, modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection) .padding(innerPadding), onModuleClick = actions.onOpenRepoDetail ) } } } @Composable private fun RepoModuleList( modules: List, listState: LazyListState, modifier: Modifier = Modifier, bottomPadding: Dp = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding(), onModuleClick: (RepoModule) -> Unit, ) { LazyColumn( modifier = modifier, state = listState, verticalArrangement = Arrangement.spacedBy(16.dp), contentPadding = PaddingValues( start = 16.dp, top = 8.dp, end = 16.dp, bottom = 16.dp + bottomPadding ), ) { items(modules, key = { it.moduleId }, contentType = { "module" }) { module -> val latestReleaseTime = remember(module.latestReleaseTime) { module.latestReleaseTime } val moduleAuthor = stringResource(id = R.string.module_author) TonalCard(modifier = Modifier.fillMaxWidth()) { Column( modifier = Modifier .fillMaxWidth() .clickable { onModuleClick(module) } .padding(22.dp, 18.dp, 22.dp, 12.dp) ) { if (module.moduleName.isNotEmpty()) { Text( text = module.moduleName, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, lineHeight = MaterialTheme.typography.bodySmall.lineHeight, ) } if (module.moduleId.isNotEmpty()) { Text( text = "ID: ${module.moduleId}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) } Text( text = "$moduleAuthor: ${module.authors}", style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, ) if (module.summary.isNotEmpty()) { Spacer(modifier = Modifier.height(8.dp)) Text( text = module.summary, color = MaterialTheme.colorScheme.outline, style = MaterialTheme.typography.bodyMedium, overflow = TextOverflow.Ellipsis, maxLines = 4, ) } Row(modifier = Modifier.padding(vertical = 4.dp)) { if (module.metamodule) { StatusTag( "META", contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } } HorizontalDivider(thickness = Dp.Hairline) Spacer(modifier = Modifier.height(4.dp)) Row( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { if (module.stargazerCount > 0) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = Icons.Rounded.Star, contentDescription = "stars", tint = MaterialTheme.colorScheme.outline, modifier = Modifier.size(16.dp) ) Text( text = module.stargazerCount.toString(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, modifier = Modifier.padding(start = 4.dp) ) } } Spacer(Modifier.weight(1f)) if (latestReleaseTime.isNotEmpty()) { Text( text = latestReleaseTime, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, ) } } } } } item { Spacer(Modifier.height(12.dp)) } } } @SuppressLint("StringFormatInvalid", "DefaultLocale") @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ModuleRepoDetailScreenMaterial( state: ModuleRepoDetailUiState, actions: ModuleRepoDetailActions, ) { val module = state.module val context = LocalContext.current val scope = rememberCoroutineScope() val confirmTitle = stringResource(R.string.module_install) var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) } val confirmDialog = rememberConfirmDialog(onConfirm = { pendingDownload?.invoke() }) val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } Scaffold( topBar = { LargeFlexibleTopAppBar( title = { Text(text = module.moduleName) }, navigationIcon = { IconButton(onClick = actions.onBack) { Icon( imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null, ) } }, actions = { if (state.webUrl.isNotEmpty()) { IconButton(onClick = actions.onOpenWebUrl) { Icon( imageVector = Icons.AutoMirrored.Outlined.ChromeReaderMode, contentDescription = null, ) } } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), scrollBehavior = scrollBehavior ) }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal), ) { innerPadding -> val tabs = listOf( stringResource(R.string.tab_readme), stringResource(R.string.tab_releases), stringResource(R.string.tab_info) ) val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabs.size }) val layoutDirection = LocalLayoutDirection.current Box(modifier = Modifier.fillMaxSize()) { HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), beyondViewportPageCount = 2 ) { page -> val paddedInnerPadding = PaddingValues( top = innerPadding.calculateTopPadding() + 56.dp + 8.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding() + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() + 16.dp ) when (page) { 0 -> ReadmePage( readmeHtml = state.readmeHtml, readmeLoaded = state.readmeLoaded, innerPadding = paddedInnerPadding, scrollBehavior = scrollBehavior ) 1 -> ReleasesPage( detailReleases = state.detailReleases, innerPadding = paddedInnerPadding, scrollBehavior = scrollBehavior, confirmTitle = confirmTitle, confirmDialog = confirmDialog, scope = scope, onInstallModule = actions.onInstallModule, context = context, setPendingDownload = { pendingDownload = it } ) 2 -> InfoPage( module = module, innerPadding = paddedInnerPadding, scrollBehavior = scrollBehavior, uriHandler = object : UriHandler { override fun openUri(uri: String) = actions.onOpenUrl(uri) }, sourceUrl = state.sourceUrl ) } } PrimaryTabRow( selectedTabIndex = pagerState.currentPage, containerColor = MaterialTheme.colorScheme.surface, modifier = Modifier.padding(top = innerPadding.calculateTopPadding()), ) { tabs.forEachIndexed { index, tab -> Tab( selected = pagerState.currentPage == index, onClick = { scope.launch { pagerState.animateScrollToPage(index) } }, text = { Text(tab) }, unselectedContentColor = MaterialTheme.colorScheme.onSurfaceVariant ) } } } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun ReadmePage( readmeHtml: String?, readmeLoaded: Boolean, innerPadding: PaddingValues, scrollBehavior: TopAppBarScrollBehavior ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), ) { item { var isLoading by remember { mutableStateOf(true) } if (isLoading) { Box( modifier = Modifier.fillParentMaxSize(), contentAlignment = Alignment.Center ) { LoadingIndicator() } } if (readmeLoaded && readmeHtml != null) { Box(modifier = Modifier.fillMaxSize()) { GithubMarkdown( content = readmeHtml, onLoadingChange = { isLoading = it }, containerColor = MaterialTheme.colorScheme.surface, ) } } } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @SuppressLint("DefaultLocale") @Composable fun ReleasesPage( detailReleases: List, innerPadding: PaddingValues, scrollBehavior: TopAppBarScrollBehavior, confirmTitle: String, confirmDialog: ConfirmDialogHandle, scope: CoroutineScope, onInstallModule: (Uri) -> Unit, context: Context, setPendingDownload: ((() -> Unit)) -> Unit, ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection) + 16.dp, end = innerPadding.calculateEndPadding(layoutDirection) + 16.dp, bottom = innerPadding.calculateBottomPadding(), ), verticalArrangement = Arrangement.spacedBy(16.dp), ) { if (detailReleases.isNotEmpty()) { items( items = detailReleases, key = { it.tagName }, contentType = { "release" } ) { rel -> val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } } TonalCard { Column( modifier = Modifier.padding(vertical = 18.dp, horizontal = 22.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { Column(modifier = Modifier.weight(1f)) { Text( text = title, style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface ) Text( text = rel.tagName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, modifier = Modifier.padding(top = 2.dp) ) } Text( text = rel.publishedAt, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, modifier = Modifier.align(Alignment.Top) ) } AnimatedVisibility( visible = rel.descriptionHTML.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Column { HorizontalDivider( modifier = Modifier.padding(vertical = 4.dp), thickness = Dp.Hairline, color = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f) ) GithubMarkdown(content = rel.descriptionHTML) } } AnimatedVisibility( visible = rel.assets.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Column { HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = Dp.Hairline ) rel.assets.forEachIndexed { index, asset -> val fileName = asset.name val sizeText = remember(asset.size) { val s = asset.size when { s >= 1024L * 1024L * 1024L -> String.format("%.1f GB", s / (1024f * 1024f * 1024f)) s >= 1024L * 1024L -> String.format("%.1f MB", s / (1024f * 1024f)) s >= 1024L -> String.format("%.0f KB", s / 1024f) else -> "$s B" } } val sizeAndDownloads = remember(sizeText, asset.downloadCount) { "$sizeText · ${asset.downloadCount} downloads" } var isDownloading by remember(fileName, asset.downloadUrl) { mutableStateOf(false) } var progress by remember(fileName, asset.downloadUrl) { mutableIntStateOf(0) } val onClickDownload = remember(fileName, asset.downloadUrl) { { val startText = context.getString(R.string.module_start_downloading, fileName) setPendingDownload { isDownloading = true scope.launch(Dispatchers.IO) { download( asset.downloadUrl, fileName, onDownloaded = onInstallModule, onDownloading = { isDownloading = true }, onProgress = { p -> scope.launch(Dispatchers.Main) { progress = p } } ) } } confirmDialog.showConfirm(title = confirmTitle, content = startText) } } Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Column(modifier = Modifier.weight(1f)) { Text( text = fileName, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurface ) Text( text = sizeAndDownloads, style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.outline, modifier = Modifier.padding(top = 2.dp) ) } FilledTonalButton( onClick = onClickDownload, contentPadding = ButtonDefaults.TextButtonContentPadding ) { if (isDownloading) { CircularWavyProgressIndicator( progress = { progress / 100f }, modifier = Modifier.size(20.dp), ) } else { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Download, contentDescription = stringResource(R.string.install) ) Text( modifier = Modifier.padding(start = 7.dp), text = stringResource(R.string.install), style = MaterialTheme.typography.labelMedium, ) } } } if (index != rel.assets.lastIndex) { HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = Dp.Hairline ) } } } } } } } } } } @OptIn(ExperimentalMaterial3Api::class) @Composable fun InfoPage( module: RepoModuleArg, innerPadding: PaddingValues, scrollBehavior: TopAppBarScrollBehavior, uriHandler: UriHandler, sourceUrl: String, ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), ) { if (module.authorsList.isNotEmpty()) { item { SegmentedColumn( title = stringResource(R.string.module_author), modifier = Modifier .padding(horizontal = 16.dp) .padding(bottom = 8.dp), content = module.authorsList.map { author -> { SegmentedListItem( headlineContent = { Text( text = author.name, fontSize = MaterialTheme.typography.bodyMedium.fontSize, lineHeight = MaterialTheme.typography.bodyMedium.lineHeight, fontFamily = MaterialTheme.typography.bodyMedium.fontFamily ) }, trailingContent = { FilledTonalButton( modifier = Modifier.defaultMinSize(52.dp, 32.dp), onClick = { uriHandler.openUri(author.link) }, contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Link, contentDescription = null ) } } ) } } ) } } if (sourceUrl.isNotEmpty()) { item { SegmentedColumn( title = stringResource(R.string.module_repos_source_code), modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf( { SegmentedListItem( headlineContent = { Text( text = sourceUrl, fontSize = MaterialTheme.typography.bodyMedium.fontSize, lineHeight = MaterialTheme.typography.bodyMedium.lineHeight, fontFamily = MaterialTheme.typography.bodyMedium.fontFamily ) }, trailingContent = { FilledTonalButton( modifier = Modifier.defaultMinSize(52.dp, 32.dp), onClick = { uriHandler.openUri(sourceUrl) }, contentPadding = ButtonDefaults.TextButtonContentPadding ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.Outlined.Link, contentDescription = null ) } } ) } ) ) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.modulerepo import android.annotation.SuppressLint import android.content.Context import android.net.Uri import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.EaseInOut import androidx.compose.animation.core.tween import androidx.compose.animation.expandVertically import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize 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.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.runtime.withFrameNanos import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLocale import androidx.compose.ui.platform.UriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.zIndex import com.kyant.capsule.ContinuousRoundedRectangle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.GithubMarkdown import me.weishu.kernelsu.ui.component.dialog.ConfirmDialogHandle import me.weishu.kernelsu.ui.component.dialog.rememberConfirmDialog import me.weishu.kernelsu.ui.component.miuix.SearchBox import me.weishu.kernelsu.ui.component.miuix.SearchPager import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.isInDarkTheme import me.weishu.kernelsu.ui.util.defaultHazeEffect import me.weishu.kernelsu.ui.util.download import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.CircularProgressIndicator import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.PullToRefresh import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.SmallTitle import top.yukonga.miuix.kmp.basic.TabRow import top.yukonga.miuix.kmp.basic.TabRowDefaults import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.FileDownloads import top.yukonga.miuix.kmp.icon.extended.HorizontalSplit import top.yukonga.miuix.kmp.icon.extended.Link import top.yukonga.miuix.kmp.icon.extended.MoreCircle import top.yukonga.miuix.kmp.icon.extended.TopDownloads import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic import java.text.Collator @SuppressLint("LocalContextGetResourceValueCall") @Composable fun ModuleRepoScreenMiuix( state: ModuleRepoUiState, actions: ModuleRepoActions, ) { val searchStatus = state.searchStatus val platformLocale = LocalLocale.current.platformLocale val metaBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f) val metaTint = colorScheme.onTertiaryContainer.copy(alpha = 0.8f) LaunchedEffect(searchStatus.searchText) { actions.onSearchTextChange(searchStatus.searchText) } val scrollBehavior = MiuixScrollBehavior() val dynamicTopPadding by remember { derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) } } val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) { TopAppBar( color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.module_repos), actions = { val showTopPopup = remember { mutableStateOf(false) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = { showTopPopup.value = false }, content = { ListPopupColumn { DropdownImpl( text = stringResource(R.string.module_repos_sort_name), optionSize = 1, isSelected = state.sortByName, onSelectedIndexChange = { actions.onToggleSortByName() showTopPopup.value = false }, index = 0 ) } }) IconButton( modifier = Modifier.padding(end = 16.dp), onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.MoreCircle, tint = colorScheme.onSurface, contentDescription = null, ) } }, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = actions.onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onSurface ) } }, scrollBehavior = scrollBehavior, ) } }, popupHost = { searchStatus.SearchPager( onSearchStatusChange = actions.onSearchStatusChange, defaultResult = {}, searchBarTopPadding = dynamicTopPadding, ) { LazyColumn( modifier = Modifier .fillMaxSize() .overScrollVertical(), ) { item { Spacer(Modifier.height(6.dp)) } val displaySearch = run { val base = state.searchResults val sortByName = state.sortByName val collator = Collator.getInstance(platformLocale) if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName }) } items(displaySearch, key = { it.moduleId }) { module -> Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), insideMargin = PaddingValues(16.dp), showIndication = true, pressFeedbackType = PressFeedbackType.Sink, onClick = { actions.onOpenRepoDetail(module) } ) { Column { if (module.moduleName.isNotEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = module.moduleName, fontSize = 17.sp, fontWeight = FontWeight(550), color = colorScheme.onSurface ) if (module.metamodule) { Text( text = "META", fontSize = 12.sp, color = metaTint, modifier = Modifier .padding(start = 6.dp) .clip(ContinuousRoundedRectangle(6.dp)) .background(metaBg) .padding(horizontal = 6.dp, vertical = 2.dp), fontWeight = FontWeight(750), maxLines = 1 ) } Spacer(Modifier.weight(1f)) if (module.stargazerCount > 0) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = MiuixIcons.TopDownloads, contentDescription = "stars", tint = colorScheme.onSurfaceVariantSummary, modifier = Modifier.size(16.dp) ) Text( text = module.stargazerCount.toString(), fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(start = 4.dp) ) } } } } if (module.moduleId.isNotEmpty()) { Text( text = "ID: ${module.moduleId}", fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, ) } Text( text = "${stringResource(id = R.string.module_author)}: ${module.authors}", fontSize = 12.sp, modifier = Modifier.padding(bottom = 1.dp), fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, ) if (module.summary.isNotEmpty()) { Text( text = module.summary, fontSize = 14.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(top = 2.dp), overflow = TextOverflow.Ellipsis, maxLines = 4, ) } } } } } } }, ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current val isLoading = state.modules.isEmpty() val offline = state.offline if (isLoading) { Box( modifier = Modifier .fillMaxSize(), contentAlignment = Alignment.Center ) { if (offline) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = stringResource(R.string.network_offline), color = colorScheme.onSurfaceVariantSummary, fontSize = 16.sp) Spacer(Modifier.height(12.dp)) TextButton( modifier = Modifier .padding(horizontal = 24.dp) .fillMaxWidth(), text = stringResource(R.string.network_retry), onClick = actions.onRefresh, ) } } else { InfiniteProgressIndicator() } } } else { searchStatus.SearchBox( onSearchStatusChange = actions.onSearchStatusChange, searchBarTopPadding = dynamicTopPadding, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), hazeState = hazeState, hazeStyle = hazeStyle ) { boxHeight -> val pullToRefreshState = rememberPullToRefreshState() val refreshTexts = listOf( stringResource(R.string.refresh_pulling), stringResource(R.string.refresh_release), stringResource(R.string.refresh_refresh), stringResource(R.string.refresh_complete), ) PullToRefresh( isRefreshing = state.isRefreshing, pullToRefreshState = pullToRefreshState, onRefresh = actions.onRefresh, refreshTexts = refreshTexts, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), ) { val displayModules = run { val base = state.modules val sortByName = state.sortByName val collator = Collator.getInstance(platformLocale) if (!sortByName) base else base.sortedWith(compareBy(collator) { it.moduleName }) } LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), overscrollEffect = null, ) { items( items = displayModules, key = { it.moduleId }, contentType = { "module" } ) { module -> val moduleAuthor = stringResource(id = R.string.module_author) Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), insideMargin = PaddingValues(16.dp), showIndication = true, onClick = { actions.onOpenRepoDetail(module) } ) { Column { if (module.moduleName.isNotEmpty()) { Row(verticalAlignment = Alignment.CenterVertically) { Text( text = module.moduleName, fontSize = 17.sp, fontWeight = FontWeight(550), color = colorScheme.onSurface ) if (module.metamodule) { Text( text = "META", fontSize = 12.sp, color = metaTint, modifier = Modifier .padding(start = 6.dp) .clip(ContinuousRoundedRectangle(6.dp)) .background(metaBg) .padding(horizontal = 6.dp, vertical = 2.dp), fontWeight = FontWeight(750), maxLines = 1 ) } } } if (module.moduleId.isNotEmpty()) { Text( text = "ID: ${module.moduleId}", fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, ) } Text( text = "$moduleAuthor: ${module.authors}", fontSize = 12.sp, modifier = Modifier.padding(bottom = 1.dp), fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, ) if (module.summary.isNotEmpty()) { Text( text = module.summary, fontSize = 14.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(top = 2.dp), overflow = TextOverflow.Ellipsis, maxLines = 4, ) } HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Row { if (module.stargazerCount > 0) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = MiuixIcons.TopDownloads, contentDescription = "stars", tint = colorScheme.onSurfaceVariantSummary, modifier = Modifier.size(16.dp) ) Text( text = module.stargazerCount.toString(), fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(start = 4.dp) ) } } Spacer(Modifier.weight(1f)) if (module.latestReleaseTime.isNotEmpty()) { Text( text = module.latestReleaseTime, fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, textAlign = TextAlign.End ) } } } } } } item { Spacer(Modifier.height(WindowInsets.systemBars.asPaddingValues().calculateBottomPadding())) } } } } } } } @Composable private fun ReadmePage( readmeHtml: String?, readmeLoaded: Boolean, innerPadding: PaddingValues, scrollBehavior: ScrollBehavior, hazeState: HazeState ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .hazeSource(state = hazeState), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), overscrollEffect = null, ) { item { var isLoading by remember { mutableStateOf(true) } if (isLoading) { Box( modifier = Modifier .fillMaxSize() .padding( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), contentAlignment = Alignment.Center ) { InfiniteProgressIndicator() } } var isReady by remember { mutableStateOf(false) } LaunchedEffect(Unit) { repeat(60) { withFrameNanos { } } isReady = true } AnimatedVisibility( visible = isReady && readmeLoaded && readmeHtml != null, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { Spacer(Modifier.height(6.dp)) Card( modifier = Modifier.padding(horizontal = 12.dp), ) { GithubMarkdown(content = readmeHtml!!, onLoadingChange = { isLoading = it }) } } } } item { Spacer(Modifier.height(12.dp)) } } } @SuppressLint("DefaultLocale") @Composable fun ReleasesPage( detailReleases: List, innerPadding: PaddingValues, scrollBehavior: ScrollBehavior, hazeState: HazeState, actionIconTint: Color, secondaryContainer: Color, confirmTitle: String, confirmDialog: ConfirmDialogHandle, scope: CoroutineScope, onInstallModule: (Uri) -> Unit, context: Context, setPendingDownload: ((() -> Unit)) -> Unit, ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .hazeSource(state = hazeState), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), overscrollEffect = null, ) { if (detailReleases.isNotEmpty()) { item { Spacer(Modifier.height(6.dp)) } items( items = detailReleases, key = { it.tagName }, contentType = { "release" } ) { rel -> val title = remember(rel.name, rel.tagName) { rel.name.ifBlank { rel.tagName } } Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp) ) { Column { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { Column( modifier = Modifier .padding(start = 16.dp, end = 16.dp, top = 16.dp) .weight(1f) ) { Text( text = title, fontSize = 17.sp, fontWeight = FontWeight(550), color = colorScheme.onSurface ) Text( text = rel.tagName, fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(top = 2.dp) ) } Text( text = rel.publishedAt, fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier .padding(start = 16.dp, end = 16.dp, top = 16.dp) .align(Alignment.Top) ) } AnimatedVisibility( visible = rel.assets.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Column { AnimatedVisibility( visible = rel.descriptionHTML.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Column { HorizontalDivider( modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = 4.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) GithubMarkdown(content = rel.descriptionHTML) } } HorizontalDivider( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) rel.assets.forEachIndexed { index, asset -> val fileName = asset.name stringResource(R.string.module_downloading) val sizeText = remember(asset.size) { val s = asset.size when { s >= 1024L * 1024L * 1024L -> String.format("%.1f GB", s / (1024f * 1024f * 1024f)) s >= 1024L * 1024L -> String.format("%.1f MB", s / (1024f * 1024f)) s >= 1024L -> String.format("%.0f KB", s / 1024f) else -> "$s B" } } val sizeAndDownloads = remember(sizeText, asset.downloadCount) { "$sizeText · ${asset.downloadCount} downloads" } var isDownloading by remember(fileName, asset.downloadUrl) { mutableStateOf(false) } var progress by remember(fileName, asset.downloadUrl) { mutableIntStateOf(0) } val onClickDownload = remember(fileName, asset.downloadUrl) { { val startText = context.getString(R.string.module_start_downloading, fileName) setPendingDownload { isDownloading = true scope.launch(Dispatchers.IO) { download( asset.downloadUrl, fileName, onDownloaded = onInstallModule, onDownloading = { isDownloading = true }, onProgress = { p -> scope.launch(Dispatchers.Main) { progress = p } } ) } } confirmDialog.showConfirm(title = confirmTitle, content = startText) } } val bottomPadding = if (index == rel.assets.lastIndex) 16.dp else 8.dp Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Column( modifier = Modifier .padding(start = 16.dp, end = 16.dp, bottom = bottomPadding) .weight(1f) ) { Text( text = fileName, fontSize = 14.sp, color = colorScheme.onSurface ) Text( text = sizeAndDownloads, fontSize = 12.sp, color = colorScheme.onSurfaceVariantSummary, modifier = Modifier.padding(top = 2.dp) ) } IconButton( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = bottomPadding), backgroundColor = secondaryContainer, minHeight = 35.dp, minWidth = 35.dp, enabled = !isDownloading, onClick = onClickDownload, ) { if (isDownloading) { CircularProgressIndicator( progress = progress / 100f, size = 20.dp, strokeWidth = 2.dp ) } else { Row( modifier = Modifier.padding(horizontal = 10.dp), verticalAlignment = Alignment.CenterVertically, ) { Icon( modifier = Modifier.size(20.dp), imageVector = MiuixIcons.FileDownloads, tint = actionIconTint, contentDescription = stringResource(R.string.install) ) Text( modifier = Modifier.padding(start = 4.dp, end = 2.dp), text = stringResource(R.string.install), color = actionIconTint, fontWeight = FontWeight.Medium, fontSize = 15.sp ) } } } } if (index != rel.assets.lastIndex) { HorizontalDivider( modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) } } } } } } } } } } @Composable fun InfoPage( module: RepoModuleArg, innerPadding: PaddingValues, scrollBehavior: ScrollBehavior, hazeState: HazeState, actionIconTint: Color, secondaryContainer: Color, uriHandler: UriHandler, sourceUrl: String, ) { val layoutDirection = LocalLayoutDirection.current LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .hazeSource(state = hazeState), contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), overscrollEffect = null, ) { if (module.authorsList.isNotEmpty()) { item { SmallTitle( text = stringResource(R.string.module_author), modifier = Modifier.padding(top = 6.dp) ) Card( modifier = Modifier .padding(horizontal = 12.dp), insideMargin = PaddingValues(16.dp) ) { Column { module.authorsList.forEachIndexed { index, author -> Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = author.name, fontSize = 14.sp, color = colorScheme.onSurface, modifier = Modifier.weight(1f) ) val clickable = author.link.isNotEmpty() val tint = if (clickable) actionIconTint else actionIconTint.copy(alpha = 0.35f) IconButton( backgroundColor = secondaryContainer, minHeight = 35.dp, minWidth = 35.dp, enabled = clickable, onClick = { if (clickable) { uriHandler.openUri(author.link) } }, ) { Icon( modifier = Modifier.size(20.dp), imageVector = MiuixIcons.Link, tint = tint, contentDescription = null ) } } if (index != module.authorsList.lastIndex) { HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) } } } } } } if (sourceUrl.isNotEmpty()) { item { SmallTitle( text = stringResource(R.string.module_repos_source_code), modifier = Modifier.padding(top = 6.dp) ) Card( modifier = Modifier .fillMaxWidth() .padding(horizontal = 12.dp) .padding(bottom = 12.dp), insideMargin = PaddingValues(16.dp) ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Text( text = sourceUrl, fontSize = 16.sp, color = colorScheme.onSurface, modifier = Modifier.weight(1f), maxLines = 1, overflow = TextOverflow.Ellipsis ) IconButton( backgroundColor = secondaryContainer, minHeight = 35.dp, minWidth = 35.dp, onClick = { uriHandler.openUri(sourceUrl) }, ) { Icon( modifier = Modifier.size(20.dp), imageVector = MiuixIcons.Link, tint = actionIconTint, contentDescription = null ) } } } } } item { Spacer(Modifier.height(12.dp)) } } } @SuppressLint("StringFormatInvalid", "DefaultLocale") @Composable fun ModuleRepoDetailScreenMiuix( state: ModuleRepoDetailUiState, actions: ModuleRepoDetailActions, ) { val context = LocalContext.current val enableBlur = LocalEnableBlur.current val actionIconTint = colorScheme.onSurface.copy(alpha = if (isInDarkTheme()) 0.7f else 0.9f) val secondaryContainer = colorScheme.secondaryContainer.copy(alpha = 0.8f) val module = state.module val scope = rememberCoroutineScope() val confirmTitle = stringResource(R.string.module_install) var pendingDownload by remember { mutableStateOf<(() -> Unit)?>(null) } val confirmDialog = rememberConfirmDialog(onConfirm = { pendingDownload?.invoke() }) val scrollBehavior = MiuixScrollBehavior() val hazeState = remember { HazeState() } val hazeStyle = HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) Scaffold( topBar = { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = module.moduleName, scrollBehavior = scrollBehavior, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = actions.onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onSurface ) } }, actions = { if (state.webUrl.isNotEmpty()) { IconButton( modifier = Modifier.padding(end = 16.dp), onClick = actions.onOpenWebUrl ) { Icon( imageVector = MiuixIcons.HorizontalSplit, contentDescription = null, tint = colorScheme.onBackground ) } } } ) }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal), ) { innerPadding -> val tabs = listOf( stringResource(R.string.tab_readme), stringResource(R.string.tab_releases), stringResource(R.string.tab_info) ) val pagerState = rememberPagerState(initialPage = 0, pageCount = { tabs.size }) val tabRowHeight by remember { mutableStateOf(40.dp) } var collapsedFraction by remember { mutableFloatStateOf(scrollBehavior.state.collapsedFraction) } LaunchedEffect(scrollBehavior.state.collapsedFraction) { snapshotFlow { scrollBehavior.state.collapsedFraction }.collectLatest { collapsedFraction = it } } val dynamicTopPadding by remember { derivedStateOf { 12.dp * (1f - collapsedFraction) } } val layoutDirection = LocalLayoutDirection.current val coroutineScope = rememberCoroutineScope() Box( modifier = Modifier.fillMaxSize() ) { Column( modifier = Modifier .then( if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else Modifier.background(colorScheme.surface), ) .zIndex(1f) .padding( top = innerPadding.calculateTopPadding() + dynamicTopPadding, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = 6.dp ) .padding(horizontal = 12.dp) ) { TabRow( tabs = tabs, selectedTabIndex = pagerState.currentPage, onTabSelected = { index -> coroutineScope.launch { pagerState.animateScrollToPage(page = index, animationSpec = tween(easing = EaseInOut)) } }, colors = TabRowDefaults.tabRowColors( backgroundColor = if (enableBlur) Color.Transparent else colorScheme.surface ), height = tabRowHeight, ) } HorizontalPager( state = pagerState, modifier = Modifier.fillMaxSize(), ) { page -> val innerPadding = PaddingValues( top = innerPadding.calculateTopPadding() + tabRowHeight + dynamicTopPadding + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding() + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding() ) when (page) { 0 -> ReadmePage( readmeHtml = state.readmeHtml, readmeLoaded = state.readmeLoaded, innerPadding = innerPadding, scrollBehavior = scrollBehavior, hazeState = hazeState ) 1 -> ReleasesPage( detailReleases = state.detailReleases, innerPadding = innerPadding, scrollBehavior = scrollBehavior, hazeState = hazeState, actionIconTint = actionIconTint, secondaryContainer = secondaryContainer, confirmTitle = confirmTitle, confirmDialog = confirmDialog, scope = scope, onInstallModule = actions.onInstallModule, context = context, setPendingDownload = { pendingDownload = it } ) 2 -> InfoPage( module = module, innerPadding = innerPadding, scrollBehavior = scrollBehavior, hazeState = hazeState, actionIconTint = actionIconTint, secondaryContainer = secondaryContainer, uriHandler = object : UriHandler { override fun openUri(uri: String) = actions.onOpenUrl(uri) }, sourceUrl = state.sourceUrl, ) } } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoModels.kt ================================================ package me.weishu.kernelsu.ui.screen.modulerepo import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class ReleaseAssetArg( val name: String, val downloadUrl: String, val size: Long, val downloadCount: Int ) : Parcelable @Parcelize data class ReleaseArg( val tagName: String, val name: String, val publishedAt: String, val assets: List, val descriptionHTML: String ) : Parcelable @Parcelize data class AuthorArg( val name: String, val link: String, ) : Parcelable @Parcelize data class RepoModuleArg( val moduleId: String, val moduleName: String, val authors: String, val authorsList: List, val latestRelease: String, val latestReleaseTime: String, val releases: List ) : Parcelable ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.modulerepo import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalUriHandler import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.util.module.fetchModuleDetail import me.weishu.kernelsu.ui.viewmodel.ModuleRepoViewModel import me.weishu.kernelsu.ui.viewmodel.ModuleViewModel @Composable fun ModuleRepoScreen() { val navigator = LocalNavigator.current val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() val installedVm = viewModel() val installedUiState by installedVm.uiState.collectAsState() LaunchedEffect(Unit) { if (uiState.modules.isEmpty()) { viewModel.refresh() } if (installedUiState.moduleList.isEmpty()) { installedVm.fetchModuleList() } } val actions = ModuleRepoActions( onBack = { navigator.pop() }, onRefresh = viewModel::refresh, onSearchTextChange = viewModel::updateSearchText, onClearSearch = { viewModel.updateSearchText("") }, onSearchStatusChange = viewModel::updateSearchStatus, onToggleSortByName = viewModel::toggleSortByName, onOpenRepoDetail = { module -> val args = RepoModuleArg( moduleId = module.moduleId, moduleName = module.moduleName, authors = module.authors, authorsList = module.authorList.map { AuthorArg(it.name, it.link) }, latestRelease = module.latestRelease, latestReleaseTime = module.latestReleaseTime, releases = emptyList() ) navigator.push(Route.ModuleRepoDetail(args)) }, ) when (LocalUiMode.current) { UiMode.Miuix -> ModuleRepoScreenMiuix(uiState, actions) UiMode.Material -> ModuleRepoScreenMaterial(uiState, actions) } } @Composable fun ModuleRepoDetailScreen(module: RepoModuleArg) { val navigator = LocalNavigator.current val uriHandler = LocalUriHandler.current var readmeHtml by remember(module.moduleId) { mutableStateOf(null) } var readmeLoaded by remember(module.moduleId) { mutableStateOf(false) } var detailReleases by remember(module.moduleId) { mutableStateOf>(emptyList()) } var webUrl by remember(module.moduleId) { mutableStateOf("https://modules.kernelsu.org/module/${module.moduleId}") } var sourceUrl by remember(module.moduleId) { mutableStateOf("https://github.com/KernelSU-Modules-Repo/${module.moduleId}") } LaunchedEffect(module.moduleId) { if (module.moduleId.isNotEmpty()) { withContext(Dispatchers.IO) { runCatching { val detail = fetchModuleDetail(module.moduleId) if (detail != null) { readmeHtml = detail.readmeHtml if (detail.sourceUrl.isNotEmpty()) sourceUrl = detail.sourceUrl detailReleases = detail.releases.map { r -> ReleaseArg( tagName = r.tagName, name = r.name, publishedAt = r.publishedAt, assets = r.assets.map { a -> ReleaseAssetArg(a.name, a.downloadUrl, a.size, a.downloadCount) }, descriptionHTML = r.descriptionHTML ) } } else { detailReleases = emptyList() } }.onSuccess { readmeLoaded = true }.onFailure { readmeLoaded = true detailReleases = emptyList() } } } else { readmeLoaded = true } } val state = ModuleRepoDetailUiState( module = module, readmeHtml = readmeHtml, readmeLoaded = readmeLoaded, detailReleases = detailReleases, webUrl = webUrl, sourceUrl = sourceUrl, ) val actions = ModuleRepoDetailActions( onBack = { navigator.pop() }, onOpenWebUrl = { if (webUrl.isNotEmpty()) uriHandler.openUri(webUrl) }, onOpenUrl = uriHandler::openUri, onInstallModule = { uri -> navigator.push(Route.Flash(FlashIt.FlashModules(listOf(uri)))) }, ) when (LocalUiMode.current) { UiMode.Miuix -> ModuleRepoDetailScreenMiuix(state, actions) UiMode.Material -> ModuleRepoDetailScreenMaterial(state, actions) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/modulerepo/ModuleRepoUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.modulerepo import androidx.compose.runtime.Immutable import me.weishu.kernelsu.data.model.RepoModule import me.weishu.kernelsu.ui.component.SearchStatus data class ModuleRepoUiState( val isRefreshing: Boolean = false, val sortByName: Boolean = false, val offline: Boolean = false, val modules: List = emptyList(), val searchStatus: SearchStatus = SearchStatus(""), val searchResults: List = emptyList(), val error: Throwable? = null ) @Immutable data class ModuleRepoActions( val onBack: () -> Unit, val onRefresh: () -> Unit, val onSearchTextChange: (String) -> Unit, val onClearSearch: () -> Unit, val onSearchStatusChange: (SearchStatus) -> Unit, val onToggleSortByName: () -> Unit, val onOpenRepoDetail: (RepoModule) -> Unit, ) @Immutable data class ModuleRepoDetailUiState( val module: RepoModuleArg, val readmeHtml: String?, val readmeLoaded: Boolean, val detailReleases: List, val webUrl: String, val sourceUrl: String, ) @Immutable data class ModuleRepoDetailActions( val onBack: () -> Unit, val onOpenWebUrl: () -> Unit, val onOpenUrl: (String) -> Unit, val onInstallModule: (android.net.Uri) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.settings import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight import androidx.compose.material.icons.filled.BugReport import androidx.compose.material.icons.filled.ContactPage import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.DeveloperMode import androidx.compose.material.icons.filled.ElectricalServices import androidx.compose.material.icons.filled.Fence import androidx.compose.material.icons.filled.FolderDelete import androidx.compose.material.icons.filled.Palette import androidx.compose.material.icons.filled.RemoveCircle import androidx.compose.material.icons.filled.RemoveModerator import androidx.compose.material.icons.filled.Update import androidx.compose.material.icons.rounded.Dashboard import androidx.compose.material.icons.rounded.UploadFile import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.SnackbarHost import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf 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.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.component.KsuIsValid import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedDropdownItem import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.material.SegmentedSwitchItem import me.weishu.kernelsu.ui.component.material.SendLogBottomSheet import me.weishu.kernelsu.ui.component.uninstalldialog.UninstallDialog import me.weishu.kernelsu.ui.util.LocalSnackbarHost /** * @author weishu * @date 2023/1/1. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingPagerMaterial( uiState: SettingsUiState, actions: SettingsScreenActions, bottomInnerPadding: Dp, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val snackBarHost = LocalSnackbarHost.current val showUninstallDialog = rememberSaveable { mutableStateOf(false) } var showBottomsheet by remember { mutableStateOf(false) } UninstallDialog( show = showUninstallDialog.value, onDismissRequest = { showUninstallDialog.value = false } ) Scaffold( topBar = { TopBar( scrollBehavior = scrollBehavior ) }, snackbarHost = { SnackbarHost(snackBarHost) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { paddingValues -> Column( modifier = Modifier .padding(paddingValues) .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) ) { Spacer(modifier = Modifier.height(8.dp)) KsuIsValid { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf( { SegmentedSwitchItem( icon = Icons.Filled.Update, title = stringResource(id = R.string.settings_check_update), summary = stringResource(id = R.string.settings_check_update_summary), checked = uiState.checkUpdate, onCheckedChange = actions.onSetCheckUpdate ) }, { SegmentedSwitchItem( icon = Icons.Rounded.UploadFile, title = stringResource(id = R.string.settings_module_check_update), summary = stringResource(id = R.string.settings_check_update_summary), checked = uiState.checkModuleUpdate, onCheckedChange = actions.onSetCheckModuleUpdate ) } ) ) } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = buildList { add { SegmentedDropdownItem( icon = Icons.Rounded.Dashboard, title = stringResource(id = R.string.settings_ui_mode), summary = stringResource(id = R.string.settings_ui_mode_summary), items = UiMode.entries.map { it.name }, selectedIndex = if (uiState.uiMode == UiMode.Material.value) 1 else 0, onItemSelected = actions.onSetUiModeIndex ) } add { SegmentedListItem( onClick = actions.onOpenTheme, headlineContent = { Text(stringResource(id = R.string.settings_theme)) }, supportingContent = { Text(stringResource(id = R.string.settings_theme_summary)) }, leadingContent = { Icon(Icons.Filled.Palette, stringResource(id = R.string.settings_theme)) }, trailingContent = { Icon( Icons.AutoMirrored.Filled.KeyboardArrowRight, null ) } ) } } ) val profileTemplate = stringResource(id = R.string.settings_profile_template) KsuIsValid { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf { SegmentedListItem( onClick = actions.onOpenProfileTemplate, headlineContent = { Text(profileTemplate) }, supportingContent = { Text(stringResource(id = R.string.settings_profile_template_summary)) }, leadingContent = { Icon(Icons.Filled.Fence, profileTemplate) }, trailingContent = { Icon( Icons.AutoMirrored.Filled.KeyboardArrowRight, null ) } ) } ) } KsuIsValid { val suCompatModeItems = listOf( stringResource(id = R.string.settings_mode_enable_by_default), stringResource(id = R.string.settings_mode_disable_until_reboot), stringResource(id = R.string.settings_mode_disable_always), ) SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf( { val suSummary = when (uiState.suCompatStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) "managed" -> stringResource(id = R.string.feature_status_managed_summary) else -> stringResource(id = R.string.settings_sucompat_summary) } SegmentedDropdownItem( icon = Icons.Filled.RemoveModerator, title = stringResource(id = R.string.settings_sucompat), summary = suSummary, items = suCompatModeItems, enabled = uiState.suCompatStatus == "supported", selectedIndex = uiState.suCompatMode, onItemSelected = actions.onSetSuCompatMode ) }, { val umountSummary = when (uiState.kernelUmountStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) "managed" -> stringResource(id = R.string.feature_status_managed_summary) else -> stringResource(id = R.string.settings_kernel_umount_summary) } SegmentedSwitchItem( icon = Icons.Filled.RemoveCircle, title = stringResource(id = R.string.settings_kernel_umount), summary = umountSummary, enabled = uiState.kernelUmountStatus == "supported", checked = uiState.isKernelUmountEnabled, onCheckedChange = actions.onSetKernelUmountEnabled ) }, { SegmentedSwitchItem( icon = Icons.Filled.FolderDelete, title = stringResource(id = R.string.settings_umount_modules_default), summary = stringResource(id = R.string.settings_umount_modules_default_summary), checked = uiState.isDefaultUmountModules, onCheckedChange = actions.onSetDefaultUmountModules ) }, { SegmentedSwitchItem( icon = Icons.Filled.DeveloperMode, title = stringResource(id = R.string.enable_web_debugging), summary = stringResource(id = R.string.enable_web_debugging_summary), checked = uiState.enableWebDebugging, onCheckedChange = actions.onSetEnableWebDebugging ) }, { SegmentedSwitchItem( icon = Icons.Filled.ElectricalServices, title = stringResource(id = R.string.settings_auto_jailbreak), summary = stringResource(id = R.string.settings_auto_jailbreak_summary), enabled = uiState.isLateLoadMode, checked = uiState.autoJailbreak, onCheckedChange = actions.onSetAutoJailbreak ) } ) ) } if (uiState.isLkmMode) { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf( { val uninstall = stringResource(id = R.string.settings_uninstall) SegmentedListItem( onClick = { showUninstallDialog.value = true }, enabled = !uiState.isLateLoadMode, headlineContent = { Text(uninstall) }, leadingContent = { Icon(Icons.Filled.Delete, uninstall) } ) } ) ) } SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = listOf( { SegmentedListItem( onClick = { showBottomsheet = true }, headlineContent = { Text(stringResource(id = R.string.send_log)) }, leadingContent = { Icon( Icons.Filled.BugReport, stringResource(id = R.string.send_log) ) }, ) }, { SegmentedListItem( onClick = actions.onOpenAbout, headlineContent = { Text(stringResource(id = R.string.about)) }, leadingContent = { Icon( Icons.Filled.ContactPage, stringResource(id = R.string.about) ) }, ) } ) ) Spacer(modifier = Modifier.height(8.dp)) if (showBottomsheet) { SendLogBottomSheet { showBottomsheet = false } } Spacer(modifier = Modifier.height(bottomInnerPadding)) } } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( scrollBehavior: TopAppBarScrollBehavior? = null ) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.settings)) }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.settings import androidx.compose.foundation.layout.Spacer 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.fillMaxHeight 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.BugReport import androidx.compose.material.icons.rounded.ContactPage import androidx.compose.material.icons.rounded.Dashboard import androidx.compose.material.icons.rounded.Delete import androidx.compose.material.icons.rounded.DeveloperMode import androidx.compose.material.icons.rounded.ElectricalServices 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.Update import androidx.compose.material.icons.rounded.UploadFile import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.component.KsuIsValid import me.weishu.kernelsu.ui.component.dialog.rememberLoadingDialog import me.weishu.kernelsu.ui.component.miuix.SendLogDialog import me.weishu.kernelsu.ui.component.uninstalldialog.UninstallDialog import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect 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.TopAppBar import top.yukonga.miuix.kmp.extra.SuperArrow import top.yukonga.miuix.kmp.extra.SuperDropdown import top.yukonga.miuix.kmp.extra.SuperSwitch import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/1/1. */ @Composable fun SettingPagerMiuix( uiState: SettingsUiState, actions: SettingsScreenActions, bottomInnerPadding: Dp ) { val scrollBehavior = MiuixScrollBehavior() val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } val loadingDialog = rememberLoadingDialog() val showUninstallDialog = rememberSaveable { mutableStateOf(false) } val showSendLogDialog = rememberSaveable { mutableStateOf(false) } Scaffold( topBar = { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.settings), scrollBehavior = scrollBehavior ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding(horizontal = 12.dp), contentPadding = innerPadding, overscrollEffect = null, ) { item { Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { SuperSwitch( title = stringResource(id = R.string.settings_check_update), summary = stringResource(id = R.string.settings_check_update_summary), startAction = { Icon( Icons.Rounded.Update, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_check_update), tint = colorScheme.onBackground ) }, checked = uiState.checkUpdate, onCheckedChange = actions.onSetCheckUpdate ) KsuIsValid { SuperSwitch( title = stringResource(id = R.string.settings_module_check_update), summary = stringResource(id = R.string.settings_check_update_summary), startAction = { Icon( Icons.Rounded.UploadFile, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_check_update), tint = colorScheme.onBackground ) }, checked = uiState.checkModuleUpdate, onCheckedChange = actions.onSetCheckModuleUpdate ) } } Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { SuperDropdown( title = stringResource(id = R.string.settings_ui_mode), summary = stringResource(id = R.string.settings_ui_mode_summary), items = UiMode.entries.map { it.name }, startAction = { Icon( Icons.Rounded.Dashboard, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_ui_mode), tint = colorScheme.onBackground ) }, selectedIndex = if (uiState.uiMode == UiMode.Material.value) 1 else 0, onSelectedIndexChange = actions.onSetUiModeIndex ) SuperArrow( title = stringResource(id = R.string.settings_theme), summary = stringResource(id = R.string.settings_theme_summary), startAction = { Icon( Icons.Rounded.Palette, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_theme), tint = colorScheme.onBackground ) }, onClick = actions.onOpenTheme ) } KsuIsValid { Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { val profileTemplate = stringResource(id = R.string.settings_profile_template) SuperArrow( title = profileTemplate, summary = stringResource(id = R.string.settings_profile_template_summary), startAction = { Icon( Icons.Rounded.Fence, modifier = Modifier.padding(end = 6.dp), contentDescription = profileTemplate, tint = colorScheme.onBackground ) }, onClick = actions.onOpenProfileTemplate ) } } KsuIsValid { Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { val suCompatModeItems = listOf( stringResource(id = R.string.settings_mode_enable_by_default), stringResource(id = R.string.settings_mode_disable_until_reboot), stringResource(id = R.string.settings_mode_disable_always), ) val suSummary = when (uiState.suCompatStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) "managed" -> stringResource(id = R.string.feature_status_managed_summary) else -> stringResource(id = R.string.settings_sucompat_summary) } SuperDropdown( title = stringResource(id = R.string.settings_sucompat), summary = suSummary, items = suCompatModeItems, startAction = { Icon( Icons.Rounded.RemoveModerator, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_sucompat), tint = colorScheme.onBackground ) }, enabled = uiState.suCompatStatus == "supported", selectedIndex = uiState.suCompatMode, onSelectedIndexChange = actions.onSetSuCompatMode ) val umountSummary = when (uiState.kernelUmountStatus) { "unsupported" -> stringResource(id = R.string.feature_status_unsupported_summary) "managed" -> stringResource(id = R.string.feature_status_managed_summary) else -> stringResource(id = R.string.settings_kernel_umount_summary) } SuperSwitch( title = stringResource(id = R.string.settings_kernel_umount), summary = umountSummary, startAction = { Icon( Icons.Rounded.RemoveCircle, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_kernel_umount), tint = colorScheme.onBackground ) }, enabled = uiState.kernelUmountStatus == "supported", checked = uiState.isKernelUmountEnabled, onCheckedChange = actions.onSetKernelUmountEnabled ) SuperSwitch( title = stringResource(id = R.string.settings_umount_modules_default), summary = stringResource(id = R.string.settings_umount_modules_default_summary), startAction = { Icon( Icons.Rounded.FolderDelete, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_umount_modules_default), tint = colorScheme.onBackground ) }, checked = uiState.isDefaultUmountModules, onCheckedChange = actions.onSetDefaultUmountModules ) SuperSwitch( title = stringResource(id = R.string.enable_web_debugging), summary = stringResource(id = R.string.enable_web_debugging_summary), startAction = { Icon( Icons.Rounded.DeveloperMode, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.enable_web_debugging), tint = colorScheme.onBackground ) }, checked = uiState.enableWebDebugging, onCheckedChange = actions.onSetEnableWebDebugging ) SuperSwitch( title = stringResource(id = R.string.settings_auto_jailbreak), summary = stringResource(id = R.string.settings_auto_jailbreak_summary), startAction = { Icon( Icons.Rounded.ElectricalServices, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.settings_auto_jailbreak), tint = colorScheme.onBackground ) }, enabled = uiState.isLateLoadMode, checked = uiState.autoJailbreak, onCheckedChange = actions.onSetAutoJailbreak ) } } if (uiState.isLkmMode) { Card( modifier = Modifier .padding(top = 12.dp) .fillMaxWidth(), ) { val uninstall = stringResource(id = R.string.settings_uninstall) SuperArrow( title = uninstall, enabled = !uiState.isLateLoadMode, startAction = { Icon( Icons.Rounded.Delete, modifier = Modifier.padding(end = 6.dp), contentDescription = uninstall, tint = colorScheme.onBackground, ) }, onClick = { showUninstallDialog.value = true } ) UninstallDialog( show = showUninstallDialog.value, onDismissRequest = { showUninstallDialog.value = false } ) } } Card( modifier = Modifier .padding(vertical = 12.dp) .fillMaxWidth(), ) { SuperArrow( title = stringResource(id = R.string.send_log), startAction = { Icon( Icons.Rounded.BugReport, modifier = Modifier.padding(end = 6.dp), contentDescription = stringResource(id = R.string.send_log), tint = colorScheme.onBackground ) }, onClick = { showSendLogDialog.value = true }, ) SendLogDialog( show = showSendLogDialog.value, onDismissRequest = { showSendLogDialog.value = false }, loadingDialog = loadingDialog ) val about = stringResource(id = R.string.about) SuperArrow( title = about, startAction = { Icon( Icons.Rounded.ContactPage, modifier = Modifier.padding(end = 6.dp), contentDescription = about, tint = colorScheme.onBackground ) }, onClick = actions.onOpenAbout ) } Spacer(Modifier.height(bottomInnerPadding)) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.settings import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.unit.Dp import androidx.lifecycle.viewmodel.compose.viewModel import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.Navigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.viewmodel.SettingsViewModel @Composable fun SettingPager( navigator: Navigator, bottomInnerPadding: Dp ) { val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() val actions = SettingsScreenActions( onSetCheckUpdate = viewModel::setCheckUpdate, onSetCheckModuleUpdate = viewModel::setCheckModuleUpdate, onOpenTheme = { navigator.push(Route.ColorPalette) }, onSetUiModeIndex = { index -> viewModel.setUiMode(if (index == 0) UiMode.Miuix.value else UiMode.Material.value) }, onOpenProfileTemplate = { navigator.push(Route.AppProfileTemplate) }, onSetSuCompatMode = viewModel::setSuCompatMode, onSetKernelUmountEnabled = viewModel::setKernelUmountEnabled, onSetDefaultUmountModules = viewModel::setDefaultUmountModules, onSetEnableWebDebugging = viewModel::setEnableWebDebugging, onSetAutoJailbreak = viewModel::setAutoJailbreak, onOpenAbout = { navigator.push(Route.About) }, ) when (LocalUiMode.current) { UiMode.Miuix -> SettingPagerMiuix(uiState, actions, bottomInnerPadding) UiMode.Material -> SettingPagerMaterial(uiState, actions, bottomInnerPadding) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/settings/SettingsUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.settings import androidx.compose.runtime.Immutable import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.ui.UiMode data class SettingsUiState( val uiMode: String = UiMode.DEFAULT_VALUE, val checkUpdate: Boolean = true, val checkModuleUpdate: Boolean = true, val themeMode: Int = 0, val miuixMonet: Boolean = false, val keyColor: Int = 0, val colorStyle: String = PaletteStyle.TonalSpot.name, val colorSpec: String = ColorSpec.SpecVersion.Default.name, val enablePredictiveBack: Boolean = false, val enableBlur: Boolean = true, val enableFloatingBottomBar: Boolean = false, val enableFloatingBottomBarBlur: Boolean = false, val pageScale: Float = 1.0f, val enableWebDebugging: Boolean = false, // Su Compat val suCompatStatus: String = "", val suCompatMode: Int = 0, // 0: enable default, 1: disable until reboot, 2: disable always val isSuEnabled: Boolean = false, // Kernel Umount val kernelUmountStatus: String = "", val isKernelUmountEnabled: Boolean = false, // Umount Modules val isDefaultUmountModules: Boolean = false, val isLkmMode: Boolean = false, val isLateLoadMode: Boolean = false, // Auto Jailbreak val autoJailbreak: Boolean = false ) @Immutable data class SettingsScreenActions( val onSetCheckUpdate: (Boolean) -> Unit, val onSetCheckModuleUpdate: (Boolean) -> Unit, val onOpenTheme: () -> Unit, val onSetUiModeIndex: (Int) -> Unit, val onOpenProfileTemplate: () -> Unit, val onSetSuCompatMode: (Int) -> Unit, val onSetKernelUmountEnabled: (Boolean) -> Unit, val onSetDefaultUmountModules: (Boolean) -> Unit, val onSetEnableWebDebugging: (Boolean) -> Unit, val onSetAutoJailbreak: (Boolean) -> Unit, val onOpenAbout: () -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.superuser import android.content.pm.ApplicationInfo import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.LinearOutSlowInEasing 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.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Remove import androidx.compose.material3.Checkbox import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.AppInfo import me.weishu.kernelsu.ui.component.AppIconImage import me.weishu.kernelsu.ui.component.material.SearchAppBar import me.weishu.kernelsu.ui.component.material.SegmentedLazyColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.util.ownerNameForUid @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun SuperUserPagerMaterial( uiState: SuperUserUiState, actions: SuperUserActions, bottomInnerPadding: Dp, ) { val scope = rememberCoroutineScope() val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val listState = rememberLazyListState() val searchListState = rememberLazyListState() val pullToRefreshState = rememberPullToRefreshState() val scaleFraction = { if (uiState.isRefreshing) 1f else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f) } var localSearchText by remember { mutableStateOf(uiState.searchStatus.searchText) } LaunchedEffect(uiState.searchStatus.searchText) { localSearchText = uiState.searchStatus.searchText } Scaffold( modifier = Modifier .nestedScroll(scrollBehavior.nestedScrollConnection) .pullToRefresh( state = pullToRefreshState, isRefreshing = uiState.isRefreshing, onRefresh = actions.onRefresh, ), topBar = { SearchAppBar( title = { Text(stringResource(R.string.superuser)) }, searchText = localSearchText, onSearchTextChange = { localSearchText = it actions.onSearchTextChange(it) scope.launch { listState.scrollToItem(0) } }, onClearClick = { localSearchText = "" actions.onClearSearch() }, actions = { var showDropdown by remember { mutableStateOf(false) } IconButton(onClick = { showDropdown = true }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings) ) DropdownMenu( expanded = showDropdown, onDismissRequest = { showDropdown = false } ) { DropdownMenuItem( text = { Text(stringResource(R.string.show_system_apps)) }, trailingIcon = { Checkbox(uiState.showSystemApps, null) }, onClick = { actions.onToggleShowSystemApps() showDropdown = false } ) if (uiState.userIds.size > 1) { DropdownMenuItem( text = { Text(stringResource(R.string.show_only_primary_user_apps)) }, trailingIcon = { Checkbox(uiState.showOnlyPrimaryUserApps, null) }, onClick = { actions.onToggleShowOnlyPrimaryUserApps() showDropdown = false } ) } } } }, scrollBehavior = scrollBehavior, searchContent = { bottomPadding, closeSearch -> LaunchedEffect(localSearchText) { searchListState.scrollToItem(0) } SegmentedLazyColumn( state = searchListState, modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp + bottomPadding ), key = { it.uid }, items = uiState.searchResults, ) { group -> Column { GroupItem( group = group, selected = false, onToggleExpand = {}, ) { closeSearch() actions.onOpenProfile(group) } AnimatedVisibility( visible = group.apps.size > 1, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { group.apps.forEach { app -> SimpleAppItem( app = app, matched = group.matchedPackageNames.contains(app.packageName), ) { closeSearch() actions.onOpenProfile(group) } } } } } } } ) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { innerPadding -> Box(modifier = Modifier.padding(innerPadding)) { val expandedSearchUids = remember { mutableStateOf(setOf()) } SegmentedLazyColumn( state = listState, modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), contentPadding = PaddingValues( start = 16.dp, end = 16.dp, top = 8.dp, bottom = 16.dp + bottomInnerPadding ), key = { it.uid }, items = uiState.groupedApps, ) { group -> val expanded = expandedSearchUids.value.contains(group.uid) val onToggleExpand = { if (group.apps.size > 1) { expandedSearchUids.value = if (expandedSearchUids.value.contains(group.uid)) { expandedSearchUids.value - group.uid } else { expandedSearchUids.value + group.uid } } } Column { GroupItem( group = group, selected = expanded, onToggleExpand = onToggleExpand, ) { actions.onOpenProfile(group) } AnimatedVisibility( visible = expanded && group.apps.size > 1, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { group.apps.forEach { app -> SimpleAppItem(app = app) { actions.onOpenProfile(group) } } } } } } Box( modifier = Modifier .align(Alignment.TopCenter) .graphicsLayer { scaleX = scaleFraction() scaleY = scaleFraction() } ) { PullToRefreshDefaults.LoadingIndicator( state = pullToRefreshState, isRefreshing = uiState.isRefreshing ) } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun SimpleAppItem( app: AppInfo, matched: Boolean = false, onNavigate: () -> Unit, ) { ListItem( onClick = onNavigate, modifier = Modifier.padding(horizontal = 4.dp), shapes = ListItemDefaults.shapes(shape = RoundedCornerShape(0.dp)), colors = ListItemDefaults.colors( containerColor = if (matched) { MaterialTheme.colorScheme.secondaryContainer } else { MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp) } ), content = { Text(app.label, overflow = TextOverflow.Ellipsis, maxLines = 1) }, supportingContent = { Text(app.packageName, overflow = TextOverflow.Ellipsis, maxLines = 1) }, leadingContent = { AppIconImage( packageInfo = app.packageInfo, label = app.label, modifier = Modifier .size(40.dp) .padding(start = 4.dp) ) }, trailingContent = { Icon( Icons.Filled.Remove, contentDescription = null, modifier = Modifier.padding(end = 4.dp) ) } ) } @Composable private fun GroupItem( group: GroupedApps, selected: Boolean, onToggleExpand: () -> Unit, onClickPrimary: () -> Unit, ) { val summaryText = if (group.apps.size > 1) { stringResource(R.string.group_contains_apps, group.apps.size) } else { group.primary.packageName } SegmentedListItem( selected = selected, onClick = onClickPrimary, onLongClick = if (group.apps.size > 1) onToggleExpand else null, headlineContent = { Text( text = if (group.apps.size > 1) ownerNameForUid(group.uid) else group.primary.label, overflow = TextOverflow.Ellipsis, maxLines = 1 ) }, supportingContent = { Column { Text( text = summaryText, color = MaterialTheme.colorScheme.outline, overflow = TextOverflow.Ellipsis, maxLines = 1 ) FlowRow { val userId = group.uid / 100000 val packageInfo = group.primary.packageInfo val applicationInfo = packageInfo.applicationInfo if (group.anyAllowSu) { StatusTag( label = "ROOT", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } else if (group.shouldUmount) { StatusTag( label = "UMOUNT", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onSecondary, backgroundColor = MaterialTheme.colorScheme.secondary ) } if (group.anyCustom) { StatusTag( label = "CUSTOM", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onSecondaryContainer, backgroundColor = MaterialTheme.colorScheme.secondaryContainer ) } if (userId != 0) { StatusTag( label = "USER $userId", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) } if (applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0 || applicationInfo.flags.and(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 ) { StatusTag( label = "SYSTEM", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) } if (!packageInfo.sharedUserId.isNullOrEmpty()) { StatusTag( label = "SHARED UID", modifier = Modifier.padding(top = 4.dp), contentColor = MaterialTheme.colorScheme.onTertiary, backgroundColor = MaterialTheme.colorScheme.tertiary ) } } } }, leadingContent = { AppIconImage( packageInfo = group.primary.packageInfo, label = group.primary.label, modifier = Modifier.size(48.dp) ) }, ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.superuser import android.content.pm.ApplicationInfo 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.Image import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.ime import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.runtime.Immutable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.kyant.capsule.ContinuousRoundedRectangle import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.AppInfo import me.weishu.kernelsu.ui.component.AppIconImage import me.weishu.kernelsu.ui.component.miuix.SearchBox import me.weishu.kernelsu.ui.component.miuix.SearchPager import me.weishu.kernelsu.ui.component.statustag.StatusTag import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.theme.isInDarkTheme import me.weishu.kernelsu.ui.util.ownerNameForUid import top.yukonga.miuix.kmp.basic.BasicComponent import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.DropdownImpl import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.PullToRefresh import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.basic.ArrowRight import top.yukonga.miuix.kmp.icon.extended.MoreCircle import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic @Composable fun SuperUserPagerMiuix( uiState: SuperUserUiState, actions: SuperUserActions, bottomInnerPadding: Dp, ) { val searchStatus = uiState.searchStatus val enableBlur = LocalEnableBlur.current val scrollBehavior = MiuixScrollBehavior() val dynamicTopPadding by remember { derivedStateOf { 12.dp * (1f - scrollBehavior.state.collapsedFraction) } } val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { searchStatus.TopAppBarAnim(hazeState = hazeState, hazeStyle = hazeStyle) { TopAppBar( color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.superuser), actions = { val showTopPopup = remember { mutableStateOf(false) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = { showTopPopup.value = false }, content = { val isMultiUser = uiState.userIds.size > 1 val size = if (isMultiUser) 2 else 1 ListPopupColumn { DropdownImpl( text = stringResource(R.string.show_system_apps), isSelected = uiState.showSystemApps, optionSize = size, onSelectedIndexChange = { actions.onToggleShowSystemApps() showTopPopup.value = false }, index = 0 ) if (isMultiUser) { DropdownImpl( text = stringResource(R.string.show_only_primary_user_apps), isSelected = uiState.showOnlyPrimaryUserApps, optionSize = size, onSelectedIndexChange = { actions.onToggleShowOnlyPrimaryUserApps() showTopPopup.value = false }, index = 1 ) } } } ) IconButton( modifier = Modifier.padding(end = 16.dp), onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.MoreCircle, tint = colorScheme.onSurface, contentDescription = null ) } }, scrollBehavior = scrollBehavior ) } }, popupHost = { val expandedSearchUids = remember { mutableStateOf(setOf()) } LaunchedEffect(uiState.searchResults) { expandedSearchUids.value = uiState.searchResults .filter { it.apps.size > 1 } .map { it.uid } .toSet() } searchStatus.SearchPager( onSearchStatusChange = actions.onSearchStatusChange, defaultResult = {}, searchBarTopPadding = dynamicTopPadding, ) { val imeBottomPadding = WindowInsets.ime.asPaddingValues().calculateBottomPadding() LazyColumn( modifier = Modifier .fillMaxSize() .overScrollVertical(), ) { item { Spacer(Modifier.height(6.dp)) } items(uiState.searchResults, key = { it.uid }) { group -> val expanded = expandedSearchUids.value.contains(group.uid) AnimatedVisibility( visible = uiState.searchResults.isNotEmpty(), enter = fadeIn() + expandVertically(), exit = fadeOut() + shrinkVertically() ) { Column { GroupItem( group = group, onToggleExpand = { if (group.apps.size > 1) { expandedSearchUids.value = if (expanded) expandedSearchUids.value - group.uid else expandedSearchUids.value + group.uid } }, ) { actions.onOpenProfile(group) } AnimatedVisibility( visible = expanded && group.apps.size > 1, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { group.apps.forEach { app -> SimpleAppItem( app = app, matched = group.matchedPackageNames.contains(app.packageName), ) } Spacer(Modifier.height(6.dp)) } } } } } item { Spacer(Modifier.height(maxOf(bottomInnerPadding, imeBottomPadding))) } } } }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val layoutDirection = LocalLayoutDirection.current searchStatus.SearchBox( onSearchStatusChange = actions.onSearchStatusChange, searchBarTopPadding = dynamicTopPadding, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), hazeState = hazeState, hazeStyle = hazeStyle ) { boxHeight -> val pullToRefreshState = rememberPullToRefreshState() val refreshTexts = listOf( stringResource(R.string.refresh_pulling), stringResource(R.string.refresh_release), stringResource(R.string.refresh_refresh), stringResource(R.string.refresh_complete), ) if (uiState.groupedApps.isEmpty() && uiState.isRefreshing) { Box( modifier = Modifier .fillMaxSize() .padding( top = innerPadding.calculateTopPadding(), start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = bottomInnerPadding ), contentAlignment = Alignment.Center ) { InfiniteProgressIndicator() } } else { val expandedUids = remember { mutableStateOf(setOf()) } PullToRefresh( isRefreshing = uiState.isRefreshing, pullToRefreshState = pullToRefreshState, onRefresh = actions.onRefresh, refreshTexts = refreshTexts, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), ) { LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + boxHeight.value + 6.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), overscrollEffect = null, ) { items(uiState.groupedApps, key = { it.uid }) { group -> val expanded = expandedUids.value.contains(group.uid) AnimatedVisibility( visible = true, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { GroupItem( group = group, onToggleExpand = { if (group.apps.size > 1) { expandedUids.value = if (expanded) expandedUids.value - group.uid else expandedUids.value + group.uid } } ) { actions.onOpenProfile(group) } AnimatedVisibility( visible = expanded && group.apps.size > 1, enter = expandVertically() + fadeIn(), exit = shrinkVertically() + fadeOut() ) { Column { group.apps.forEach { app -> SimpleAppItem(app = app) } Spacer(Modifier.height(6.dp)) } } } } } item { Spacer(Modifier.height(bottomInnerPadding)) } } } } } } } @Composable private fun SimpleAppItem( app: AppInfo, matched: Boolean = false, ) { Row { Box( modifier = Modifier .padding(start = 12.dp) .width(6.dp) .height(24.dp) .align(Alignment.CenterVertically) .clip(ContinuousRoundedRectangle(16.dp)) .background(if (matched) colorScheme.primary else colorScheme.primaryContainer) ) Card( modifier = Modifier .padding(start = 6.dp, end = 12.dp, bottom = 6.dp) ) { BasicComponent( title = app.label, summary = app.packageName, startAction = { AppIconImage( packageInfo = app.packageInfo, label = app.label, modifier = Modifier .padding(end = 9.dp) .size(40.dp) ) }, insideMargin = PaddingValues(horizontal = 9.dp) ) } } } @Composable private fun GroupItem( group: GroupedApps, onToggleExpand: () -> Unit, onClickPrimary: () -> Unit, ) { val isInDarkTheme = isInDarkTheme() val bg = colorScheme.secondaryContainer.copy(alpha = 0.8f) val rootBg = colorScheme.tertiaryContainer.copy(alpha = 0.6f) val unmountBg = if (isInDarkTheme) Color.White.copy(alpha = 0.4f) else Color.Black.copy(alpha = 0.3f) val fg = colorScheme.onSecondaryContainer val rootFg = colorScheme.onTertiaryContainer.copy(alpha = 0.8f) val unmountFg = if (isInDarkTheme) Color.Black.copy(alpha = 0.4f) else Color.White.copy(alpha = 0.8f) val userId = group.uid / 100000 val packageInfo = group.primary.packageInfo val applicationInfo = packageInfo.applicationInfo val hasSharedUserId = !packageInfo.sharedUserId.isNullOrEmpty() val isSystemApp = applicationInfo?.flags?.and(ApplicationInfo.FLAG_SYSTEM) != 0 || applicationInfo.flags.and(ApplicationInfo.FLAG_UPDATED_SYSTEM_APP) != 0 val tags = buildList { if (group.anyAllowSu) add(StatusMeta("ROOT", rootBg, rootFg)) if (group.shouldUmount) add(StatusMeta("UMOUNT", unmountBg, unmountFg)) if (group.anyCustom) add(StatusMeta("CUSTOM", bg, fg)) if (userId != 0) add(StatusMeta("USER $userId", bg, fg)) if (isSystemApp) add(StatusMeta("SYSTEM", bg, fg)) if (hasSharedUserId) add(StatusMeta("SHARED UID", bg, fg)) } Card( modifier = Modifier .padding(horizontal = 12.dp) .padding(bottom = 12.dp), onClick = onClickPrimary, onLongPress = if (group.apps.size > 1) onToggleExpand else null, showIndication = true, insideMargin = PaddingValues(vertical = 8.dp, horizontal = 16.dp) ) { Row( verticalAlignment = Alignment.CenterVertically ) { AppIconImage( packageInfo = group.primary.packageInfo, label = group.primary.label, modifier = Modifier .padding(end = 14.dp) .size(48.dp) ) Column( modifier = Modifier .weight(1f), ) { Text( text = if (group.apps.size > 1) ownerNameForUid(group.uid) else group.primary.label, modifier = Modifier.basicMarquee(), fontWeight = FontWeight(550), color = colorScheme.onSurface, maxLines = 1, softWrap = false ) Text( text = if (group.apps.size > 1) { stringResource(R.string.group_contains_apps, group.apps.size) } else { group.primary.packageName }, modifier = Modifier .basicMarquee(), fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, maxLines = 1, softWrap = false ) FlowRow( modifier = Modifier.padding(top = 3.dp, bottom = 3.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(4.dp) ) { tags.forEach { tag -> StatusTag( label = tag.label, backgroundColor = tag.bg, contentColor = tag.fg ) } } } val layoutDirection = LocalLayoutDirection.current Image( modifier = Modifier .graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f } .padding(start = 8.dp) .size(width = 10.dp, height = 16.dp), imageVector = MiuixIcons.Basic.ArrowRight, contentDescription = null, colorFilter = ColorFilter.tint(colorScheme.onSurfaceVariantActions), ) } } } @Immutable private data class StatusMeta( val label: String, val bg: Color, val fg: Color ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.superuser import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.unit.Dp import androidx.lifecycle.viewmodel.compose.viewModel import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.Navigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel @Composable fun SuperUserPager( navigator: Navigator, bottomInnerPadding: Dp ) { val viewModel = viewModel() val uiState by viewModel.uiState.collectAsState() LaunchedEffect(Unit) { if (uiState.groupedApps.isEmpty()) { viewModel.initializePreferences() viewModel.loadAppList().join() } else if (viewModel.isNeedRefresh) { viewModel.loadAppList(resort = false).join() } } val onSearchTextChange: (String) -> Unit = viewModel::updateSearchText val onToggleShowSystemApps: () -> Unit = { viewModel.toggleShowSystemApps() } val onToggleShowOnlyPrimaryUserApps: () -> Unit = { viewModel.toggleShowOnlyPrimaryUserApps() } val onOpenProfile: (GroupedApps) -> Unit = { group -> navigator.push(Route.AppProfile(group.uid)) viewModel.markNeedRefresh() } val actions = SuperUserActions( onRefresh = { viewModel.loadAppList(force = true) }, onSearchTextChange = onSearchTextChange, onSearchStatusChange = viewModel::updateSearchStatus, onClearSearch = { onSearchTextChange("") }, onToggleShowSystemApps = onToggleShowSystemApps, onToggleShowOnlyPrimaryUserApps = onToggleShowOnlyPrimaryUserApps, onOpenProfile = onOpenProfile, ) when (LocalUiMode.current) { UiMode.Miuix -> SuperUserPagerMiuix( uiState = uiState, actions = actions, bottomInnerPadding = bottomInnerPadding, ) UiMode.Material -> SuperUserPagerMaterial( uiState = uiState, actions = actions, bottomInnerPadding = bottomInnerPadding, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/superuser/SuperUserUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.superuser import androidx.compose.runtime.Immutable import me.weishu.kernelsu.data.model.AppInfo import me.weishu.kernelsu.ui.component.SearchStatus @Immutable data class GroupedApps( val uid: Int, val apps: List, val primary: AppInfo, val anyAllowSu: Boolean, val anyCustom: Boolean, val shouldUmount: Boolean, val ownerName: String? = null, val matchedPackageNames: Set = emptySet(), ) data class SuperUserUiState( val isRefreshing: Boolean = false, val groupedApps: List = emptyList(), val userIds: List = emptyList(), val searchStatus: SearchStatus = SearchStatus(""), val searchResults: List = emptyList(), val showSystemApps: Boolean = false, val showOnlyPrimaryUserApps: Boolean = false, val error: Throwable? = null ) @Immutable data class SuperUserActions( val onRefresh: () -> Unit, val onSearchTextChange: (String) -> Unit, val onSearchStatusChange: (SearchStatus) -> Unit, val onClearSearch: () -> Unit, val onToggleShowSystemApps: () -> Unit, val onToggleShowOnlyPrimaryUserApps: () -> Unit, val onOpenProfile: (GroupedApps) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.template import androidx.compose.animation.core.LinearOutSlowInEasing import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.ContentCopy import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.LoadingIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults import androidx.compose.material3.pulltorefresh.pullToRefresh import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.TemplateInfo import me.weishu.kernelsu.ui.component.material.SegmentedLazyColumn import me.weishu.kernelsu.ui.component.material.SegmentedListItem import me.weishu.kernelsu.ui.component.statustag.StatusTag /** * @author weishu * @date 2023/10/20. */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppProfileTemplateScreenMaterial( state: TemplateUiState, actions: TemplateActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) val pullToRefreshState = rememberPullToRefreshState() val listState = rememberLazyListState() val threshold = with(LocalDensity.current) { 100.dp.toPx() } val fabExpanded by remember { var lastIndex = 0 var lastOffset = 0 var scrollDelta = 0f var expanded = true derivedStateOf { val currentIndex = listState.firstVisibleItemIndex val currentOffset = listState.firstVisibleItemScrollOffset val delta = if (currentIndex == lastIndex) { (currentOffset - lastOffset).toFloat() } else if (currentIndex > lastIndex) { 100f } else { -100f } scrollDelta = (scrollDelta + delta).coerceIn(-threshold, threshold) lastIndex = currentIndex lastOffset = currentOffset if (currentIndex == 0) { expanded = true scrollDelta = 0f } else if (expanded && scrollDelta >= threshold) { expanded = false scrollDelta = 0f } else if (!expanded && scrollDelta <= -threshold) { expanded = true scrollDelta = 0f } expanded } } LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } val scaleFraction = { if (state.isRefreshing) 1f else LinearOutSlowInEasing.transform(pullToRefreshState.distanceFraction).coerceIn(0f, 1f) } Scaffold( modifier = Modifier.pullToRefresh( state = pullToRefreshState, isRefreshing = state.isRefreshing, onRefresh = { actions.onRefresh(false) }, ), topBar = { TopBar( onBack = actions.onBack, onImport = actions.onImport, onExport = actions.onExport, scrollBehavior = scrollBehavior ) }, floatingActionButton = { ExtendedFloatingActionButton( expanded = fabExpanded, onClick = actions.onCreateTemplate, icon = { Icon(Icons.Filled.Add, null) }, text = { Text(stringResource(id = R.string.app_profile_template_create)) }, modifier = Modifier.padding( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding(), ), ) }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> val isLoading = state.templateList.isEmpty() if (isLoading && !state.isRefreshing) { Box( modifier = Modifier .fillMaxSize() .padding(innerPadding), contentAlignment = Alignment.Center ) { if (state.offline) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = stringResource(R.string.network_offline), color = MaterialTheme.colorScheme.outline) Spacer(Modifier.height(12.dp)) Button( onClick = { actions.onRefresh(false) }, ) { Text(stringResource(R.string.network_retry)) } } } else { LoadingIndicator() } } } else { val templateList = state.templateList val navBars = WindowInsets.navigationBars.asPaddingValues() val captionBar = WindowInsets.captionBar.asPaddingValues() Box(Modifier.padding(innerPadding)) { SegmentedLazyColumn( modifier = Modifier .fillMaxSize() .nestedScroll(scrollBehavior.nestedScrollConnection), state = listState, contentPadding = PaddingValues( start = 16.dp, top = 8.dp, end = 16.dp, bottom = 16.dp + 56.dp + 16.dp + navBars.calculateBottomPadding() + captionBar.calculateBottomPadding() ), items = templateList, itemContent = { template -> TemplateItem( template = template, onClick = { actions.onOpenTemplate(template) }, ) } ) Box( modifier = Modifier .align(Alignment.TopCenter) .graphicsLayer { scaleX = scaleFraction() scaleY = scaleFraction() } ) { PullToRefreshDefaults.LoadingIndicator(state = pullToRefreshState, isRefreshing = state.isRefreshing) } } } } } @OptIn(ExperimentalLayoutApi::class) @Composable private fun TemplateItem( template: TemplateInfo, onClick: () -> Unit, ) { SegmentedListItem( onClick = onClick, headlineContent = { Text(template.name) }, supportingContent = { Column { Text( text = "${template.id}${if (template.author.isEmpty()) "" else "@${template.author}"}", style = MaterialTheme.typography.bodyMedium, fontSize = MaterialTheme.typography.bodyMedium.fontSize, ) Text(template.description, color = MaterialTheme.colorScheme.outline) FlowRow(modifier = Modifier.padding(top = 4.dp)) { StatusTag( label = "UID: ${template.uid}", contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) StatusTag( label = "GID: ${template.gid}", contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) StatusTag( label = template.context, contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) if (template.local) { StatusTag( label = "local", contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } else { StatusTag( label = "remote", contentColor = MaterialTheme.colorScheme.onPrimary, backgroundColor = MaterialTheme.colorScheme.primary ) } } } }, ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( onBack: () -> Unit, onImport: () -> Unit = {}, onExport: () -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null ) { LargeFlexibleTopAppBar( title = { Text(stringResource(R.string.settings_profile_template)) }, navigationIcon = { IconButton( onClick = onBack ) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, actions = { var showDropdown by remember { mutableStateOf(false) } IconButton( onClick = { showDropdown = true } ) { Icon( imageVector = Icons.Filled.ContentCopy, contentDescription = stringResource(id = R.string.app_profile_import_export) ) DropdownMenu( expanded = showDropdown, onDismissRequest = { showDropdown = false } ) { DropdownMenuItem( text = { Text(stringResource(id = R.string.app_profile_import_from_clipboard)) }, onClick = { onImport() showDropdown = false } ) DropdownMenuItem( text = { Text(stringResource(id = R.string.app_profile_export_to_clipboard)) }, onClick = { onExport() showDropdown = false } ) } } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.template import android.annotation.SuppressLint import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Fingerprint import androidx.compose.material.icons.outlined.Group import androidx.compose.material.icons.outlined.Shield import androidx.compose.material.icons.rounded.Add import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollSource import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.data.model.TemplateInfo import me.weishu.kernelsu.ui.component.miuix.DropdownItem import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect import top.yukonga.miuix.kmp.basic.Card import top.yukonga.miuix.kmp.basic.FloatingActionButton import top.yukonga.miuix.kmp.basic.HorizontalDivider import top.yukonga.miuix.kmp.basic.Icon import top.yukonga.miuix.kmp.basic.IconButton import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator import top.yukonga.miuix.kmp.basic.ListPopupColumn import top.yukonga.miuix.kmp.basic.ListPopupDefaults import top.yukonga.miuix.kmp.basic.MiuixScrollBehavior import top.yukonga.miuix.kmp.basic.PopupPositionProvider import top.yukonga.miuix.kmp.basic.PullToRefresh import top.yukonga.miuix.kmp.basic.Scaffold import top.yukonga.miuix.kmp.basic.ScrollBehavior import top.yukonga.miuix.kmp.basic.Text import top.yukonga.miuix.kmp.basic.TextButton import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.basic.rememberPullToRefreshState import top.yukonga.miuix.kmp.extra.SuperListPopup import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Copy import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.PressFeedbackType import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ @SuppressLint("LocalContextGetResourceValueCall") @Composable fun AppProfileTemplateScreenMiuix( state: TemplateUiState, actions: TemplateActions, ) { val scrollBehavior = MiuixScrollBehavior() val listState = rememberLazyListState() var fabVisible by remember { mutableStateOf(true) } var scrollDistance by remember { mutableFloatStateOf(0f) } val nestedScrollConnection = remember { object : NestedScrollConnection { override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset { val isScrolledToEnd = (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.index == listState.layoutInfo.totalItemsCount - 1 && (listState.layoutInfo.visibleItemsInfo.lastOrNull()?.size ?: 0) < listState.layoutInfo.viewportEndOffset) val delta = available.y if (!isScrolledToEnd) { scrollDistance += delta if (scrollDistance < -50f) { if (fabVisible) fabVisible = false scrollDistance = 0f } else if (scrollDistance > 50f) { if (!fabVisible) fabVisible = true scrollDistance = 0f } } return Offset.Zero } } } val offsetHeight by animateDpAsState( targetValue = if (fabVisible) 0.dp else 100.dp + WindowInsets.systemBars.asPaddingValues().calculateBottomPadding(), animationSpec = tween(durationMillis = 350) ) val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopBar( onBack = actions.onBack, onImport = actions.onImport, onExport = actions.onExport, scrollBehavior = scrollBehavior, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, floatingActionButton = { FloatingActionButton( containerColor = colorScheme.primary, shadowElevation = 0.dp, onClick = actions.onCreateTemplate, modifier = Modifier .offset { IntOffset(x = 0, y = offsetHeight.roundToPx()) } .padding( bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() + 20.dp, end = 20.dp ) .border(0.05.dp, colorScheme.outline.copy(alpha = 0.5f), CircleShape), content = { Icon( Icons.Rounded.Add, null, Modifier.size(40.dp), tint = colorScheme.onPrimary ) }, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> if (state.templateList.isEmpty() && !state.isRefreshing) { val layoutDirection = LocalLayoutDirection.current Box( modifier = Modifier .fillMaxSize() .padding( start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection), bottom = innerPadding.calculateBottomPadding(), ), contentAlignment = Alignment.Center ) { if (state.offline) { Column(horizontalAlignment = Alignment.CenterHorizontally) { Text(text = stringResource(R.string.network_offline), color = colorScheme.onSurfaceVariantSummary, fontSize = 16.sp) Spacer(Modifier.height(12.dp)) TextButton( modifier = Modifier .padding(horizontal = 24.dp) .fillMaxWidth(), text = stringResource(R.string.network_retry), onClick = { actions.onRefresh(false) }, ) } } else { InfiniteProgressIndicator() } } } val pullToRefreshState = rememberPullToRefreshState() val refreshTexts = listOf( stringResource(R.string.refresh_pulling), stringResource(R.string.refresh_release), stringResource(R.string.refresh_refresh), stringResource(R.string.refresh_complete), ) val layoutDirection = LocalLayoutDirection.current PullToRefresh( isRefreshing = state.isRefreshing, pullToRefreshState = pullToRefreshState, onRefresh = { actions.onRefresh(true) }, refreshTexts = refreshTexts, contentPadding = PaddingValues( top = innerPadding.calculateTopPadding() + 12.dp, start = innerPadding.calculateStartPadding(layoutDirection), end = innerPadding.calculateEndPadding(layoutDirection) ), ) { LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(nestedScrollConnection) .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it } .padding(horizontal = 12.dp), contentPadding = innerPadding, overscrollEffect = null ) { item { Spacer(Modifier.height(12.dp)) } items(state.templateList, key = { it.id }) { app -> TemplateItem( template = app, onClick = { actions.onOpenTemplate(app) }, ) } item { Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } } @Composable private fun TemplateItem( template: TemplateInfo, onClick: () -> Unit, ) { Card( modifier = Modifier.padding(bottom = 12.dp), onClick = onClick, showIndication = true, pressFeedbackType = PressFeedbackType.Sink ) { Column( modifier = Modifier .fillMaxWidth() .padding(16.dp) ) { Row( verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth() ) { Text( text = template.name, fontWeight = FontWeight(550), color = colorScheme.onSurface, ) Spacer(modifier = Modifier.weight(1f)) if (template.local) { Text( text = "LOCAL", color = colorScheme.onTertiaryContainer, fontWeight = FontWeight(750), style = MiuixTheme.textStyles.footnote1 ) } else { Text( text = "REMOTE", color = colorScheme.onSurfaceSecondary, fontWeight = FontWeight(750), style = MiuixTheme.textStyles.footnote1 ) } } Text( text = "${template.id}${if (template.author.isEmpty()) "" else " by @${template.author}"}", modifier = Modifier.padding(top = 1.dp), fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceVariantSummary, ) Spacer(modifier = Modifier.height(4.dp)) Text( text = template.description, fontSize = 14.sp, color = colorScheme.onSurfaceVariantSummary, ) HorizontalDivider( modifier = Modifier.padding(vertical = 8.dp), thickness = 0.5.dp, color = colorScheme.outline.copy(alpha = 0.5f) ) FlowRow( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { InfoChip( icon = Icons.Outlined.Fingerprint, text = "UID: ${template.uid}" ) InfoChip( icon = Icons.Outlined.Group, text = "GID: ${template.gid}" ) InfoChip( icon = Icons.Outlined.Shield, text = template.context ) } } } } @Composable private fun InfoChip(icon: ImageVector, text: String) { Row(verticalAlignment = Alignment.CenterVertically) { Icon( imageVector = icon, contentDescription = null, modifier = Modifier.size(14.dp), tint = colorScheme.onSurfaceSecondary.copy(alpha = 0.8f) ) Text( modifier = Modifier.padding(start = 4.dp), text = text, fontSize = 12.sp, fontWeight = FontWeight(550), color = colorScheme.onSurfaceSecondary ) } } @Composable private fun TopBar( onBack: () -> Unit, onImport: () -> Unit = {}, onExport: () -> Unit = {}, scrollBehavior: ScrollBehavior, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = stringResource(R.string.settings_profile_template), navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onBackground ) } }, actions = { val showTopPopup = remember { mutableStateOf(false) } SuperListPopup( show = showTopPopup.value, popupPositionProvider = ListPopupDefaults.ContextMenuPositionProvider, alignment = PopupPositionProvider.Align.TopEnd, onDismissRequest = { showTopPopup.value = false }, content = { ListPopupColumn { val items = listOf( stringResource(id = R.string.app_profile_import_from_clipboard), stringResource(id = R.string.app_profile_export_to_clipboard) ) items.forEachIndexed { index, text -> DropdownItem( text = text, optionSize = items.size, index = index, onSelectedIndexChange = { selectedIndex -> if (selectedIndex == 0) { onImport() } else { onExport() } showTopPopup.value = false } ) } } } ) IconButton( modifier = Modifier.padding(end = 16.dp), onClick = { showTopPopup.value = true }, holdDownState = showTopPopup.value ) { Icon( imageVector = MiuixIcons.Copy, contentDescription = stringResource(id = R.string.app_profile_import_export), tint = colorScheme.onBackground ) } }, scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.template import android.content.ClipData import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.navigation3.Route import me.weishu.kernelsu.ui.util.isNetworkAvailable import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel @Composable fun AppProfileTemplateScreen() { val uiMode = LocalUiMode.current val navigator = LocalNavigator.current val viewModel = viewModel() val screenState by viewModel.uiState.collectAsState() val clipboard = LocalClipboard.current val context = LocalContext.current val scope = rememberCoroutineScope() val requestKey = "template_edit" LaunchedEffect(Unit) { if (screenState.templateList.isEmpty()) { viewModel.fetchTemplates() } } LaunchedEffect(Unit) { navigator.observeResult(requestKey).collect { success -> if (success) { if (uiMode == UiMode.Miuix) { navigator.clearResult(requestKey) } viewModel.fetchTemplates() } } } val importEmptyText = stringResource(R.string.app_profile_template_import_empty) val importSuccessText = stringResource(R.string.app_profile_template_import_success) val exportEmptyText = stringResource(R.string.app_profile_template_export_empty) val showToast: (String) -> Unit = { message -> scope.launch(Dispatchers.Main) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } } val uiState = screenState.copy(offline = !isNetworkAvailable(context)) val actions = TemplateActions( onBack = dropUnlessResumed { navigator.pop() }, onRefresh = { forceSync -> scope.launch { viewModel.fetchTemplates(forceSync) } }, onImport = { scope.launch { clipboard.getClipEntry()?.clipData?.getItemAt(0)?.text?.toString()?.let { templateText -> if (templateText.isEmpty()) { showToast(importEmptyText) return@let } viewModel.importTemplates( templateText, onSuccess = { showToast(importSuccessText) viewModel.fetchTemplates(false) }, onFailure = showToast, ) } } }, onExport = { scope.launch { viewModel.exportTemplates( onTemplateEmpty = { showToast(exportEmptyText) }, callback = { templateText -> clipboard.setClipEntry( ClipEntry(ClipData.newPlainText("template", templateText)) ) }, ) } }, onCreateTemplate = { when (uiMode) { UiMode.Miuix -> navigator.navigateForResult( Route.TemplateEditor(TemplateViewModel.TemplateInfo(), false), requestKey, ) UiMode.Material -> navigator.push( Route.TemplateEditor(TemplateViewModel.TemplateInfo(), false) ) } }, onOpenTemplate = { template -> when (uiMode) { UiMode.Miuix -> navigator.navigateForResult( Route.TemplateEditor(template, !template.local), requestKey, ) UiMode.Material -> navigator.push( Route.TemplateEditor(template, !template.local) ) } }, ) when (uiMode) { UiMode.Miuix -> AppProfileTemplateScreenMiuix( state = uiState, actions = actions, ) UiMode.Material -> AppProfileTemplateScreenMaterial( state = uiState, actions = actions, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/template/TemplateUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.template import androidx.compose.runtime.Immutable import me.weishu.kernelsu.data.model.TemplateInfo @Immutable data class TemplateUiState( val isRefreshing: Boolean = false, val offline: Boolean = false, val templates: List = emptyList(), val templateList: List = emptyList(), val error: Throwable? = null, ) @Immutable data class TemplateActions( val onBack: () -> Unit, val onRefresh: (Boolean) -> Unit, val onImport: () -> Unit, val onExport: () -> Unit, val onCreateTemplate: () -> Unit, val onOpenTemplate: (TemplateInfo) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorMaterial.kt ================================================ package me.weishu.kernelsu.ui.screen.templateeditor import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.only import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.filled.DeleteForever import androidx.compose.material.icons.filled.Save import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.rememberTopAppBarState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.material.SegmentedColumn import me.weishu.kernelsu.ui.component.material.SegmentedTextField import me.weishu.kernelsu.ui.component.profile.RootProfileConfig @OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterial3Api::class) @Composable fun TemplateEditorScreenMaterial( state: TemplateEditorUiState, actions: TemplateEditorActions, ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(rememberTopAppBarState()) LaunchedEffect(Unit) { scrollBehavior.state.heightOffset = scrollBehavior.state.heightOffsetLimit } Scaffold( topBar = { TopBar( title = if (state.isCreation) { stringResource(R.string.app_profile_template_create) } else if (state.readOnly) { stringResource(R.string.app_profile_template_view) } else { stringResource(R.string.app_profile_template_edit) }, readOnly = state.readOnly, summary = state.titleSummary, onBack = actions.onBack, onDelete = actions.onDelete, onSave = actions.onSave, scrollBehavior = scrollBehavior ) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) ) { innerPadding -> Column( modifier = Modifier .padding(innerPadding) .imePadding() .nestedScroll(scrollBehavior.nestedScrollConnection) .verticalScroll(rememberScrollState()) ) { SegmentedColumn( modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp), content = buildList { add( { TemplateEditorListItem( label = stringResource(id = R.string.app_profile_template_name), value = state.template.name, readOnly = state.readOnly, onValueChange = actions.onNameChange, ) } ) if (state.isCreation) { add( { TemplateEditorListItem( label = stringResource(id = R.string.app_profile_template_id), value = state.template.id, errorHint = state.idErrorHint, isError = state.idErrorHint.isNotEmpty(), readOnly = state.readOnly, onValueChange = actions.onIdChange, ) } ) } add( { TemplateEditorListItem( label = stringResource(id = R.string.module_author), value = state.template.author, readOnly = state.readOnly, onValueChange = actions.onAuthorChange, ) } ) add( { TemplateEditorListItem( label = stringResource(id = R.string.app_profile_template_description), value = state.template.description, multiline = true, readOnly = state.readOnly, onValueChange = actions.onDescriptionChange, ) } ) } ) RootProfileConfig( fixedName = true, enabled = !state.readOnly, profile = toNativeProfile(state.template), onProfileChange = actions.onProfileChange, ) Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() ) ) } } } @Composable private fun TemplateEditorListItem( label: String, value: String, errorHint: String = "", isError: Boolean = false, multiline: Boolean = false, readOnly: Boolean = false, onValueChange: (String) -> Unit ) { SegmentedTextField( value = value, onValueChange = onValueChange, label = label, supportingContent = if (isError && errorHint.isNotEmpty()) { { Text(errorHint, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.labelSmall) } } else null, isError = isError, singleLine = !multiline, minLines = 1, maxLines = if (multiline) 100 else 1, readOnly = readOnly ) } @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable private fun TopBar( title: String, readOnly: Boolean, summary: String = "", onBack: () -> Unit, onDelete: () -> Unit = {}, onSave: () -> Unit = {}, scrollBehavior: TopAppBarScrollBehavior? = null ) { LargeFlexibleTopAppBar( title = { Column { Text(title) if (summary.isNotBlank()) { Text( text = summary, style = MaterialTheme.typography.bodyMedium, ) } } }, navigationIcon = { IconButton(onClick = onBack) { Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) } }, actions = { if (readOnly) return@LargeFlexibleTopAppBar IconButton(onClick = onDelete) { Icon( Icons.Filled.DeleteForever, contentDescription = stringResource(id = R.string.app_profile_template_delete) ) } IconButton(onClick = onSave) { Icon( imageVector = Icons.Filled.Save, contentDescription = stringResource(id = R.string.app_profile_template_save) ) } }, colors = TopAppBarDefaults.topAppBarColors( containerColor = MaterialTheme.colorScheme.surface, scrolledContainerColor = MaterialTheme.colorScheme.surface ), windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorMiuix.kt ================================================ package me.weishu.kernelsu.ui.screen.templateeditor import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsetsSides import androidx.compose.foundation.layout.add import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.captionBar import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.navigationBars 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.foundation.text.KeyboardOptions import androidx.compose.runtime.Composable import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.HazeTint import dev.chrisbanes.haze.hazeSource import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.component.miuix.EditText import me.weishu.kernelsu.ui.component.profile.RootProfileConfig import me.weishu.kernelsu.ui.theme.LocalEnableBlur import me.weishu.kernelsu.ui.util.defaultHazeEffect 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.ScrollBehavior import top.yukonga.miuix.kmp.basic.TopAppBar import top.yukonga.miuix.kmp.icon.MiuixIcons import top.yukonga.miuix.kmp.icon.extended.Back import top.yukonga.miuix.kmp.icon.extended.Delete import top.yukonga.miuix.kmp.icon.extended.Ok import top.yukonga.miuix.kmp.theme.MiuixTheme.colorScheme import top.yukonga.miuix.kmp.utils.overScrollVertical import top.yukonga.miuix.kmp.utils.scrollEndHaptic /** * @author weishu * @date 2023/10/20. */ @Composable fun TemplateEditorScreenMiuix( state: TemplateEditorUiState, actions: TemplateEditorActions, ) { val scrollBehavior = MiuixScrollBehavior() val enableBlur = LocalEnableBlur.current val hazeState = remember { HazeState() } val hazeStyle = if (enableBlur) { HazeStyle( backgroundColor = colorScheme.surface, tint = HazeTint(colorScheme.surface.copy(0.8f)) ) } else { HazeStyle.Unspecified } Scaffold( topBar = { TopBar( title = if (state.isCreation) { stringResource(R.string.app_profile_template_create) } else if (state.readOnly) { stringResource(R.string.app_profile_template_view) } else { stringResource(R.string.app_profile_template_edit) }, readOnly = state.readOnly, isCreation = state.isCreation, onBack = actions.onBack, onDelete = actions.onDelete, onSave = actions.onSave, scrollBehavior = scrollBehavior, hazeState = hazeState, hazeStyle = hazeStyle, enableBlur = enableBlur, ) }, popupHost = { }, contentWindowInsets = WindowInsets.systemBars.add(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal) ) { innerPadding -> LazyColumn( modifier = Modifier .fillMaxHeight() .scrollEndHaptic() .overScrollVertical() .nestedScroll(scrollBehavior.nestedScrollConnection) .let { if (enableBlur) it.hazeSource(state = hazeState) else it }, contentPadding = innerPadding, overscrollEffect = null ) { item { Card( modifier = Modifier .fillMaxWidth() .padding(12.dp), ) { TextEdit( label = stringResource(id = R.string.app_profile_template_name), text = state.template.name, enabled = !state.readOnly, onValueChange = actions.onNameChange, ) TextEdit( label = stringResource(id = R.string.app_profile_template_id), text = state.template.id, isError = state.idErrorHint.isNotEmpty(), enabled = !state.readOnly, onValueChange = actions.onIdChange, ) TextEdit( label = stringResource(R.string.module_author), text = state.template.author, enabled = !state.readOnly, onValueChange = actions.onAuthorChange, ) TextEdit( label = stringResource(id = R.string.app_profile_template_description), text = state.template.description, enabled = !state.readOnly, onValueChange = actions.onDescriptionChange, ) RootProfileConfig( fixedName = true, enabled = !state.readOnly, profile = toNativeProfile(state.template), onProfileChange = actions.onProfileChange, ) } Spacer( Modifier.height( WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding() + WindowInsets.captionBar.asPaddingValues().calculateBottomPadding() ) ) } } } } @Composable private fun TopBar( title: String, readOnly: Boolean, isCreation: Boolean, onBack: () -> Unit, onDelete: () -> Unit = {}, onSave: () -> Unit = {}, scrollBehavior: ScrollBehavior, hazeState: HazeState, hazeStyle: HazeStyle, enableBlur: Boolean ) { TopAppBar( modifier = if (enableBlur) { Modifier.defaultHazeEffect(hazeState, hazeStyle) } else { Modifier }, color = if (enableBlur) Color.Transparent else colorScheme.surface, title = title, navigationIcon = { IconButton( modifier = Modifier.padding(start = 16.dp), onClick = onBack ) { val layoutDirection = LocalLayoutDirection.current Icon( modifier = Modifier.graphicsLayer { if (layoutDirection == LayoutDirection.Rtl) scaleX = -1f }, imageVector = MiuixIcons.Back, contentDescription = null, tint = colorScheme.onSurface ) } }, actions = { when { !readOnly && !isCreation -> { IconButton( modifier = Modifier.padding(end = 16.dp), onClick = onDelete ) { Icon( imageVector = MiuixIcons.Delete, contentDescription = stringResource(id = R.string.app_profile_template_delete), tint = colorScheme.onBackground ) } } isCreation -> { IconButton( modifier = Modifier.padding(end = 16.dp), onClick = onSave ) { Icon( imageVector = MiuixIcons.Ok, contentDescription = stringResource(id = R.string.app_profile_template_save), tint = colorScheme.onBackground ) } } } }, scrollBehavior = scrollBehavior ) } @Composable private fun TextEdit( label: String, text: String, isError: Boolean = false, enabled: Boolean = true, onValueChange: (String) -> Unit = {} ) { val editText = remember(text) { mutableStateOf(text) } EditText( title = label.uppercase(), textValue = editText, onTextValueChange = { newText -> editText.value = newText onValueChange(newText) }, keyboardOptions = KeyboardOptions( keyboardType = KeyboardType.Ascii, ), isError = isError, enabled = enabled, ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorScreen.kt ================================================ package me.weishu.kernelsu.ui.screen.templateeditor import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.dropUnlessResumed import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.navigation3.LocalNavigator import me.weishu.kernelsu.ui.util.deleteAppProfileTemplate import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel @Composable fun TemplateEditorScreen(template: TemplateViewModel.TemplateInfo, readOnly: Boolean) { val navigator = LocalNavigator.current val context = LocalContext.current val uiMode = LocalUiMode.current val isCreation = template.id.isBlank() val autoSave = uiMode == UiMode.Miuix && !isCreation var currentTemplate by rememberSaveable { mutableStateOf(template) } var idErrorHint by remember { mutableStateOf("") } val saveTemplateFailed = stringResource(id = R.string.app_profile_template_save_failed) val idConflictError = stringResource(id = R.string.app_profile_template_id_exist) val idInvalidError = stringResource(id = R.string.app_profile_template_id_invalid) fun showToast(message: String) { Toast.makeText(context, message, Toast.LENGTH_SHORT).show() } fun finishEditing() { if (!readOnly) { navigator.setResult("template_edit", true) } else { navigator.pop() } } fun updateTemplate(updatedTemplate: TemplateViewModel.TemplateInfo) { if (autoSave) { if (!saveTemplate(updatedTemplate)) { return } } currentTemplate = updatedTemplate } val uiState = TemplateEditorUiState( template = currentTemplate, initialTemplate = template, readOnly = readOnly, isCreation = isCreation, idErrorHint = idErrorHint, ) fun saveCurrentTemplate() { if (uiMode == UiMode.Miuix) { when (idCheck(currentTemplate.id)) { 1 -> { showToast(idConflictError) return } 2 -> { showToast(idInvalidError) return } } } if (saveTemplate(currentTemplate, isCreation)) { navigator.setResult("template_edit", true) } else { showToast(saveTemplateFailed) } } val actions = TemplateEditorActions( onBack = dropUnlessResumed { finishEditing() }, onDelete = { if (deleteAppProfileTemplate(currentTemplate.id)) { navigator.setResult("template_edit", true) } }, onSave = ::saveCurrentTemplate, onNameChange = { value -> updateTemplate(currentTemplate.copy(name = value)) }, onIdChange = { value -> idErrorHint = if (isTemplateExist(value)) { idConflictError } else if (!isValidTemplateId(value)) { idInvalidError } else { "" } currentTemplate = currentTemplate.copy(id = value) }, onAuthorChange = { value -> updateTemplate(currentTemplate.copy(author = value)) }, onDescriptionChange = { value -> updateTemplate(currentTemplate.copy(description = value)) }, onProfileChange = { profile -> updateTemplate( currentTemplate.copy( uid = profile.uid, gid = profile.gid, groups = profile.groups, capabilities = profile.capabilities, context = profile.context, namespace = profile.namespace, rules = profile.rules.split("\n"), ) ) }, ) when (uiMode) { UiMode.Miuix -> TemplateEditorScreenMiuix( state = uiState, actions = actions, ) UiMode.Material -> TemplateEditorScreenMaterial( state = uiState, actions = actions, ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorUiState.kt ================================================ package me.weishu.kernelsu.ui.screen.templateeditor import androidx.compose.runtime.Immutable import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.model.TemplateInfo @Immutable data class TemplateEditorUiState( val template: TemplateInfo, val initialTemplate: TemplateInfo, val readOnly: Boolean, val isCreation: Boolean, val idErrorHint: String = "", ) { val titleSummary: String get() = initialTemplate.id + if (initialTemplate.author.isNotEmpty()) "@${initialTemplate.author}" else "" } @Immutable data class TemplateEditorActions( val onBack: () -> Unit, val onDelete: () -> Unit, val onSave: () -> Unit, val onNameChange: (String) -> Unit, val onIdChange: (String) -> Unit, val onAuthorChange: (String) -> Unit, val onDescriptionChange: (String) -> Unit, val onProfileChange: (Natives.Profile) -> Unit, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/screen/templateeditor/TemplateEditorUtils.kt ================================================ package me.weishu.kernelsu.ui.screen.templateeditor import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.model.TemplateInfo import me.weishu.kernelsu.ui.util.getAppProfileTemplate import me.weishu.kernelsu.ui.util.setAppProfileTemplate fun toNativeProfile(templateInfo: TemplateInfo): Natives.Profile { return Natives.Profile().copy( rootTemplate = templateInfo.id, uid = templateInfo.uid, gid = templateInfo.gid, groups = templateInfo.groups, capabilities = templateInfo.capabilities, context = templateInfo.context, namespace = templateInfo.namespace, rules = templateInfo.rules.joinToString("\n").ifBlank { "" } ) } fun isTemplateValid(template: TemplateInfo): Boolean { if (template.id.isBlank()) { return false } if (!isValidTemplateId(template.id)) { return false } return true } fun idCheck(value: String): Int { return if (value.isEmpty()) 0 else if (isTemplateExist(value)) 1 else if (!isValidTemplateId(value)) 2 else 0 } fun saveTemplate(template: TemplateInfo, isCreation: Boolean = false): Boolean { if (!isTemplateValid(template)) { return false } if (isCreation && isTemplateExist(template.id)) { return false } val json = template.toJSON() json.put("local", true) return setAppProfileTemplate(template.id, json.toString()) } fun isValidTemplateId(id: String): Boolean { return Regex("""^([A-Za-z][A-Za-z\d_]*\.)*[A-Za-z][A-Za-z\d_]*$""").matches(id) } fun isTemplateExist(id: String): Boolean { return getAppProfileTemplate(id).isNotBlank() } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Colors.kt ================================================ package me.weishu.kernelsu.ui.theme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb val keyColorOptions = listOf( Color(0xFFF44336).toArgb(), Color(0xFFE91E63).toArgb(), Color(0xFF9C27B0).toArgb(), Color(0xFF673AB7).toArgb(), Color(0xFF3F51B5).toArgb(), Color(0xFF2196F3).toArgb(), Color(0xFF00BCD4).toArgb(), Color(0xFF009688).toArgb(), Color(0xFF4FAF50).toArgb(), Color(0xFFFFEB3B).toArgb(), Color(0xFFFFC107).toArgb(), Color(0xFFFF9800).toArgb(), Color(0xFF795548).toArgb(), Color(0xFF607D8F).toArgb(), Color(0xFFFF9CA8).toArgb(), ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/theme/MaterialTheme.kt ================================================ package me.weishu.kernelsu.ui.theme import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialExpressiveTheme import androidx.compose.material3.MotionScheme import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowInsetsControllerCompat import com.materialkolor.rememberDynamicColorScheme import me.weishu.kernelsu.ui.webui.MonetColorsProvider @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable fun MaterialKernelSUTheme( appSettings: AppSettings, content: @Composable () -> Unit ) { val context = LocalContext.current val systemDarkTheme = isSystemInDarkTheme() val darkTheme = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && systemDarkTheme) val amoledMode = appSettings.colorMode.isAmoled val dynamicColor = appSettings.keyColor == 0 val colorStyle = appSettings.paletteStyle val colorSpec = appSettings.colorSpec val colorScheme = if (dynamicColor) { val baseScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) rememberDynamicColorScheme( seedColor = Color.Unspecified, isDark = darkTheme, isAmoled = amoledMode, style = colorStyle, specVersion = colorSpec, primary = baseScheme.primary, secondary = baseScheme.secondary, tertiary = baseScheme.tertiary, neutral = baseScheme.surface, neutralVariant = baseScheme.surfaceVariant, error = baseScheme.error ) } else { rememberDynamicColorScheme( seedColor = Color(appSettings.keyColor), isDark = darkTheme, isAmoled = amoledMode, style = colorStyle, specVersion = colorSpec, ) } LaunchedEffect(darkTheme) { val window = (context as? Activity)?.window ?: return@LaunchedEffect WindowInsetsControllerCompat(window, window.decorView).apply { isAppearanceLightStatusBars = !darkTheme isAppearanceLightNavigationBars = !darkTheme } } MaterialExpressiveTheme( colorScheme = colorScheme, motionScheme = MotionScheme.expressive(), content = { MonetColorsProvider.UpdateCss() content() } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/theme/MiuixTheme.kt ================================================ package me.weishu.kernelsu.ui.theme import android.app.Activity import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.core.view.WindowInsetsControllerCompat import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.ui.webui.MonetColorsProvider import top.yukonga.miuix.kmp.theme.ColorSchemeMode import top.yukonga.miuix.kmp.theme.LocalContentColor import top.yukonga.miuix.kmp.theme.MiuixTheme import top.yukonga.miuix.kmp.theme.ThemeColorSpec import top.yukonga.miuix.kmp.theme.ThemeController import top.yukonga.miuix.kmp.theme.ThemePaletteStyle @Composable fun MiuixKernelSUTheme( appSettings: AppSettings, content: @Composable () -> Unit ) { val context = LocalContext.current val systemDarkTheme = isSystemInDarkTheme() val darkTheme = appSettings.colorMode.isDark || (appSettings.colorMode.isSystem && systemDarkTheme) val colorStyle = appSettings.paletteStyle val colorSpec = appSettings.colorSpec val miuixPaletteStyle = try { ThemePaletteStyle.valueOf(colorStyle.name) } catch (_: Exception) { ThemePaletteStyle.TonalSpot } val miuixColorSpec = if (colorSpec == ColorSpec.SpecVersion.SPEC_2025) { ThemeColorSpec.Spec2025 } else { ThemeColorSpec.Spec2021 } val controller = ThemeController( when (appSettings.colorMode) { ColorMode.SYSTEM -> ColorSchemeMode.System ColorMode.LIGHT -> ColorSchemeMode.Light ColorMode.DARK -> ColorSchemeMode.Dark ColorMode.MONET_SYSTEM -> ColorSchemeMode.MonetSystem ColorMode.MONET_LIGHT -> ColorSchemeMode.MonetLight ColorMode.MONET_DARK, ColorMode.DARK_AMOLED -> ColorSchemeMode.MonetDark }, keyColor = if (appSettings.keyColor == 0) null else Color(appSettings.keyColor), isDark = darkTheme, paletteStyle = miuixPaletteStyle, colorSpec = miuixColorSpec, ) MiuixTheme( controller = controller, content = { LaunchedEffect(darkTheme) { val window = (context as? Activity)?.window ?: return@LaunchedEffect WindowInsetsControllerCompat(window, window.decorView).apply { isAppearanceLightStatusBars = !darkTheme isAppearanceLightNavigationBars = !darkTheme } } MonetColorsProvider.UpdateCss() CompositionLocalProvider( LocalContentColor provides MiuixTheme.colorScheme.onBackground, ) { content() } } ) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/theme/Theme.kt ================================================ package me.weishu.kernelsu.ui.theme import android.content.Context import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import com.materialkolor.PaletteStyle import com.materialkolor.dynamiccolor.ColorSpec import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode enum class ColorMode(val value: Int) { SYSTEM(0), LIGHT(1), DARK(2), MONET_SYSTEM(3), MONET_LIGHT(4), MONET_DARK(5), DARK_AMOLED(6); companion object { fun fromValue(value: Int) = entries.find { it.value == value } ?: SYSTEM } val isSystem: Boolean get() = value == 0 || value == 3 val isDark: Boolean get() = value == 2 || value == 5 || value == 6 val isAmoled: Boolean get() = value == 6 val isMonet: Boolean get() = value >= 3 fun toNonMonetMode(): Int = when (this) { MONET_SYSTEM -> 0 MONET_LIGHT -> 1 MONET_DARK, DARK_AMOLED -> 2 else -> value } fun toMonetMode(): Int = when (this) { SYSTEM -> 3 LIGHT -> 4 DARK -> 5 else -> value } } data class AppSettings( val colorMode: ColorMode, val keyColor: Int, val paletteStyle: PaletteStyle, val colorSpec: ColorSpec.SpecVersion, ) object ThemeController { fun getAppSettings(context: Context): AppSettings { val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) val uiMode = prefs.getString("ui_mode", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE var colorModeValue = prefs.getInt("color_mode", ColorMode.SYSTEM.value) if (uiMode == "miuix") { val miuixMonet = prefs.getBoolean("miuix_monet", false) val colorMode = ColorMode.fromValue(colorModeValue) colorModeValue = if (!miuixMonet && colorMode.isMonet) { colorMode.toNonMonetMode() } else if (miuixMonet && !colorMode.isMonet) { colorMode.toMonetMode() } else { colorModeValue } } val colorMode = ColorMode.fromValue(colorModeValue) val keyColor = prefs.getInt("key_color", 0) val paletteStyleStr = prefs.getString("color_style", PaletteStyle.TonalSpot.name) val paletteStyle = try { PaletteStyle.valueOf(paletteStyleStr!!) } catch (_: Exception) { PaletteStyle.TonalSpot } val colorSpecStr = prefs.getString("color_spec", ColorSpec.SpecVersion.Default.name) val colorSpec = try { ColorSpec.SpecVersion.valueOf(colorSpecStr!!) } catch (_: Exception) { ColorSpec.SpecVersion.Default } return AppSettings(colorMode, keyColor, paletteStyle, colorSpec) } } @Composable fun KernelSUTheme( appSettings: AppSettings? = null, uiMode: UiMode = LocalUiMode.current, content: @Composable () -> Unit ) { val context = LocalContext.current val currentAppSettings = appSettings ?: ThemeController.getAppSettings(context) when (uiMode) { UiMode.Miuix -> MiuixKernelSUTheme( appSettings = currentAppSettings, content = content ) UiMode.Material -> MaterialKernelSUTheme( appSettings = currentAppSettings, content = content ) } } @Composable @ReadOnlyComposable fun isInDarkTheme(): Boolean { return when (LocalColorMode.current) { 1, 4 -> false // Force light mode 2, 5, 6 -> true // Force dark mode else -> isSystemInDarkTheme() // Follow system (0 or default) } } val LocalColorMode = staticCompositionLocalOf { 0 } val LocalEnableBlur = staticCompositionLocalOf { false } val LocalEnableFloatingBottomBar = staticCompositionLocalOf { false } val LocalEnableFloatingBottomBarBlur = staticCompositionLocalOf { false } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/AppIconCache.kt ================================================ package me.weishu.kernelsu.ui.util import android.content.Context import android.content.pm.ApplicationInfo import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.UserHandle import android.util.LruCache import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.toDrawable import androidx.core.graphics.scale import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext object AppIconCache { private val maxMemory = Runtime.getRuntime().maxMemory() / 1024 private val cacheSize = (maxMemory / 8).toInt() private val lruCache = object : LruCache(cacheSize) { override fun sizeOf(key: String, value: Bitmap): Int { return value.byteCount / 1024 } } suspend fun loadIcon(context: Context, applicationInfo: ApplicationInfo, size: Int): Bitmap { val key = "${applicationInfo.packageName}:${applicationInfo.uid}" synchronized(lruCache) { val cachedBitmap = lruCache.get(key) if (cachedBitmap != null) return cachedBitmap } return withContext(Dispatchers.IO) { val pm = context.packageManager var finalDrawable: Drawable? = null try { val appRes = pm.getResourcesForApplication(applicationInfo) val iconId = applicationInfo.icon if (iconId != 0) { val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } BitmapFactory.decodeResource(appRes, iconId, options) if (options.outWidth > size * 6 || options.outHeight > size * 6) { options.inSampleSize = calculateInSampleSize(options, size, size) options.inJustDecodeBounds = false val scaledBitmap = BitmapFactory.decodeResource(appRes, iconId, options) if (scaledBitmap != null) { finalDrawable = scaledBitmap.toDrawable(context.resources) } } } } catch (_: Exception) { // Ignore } if (finalDrawable == null) { finalDrawable = applicationInfo.loadUnbadgedIcon(pm) } // Add system badges val handle = UserHandle.getUserHandleForUid(applicationInfo.uid) val badgedDrawable = try { pm.getUserBadgedIcon(finalDrawable, handle) } catch (_: Exception) { finalDrawable } // Convert to Bitmap for caching val bitmap = if (badgedDrawable is BitmapDrawable) { badgedDrawable.bitmap } else { val w = if (badgedDrawable.intrinsicWidth > 0) badgedDrawable.intrinsicWidth else size val h = if (badgedDrawable.intrinsicHeight > 0) badgedDrawable.intrinsicHeight else size val bmp = createBitmap(w, h) val canvas = Canvas(bmp) badgedDrawable.setBounds(0, 0, canvas.width, canvas.height) badgedDrawable.draw(canvas) bmp } // Resize if too large (consistent with original logic) val resultBitmap = if (bitmap.width > size * 2 || bitmap.height > size * 2) { val scaled = bitmap.scale(size, size) scaled } else { bitmap } synchronized(lruCache) { lruCache.put(key, resultBitmap) } resultBitmap } } private fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int { val height: Int = options.outHeight val width: Int = options.outWidth var inSampleSize = 1 if (height > reqHeight || width > reqWidth) { while ((height / (inSampleSize * 2)) >= reqHeight && (width / (inSampleSize * 2)) >= reqWidth) { inSampleSize *= 2 } } return inSampleSize } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/Colors.kt ================================================ package me.weishu.kernelsu.ui.util import kotlin.math.pow fun cssColorFromArgb(argb: Int): String { val a = ((argb ushr 24) and 0xFF) / 255f val r = (argb ushr 16) and 0xFF val g = (argb ushr 8) and 0xFF val b = argb and 0xFF return "rgba(${r},${g},${b},${"%.3f".format(a)})" } fun mixArgb(c1: Int, c2: Int, ratio: Float): Int { val r1 = (c1 ushr 16) and 0xFF val g1 = (c1 ushr 8) and 0xFF val b1 = c1 and 0xFF val a1 = (c1 ushr 24) and 0xFF val r2 = (c2 ushr 16) and 0xFF val g2 = (c2 ushr 8) and 0xFF val b2 = c2 and 0xFF val a2 = (c2 ushr 24) and 0xFF val r = (r1 * (1 - ratio) + r2 * ratio).toInt().coerceIn(0, 255) val g = (g1 * (1 - ratio) + g2 * ratio).toInt().coerceIn(0, 255) val b = (b1 * (1 - ratio) + b2 * ratio).toInt().coerceIn(0, 255) val a = (a1 * (1 - ratio) + a2 * ratio).toInt().coerceIn(0, 255) return (a shl 24) or (r shl 16) or (g shl 8) or b } fun relativeLuminance(argb: Int): Double { fun linearize(c: Int): Double { val s = c / 255.0 return if (s <= 0.03928) s / 12.92 else ((s + 0.055) / 1.055).pow(2.4) } val r = linearize((argb ushr 16) and 0xFF) val g = linearize((argb ushr 8) and 0xFF) val b = linearize(argb and 0xFF) return 0.2126 * r + 0.7152 * g + 0.0722 * b } fun contrastRatio(a: Int, b: Int): Double { val l1 = relativeLuminance(a) val l2 = relativeLuminance(b) val (hi, lo) = if (l1 >= l2) Pair(l1, l2) else Pair(l2, l1) return (hi + 0.05) / (lo + 0.05) } fun argbToHsl(argb: Int): Triple { val r = ((argb ushr 16) and 0xFF) / 255f val g = ((argb ushr 8) and 0xFF) / 255f val b = (argb and 0xFF) / 255f val max = maxOf(r, g, b) val min = minOf(r, g, b) val l = (max + min) / 2f val d = max - min val s = if (d == 0f) 0f else d / (1f - kotlin.math.abs(2f * l - 1f)) val h = when { d == 0f -> 0f max == r -> ((g - b) / d % 6f) * 60f max == g -> ((b - r) / d + 2f) * 60f else -> ((r - g) / d + 4f) * 60f }.let { if (it < 0f) it + 360f else it } return Triple(h, s, l) } fun hslToArgb(h: Float, s: Float, l: Float, alpha: Int = 0xFF): Int { val c = (1f - kotlin.math.abs(2f * l - 1f)) * s val x = c * (1f - kotlin.math.abs((h / 60f) % 2f - 1f)) val m = l - c / 2f val (r1, g1, b1) = when { h < 60f -> Triple(c, x, 0f) h < 120f -> Triple(x, c, 0f) h < 180f -> Triple(0f, c, x) h < 240f -> Triple(0f, x, c) h < 300f -> Triple(x, 0f, c) else -> Triple(c, 0f, x) } val r = ((r1 + m) * 255f).toInt().coerceIn(0, 255) val g = ((g1 + m) * 255f).toInt().coerceIn(0, 255) val b = ((b1 + m) * 255f).toInt().coerceIn(0, 255) return (alpha shl 24) or (r shl 16) or (g shl 8) or b } fun adjustLightnessArgb(argb: Int, delta: Float): Int { val (h, s, l) = argbToHsl(argb) val nl = (l + delta).coerceIn(0f, 1f) val alpha = (argb ushr 24) and 0xFF return hslToArgb(h, s, nl, alpha) } fun ensureVisibleByMix(original: Int, candidate: Int, minRatio: Double, mixWithWhiteIfLighter: Boolean): Int { if (contrastRatio(original, candidate) >= minRatio) return candidate val target = if (mixWithWhiteIfLighter) 0xFFFFFFFF.toInt() else 0xFF000000.toInt() var lo = 0f var hi = 1f var best = candidate for (i in 0 until 12) { val mid = (lo + hi) / 2f val mixed = mixArgb(candidate, target, mid) if (contrastRatio(original, mixed) >= minRatio) { best = mixed hi = mid } else { lo = mid } } return best } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/CompositionProvider.kt ================================================ package me.weishu.kernelsu.ui.util import androidx.compose.material3.SnackbarHostState import androidx.compose.runtime.compositionLocalOf val LocalSnackbarHost = compositionLocalOf { error("CompositionLocal LocalSnackbarHost not present") } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/Downloader.kt ================================================ package me.weishu.kernelsu.ui.util import android.annotation.SuppressLint import android.net.Uri import android.os.Environment import android.os.Handler import android.os.Looper import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.util.module.LatestVersionInfo import okhttp3.Request import java.io.File import java.io.FileOutputStream import java.io.IOException /** * @author weishu * @date 2023/6/22. */ @SuppressLint("Range") fun download( url: String, fileName: String, onDownloaded: (Uri) -> Unit = {}, onDownloading: () -> Unit = {}, onProgress: (Int) -> Unit = {} ) { onDownloading() Thread { val target = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), fileName) try { ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp -> if (!resp.isSuccessful) throw IOException("HTTP ${resp.code}") val body = resp.body val total = body.contentLength() target.parentFile?.mkdirs() FileOutputStream(target).use { fos -> val buf = ByteArray(8 * 1024) var read: Int var soFar = 0L val source = body.byteStream() while (true) { read = source.read(buf) if (read == -1) break fos.write(buf, 0, read) soFar += read if (total > 0) { val percent = ((soFar * 100L) / total).toInt().coerceIn(0, 100) onProgress(percent) } } fos.flush() } } Handler(Looper.getMainLooper()).post { onDownloaded(Uri.fromFile(target)) } } catch (_: Exception) { // ignore, keep UI state } }.start() } fun checkNewVersion(): LatestVersionInfo { if (!isNetworkAvailable(ksuApp)) return LatestVersionInfo() val url = "https://api.github.com/repos/tiann/KernelSU/releases/latest" // default null value if failed val defaultValue = LatestVersionInfo() runCatching { ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute() .use { response -> if (!response.isSuccessful) { return defaultValue } val body = response.body.string() val json = org.json.JSONObject(body) val changelog = json.optString("body") val assets = json.getJSONArray("assets") for (i in 0 until assets.length()) { val asset = assets.getJSONObject(i) val name = asset.getString("name") if (!name.endsWith(".apk")) { continue } val regex = Regex("v(.+?)_(\\d+)-") val matchResult = regex.find(name) ?: continue matchResult.groupValues[1] val versionCode = matchResult.groupValues[2].toInt() val downloadUrl = asset.getString("browser_download_url") return LatestVersionInfo( versionCode, downloadUrl, changelog ) } } } return defaultValue } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/HanziToPinyin.java ================================================ package me.weishu.kernelsu.ui.util; /* * Copyright (C) 2009 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import android.text.TextUtils; import android.util.Log; import java.text.Collator; import java.util.ArrayList; import java.util.Locale; /** * An object to convert Chinese character to its corresponding pinyin string. For characters with * multiple possible pinyin string, only one is selected according to collator. Polyphone is not * supported in this implementation. This class is implemented to achieve the best runtime * performance and minimum runtime resources with tolerable sacrifice of accuracy. This * implementation highly depends on zh_CN ICU collation data and must be always synchronized with * ICU. *

* Currently this file is aligned to zh.txt in ICU 4.6 */ public class HanziToPinyin { /** * Unihans array. *

* Each unihans is the first one within same pinyin when collator is zh_CN. */ public static final char[] UNIHANS = { '\u963f', '\u54ce', '\u5b89', '\u80ae', '\u51f9', '\u516b', '\u6300', '\u6273', '\u90a6', '\u52f9', '\u9642', '\u5954', '\u4f3b', '\u5c44', '\u8fb9', '\u706c', '\u618b', '\u6c43', '\u51ab', '\u7676', '\u5cec', '\u5693', '\u5072', '\u53c2', '\u4ed3', '\u64a1', '\u518a', '\u5d7e', '\u66fd', '\u66fe', '\u5c64', '\u53c9', '\u8286', '\u8fbf', '\u4f25', '\u6284', '\u8f66', '\u62bb', '\u6c88', '\u6c89', '\u9637', '\u5403', '\u5145', '\u62bd', '\u51fa', '\u6b3b', '\u63e3', '\u5ddb', '\u5205', '\u5439', '\u65fe', '\u9034', '\u5472', '\u5306', '\u51d1', '\u7c97', '\u6c46', '\u5d14', '\u90a8', '\u6413', '\u5491', '\u5446', '\u4e39', '\u5f53', '\u5200', '\u561a', '\u6265', '\u706f', '\u6c10', '\u55f2', '\u7538', '\u5201', '\u7239', '\u4e01', '\u4e1f', '\u4e1c', '\u543a', '\u53be', '\u8011', '\u8968', '\u5428', '\u591a', '\u59b8', '\u8bf6', '\u5940', '\u97a5', '\u513f', '\u53d1', '\u5e06', '\u531a', '\u98de', '\u5206', '\u4e30', '\u8985', '\u4ecf', '\u7d11', '\u4f15', '\u65ee', '\u4f85', '\u7518', '\u5188', '\u768b', '\u6208', '\u7ed9', '\u6839', '\u522f', '\u5de5', '\u52fe', '\u4f30', '\u74dc', '\u4e56', '\u5173', '\u5149', '\u5f52', '\u4e28', '\u5459', '\u54c8', '\u548d', '\u4f44', '\u592f', '\u8320', '\u8bc3', '\u9ed2', '\u62eb', '\u4ea8', '\u5677', '\u53ff', '\u9f41', '\u4e6f', '\u82b1', '\u6000', '\u72bf', '\u5ddf', '\u7070', '\u660f', '\u5419', '\u4e0c', '\u52a0', '\u620b', '\u6c5f', '\u827d', '\u9636', '\u5dfe', '\u5755', '\u5182', '\u4e29', '\u51e5', '\u59e2', '\u5658', '\u519b', '\u5494', '\u5f00', '\u520a', '\u5ffc', '\u5c3b', '\u533c', '\u808e', '\u52a5', '\u7a7a', '\u62a0', '\u625d', '\u5938', '\u84af', '\u5bbd', '\u5321', '\u4e8f', '\u5764', '\u6269', '\u5783', '\u6765', '\u5170', '\u5577', '\u635e', '\u808b', '\u52d2', '\u5d1a', '\u5215', '\u4fe9', '\u5941', '\u826f', '\u64a9', '\u5217', '\u62ce', '\u5222', '\u6e9c', '\u56d6', '\u9f99', '\u779c', '\u565c', '\u5a08', '\u7567', '\u62a1', '\u7f57', '\u5463', '\u5988', '\u57cb', '\u5ada', '\u7264', '\u732b', '\u4e48', '\u5445', '\u95e8', '\u753f', '\u54aa', '\u5b80', '\u55b5', '\u4e5c', '\u6c11', '\u540d', '\u8c2c', '\u6478', '\u54de', '\u6bea', '\u55ef', '\u62cf', '\u8149', '\u56e1', '\u56d4', '\u5b6c', '\u7592', '\u5a1e', '\u6041', '\u80fd', '\u59ae', '\u62c8', '\u5b22', '\u9e1f', '\u634f', '\u56dc', '\u5b81', '\u599e', '\u519c', '\u7fba', '\u5974', '\u597b', '\u759f', '\u9ec1', '\u90cd', '\u5594', '\u8bb4', '\u5991', '\u62cd', '\u7705', '\u4e53', '\u629b', '\u5478', '\u55b7', '\u5309', '\u4e15', '\u56e8', '\u527d', '\u6c15', '\u59d8', '\u4e52', '\u948b', '\u5256', '\u4ec6', '\u4e03', '\u6390', '\u5343', '\u545b', '\u6084', '\u767f', '\u4eb2', '\u72c5', '\u828e', '\u4e18', '\u533a', '\u5cd1', '\u7f3a', '\u590b', '\u5465', '\u7a63', '\u5a06', '\u60f9', '\u4eba', '\u6254', '\u65e5', '\u8338', '\u53b9', '\u909a', '\u633c', '\u5827', '\u5a51', '\u77a4', '\u637c', '\u4ee8', '\u6be2', '\u4e09', '\u6852', '\u63bb', '\u95aa', '\u68ee', '\u50e7', '\u6740', '\u7b5b', '\u5c71', '\u4f24', '\u5f30', '\u5962', '\u7533', '\u8398', '\u6552', '\u5347', '\u5c38', '\u53ce', '\u4e66', '\u5237', '\u8870', '\u95e9', '\u53cc', '\u8c01', '\u542e', '\u8bf4', '\u53b6', '\u5fea', '\u635c', '\u82cf', '\u72fb', '\u590a', '\u5b59', '\u5506', '\u4ed6', '\u56fc', '\u574d', '\u6c64', '\u5932', '\u5fd1', '\u71a5', '\u5254', '\u5929', '\u65eb', '\u5e16', '\u5385', '\u56f2', '\u5077', '\u51f8', '\u6e4d', '\u63a8', '\u541e', '\u4e47', '\u7a75', '\u6b6a', '\u5f2f', '\u5c23', '\u5371', '\u6637', '\u7fc1', '\u631d', '\u4e4c', '\u5915', '\u8672', '\u4eda', '\u4e61', '\u7071', '\u4e9b', '\u5fc3', '\u661f', '\u51f6', '\u4f11', '\u5401', '\u5405', '\u524a', '\u5743', '\u4e2b', '\u6079', '\u592e', '\u5e7a', '\u503b', '\u4e00', '\u56d9', '\u5e94', '\u54df', '\u4f63', '\u4f18', '\u625c', '\u56e6', '\u66f0', '\u6655', '\u7b60', '\u7b7c', '\u5e00', '\u707d', '\u5142', '\u5328', '\u50ae', '\u5219', '\u8d3c', '\u600e', '\u5897', '\u624e', '\u635a', '\u6cbe', '\u5f20', '\u957f', '\u9577', '\u4f4b', '\u8707', '\u8d1e', '\u4e89', '\u4e4b', '\u5cd9', '\u5ea2', '\u4e2d', '\u5dde', '\u6731', '\u6293', '\u62fd', '\u4e13', '\u5986', '\u96b9', '\u5b92', '\u5353', '\u4e72', '\u5b97', '\u90b9', '\u79df', '\u94bb', '\u539c', '\u5c0a', '\u6628', '\u5159', '\u9fc3', '\u9fc4',}; /** * Pinyin array. *

* Each pinyin is corresponding to unihans of same * offset in the unihans array. */ public static final byte[][] PINYINS = { {65, 0, 0, 0, 0, 0}, {65, 73, 0, 0, 0, 0}, {65, 78, 0, 0, 0, 0}, {65, 78, 71, 0, 0, 0}, {65, 79, 0, 0, 0, 0}, {66, 65, 0, 0, 0, 0}, {66, 65, 73, 0, 0, 0}, {66, 65, 78, 0, 0, 0}, {66, 65, 78, 71, 0, 0}, {66, 65, 79, 0, 0, 0}, {66, 69, 73, 0, 0, 0}, {66, 69, 78, 0, 0, 0}, {66, 69, 78, 71, 0, 0}, {66, 73, 0, 0, 0, 0}, {66, 73, 65, 78, 0, 0}, {66, 73, 65, 79, 0, 0}, {66, 73, 69, 0, 0, 0}, {66, 73, 78, 0, 0, 0}, {66, 73, 78, 71, 0, 0}, {66, 79, 0, 0, 0, 0}, {66, 85, 0, 0, 0, 0}, {67, 65, 0, 0, 0, 0}, {67, 65, 73, 0, 0, 0}, {67, 65, 78, 0, 0, 0}, {67, 65, 78, 71, 0, 0}, {67, 65, 79, 0, 0, 0}, {67, 69, 0, 0, 0, 0}, {67, 69, 78, 0, 0, 0}, {67, 69, 78, 71, 0, 0}, {90, 69, 78, 71, 0, 0}, {67, 69, 78, 71, 0, 0}, {67, 72, 65, 0, 0, 0}, {67, 72, 65, 73, 0, 0}, {67, 72, 65, 78, 0, 0}, {67, 72, 65, 78, 71, 0}, {67, 72, 65, 79, 0, 0}, {67, 72, 69, 0, 0, 0}, {67, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 0, 0}, {67, 72, 69, 78, 71, 0}, {67, 72, 73, 0, 0, 0}, {67, 72, 79, 78, 71, 0}, {67, 72, 79, 85, 0, 0}, {67, 72, 85, 0, 0, 0}, {67, 72, 85, 65, 0, 0}, {67, 72, 85, 65, 73, 0}, {67, 72, 85, 65, 78, 0}, {67, 72, 85, 65, 78, 71}, {67, 72, 85, 73, 0, 0}, {67, 72, 85, 78, 0, 0}, {67, 72, 85, 79, 0, 0}, {67, 73, 0, 0, 0, 0}, {67, 79, 78, 71, 0, 0}, {67, 79, 85, 0, 0, 0}, {67, 85, 0, 0, 0, 0}, {67, 85, 65, 78, 0, 0}, {67, 85, 73, 0, 0, 0}, {67, 85, 78, 0, 0, 0}, {67, 85, 79, 0, 0, 0}, {68, 65, 0, 0, 0, 0}, {68, 65, 73, 0, 0, 0}, {68, 65, 78, 0, 0, 0}, {68, 65, 78, 71, 0, 0}, {68, 65, 79, 0, 0, 0}, {68, 69, 0, 0, 0, 0}, {68, 69, 78, 0, 0, 0}, {68, 69, 78, 71, 0, 0}, {68, 73, 0, 0, 0, 0}, {68, 73, 65, 0, 0, 0}, {68, 73, 65, 78, 0, 0}, {68, 73, 65, 79, 0, 0}, {68, 73, 69, 0, 0, 0}, {68, 73, 78, 71, 0, 0}, {68, 73, 85, 0, 0, 0}, {68, 79, 78, 71, 0, 0}, {68, 79, 85, 0, 0, 0}, {68, 85, 0, 0, 0, 0}, {68, 85, 65, 78, 0, 0}, {68, 85, 73, 0, 0, 0}, {68, 85, 78, 0, 0, 0}, {68, 85, 79, 0, 0, 0}, {69, 0, 0, 0, 0, 0}, {69, 73, 0, 0, 0, 0}, {69, 78, 0, 0, 0, 0}, {69, 78, 71, 0, 0, 0}, {69, 82, 0, 0, 0, 0}, {70, 65, 0, 0, 0, 0}, {70, 65, 78, 0, 0, 0}, {70, 65, 78, 71, 0, 0}, {70, 69, 73, 0, 0, 0}, {70, 69, 78, 0, 0, 0}, {70, 69, 78, 71, 0, 0}, {70, 73, 65, 79, 0, 0}, {70, 79, 0, 0, 0, 0}, {70, 79, 85, 0, 0, 0}, {70, 85, 0, 0, 0, 0}, {71, 65, 0, 0, 0, 0}, {71, 65, 73, 0, 0, 0}, {71, 65, 78, 0, 0, 0}, {71, 65, 78, 71, 0, 0}, {71, 65, 79, 0, 0, 0}, {71, 69, 0, 0, 0, 0}, {71, 69, 73, 0, 0, 0}, {71, 69, 78, 0, 0, 0}, {71, 69, 78, 71, 0, 0}, {71, 79, 78, 71, 0, 0}, {71, 79, 85, 0, 0, 0}, {71, 85, 0, 0, 0, 0}, {71, 85, 65, 0, 0, 0}, {71, 85, 65, 73, 0, 0}, {71, 85, 65, 78, 0, 0}, {71, 85, 65, 78, 71, 0}, {71, 85, 73, 0, 0, 0}, {71, 85, 78, 0, 0, 0}, {71, 85, 79, 0, 0, 0}, {72, 65, 0, 0, 0, 0}, {72, 65, 73, 0, 0, 0}, {72, 65, 78, 0, 0, 0}, {72, 65, 78, 71, 0, 0}, {72, 65, 79, 0, 0, 0}, {72, 69, 0, 0, 0, 0}, {72, 69, 73, 0, 0, 0}, {72, 69, 78, 0, 0, 0}, {72, 69, 78, 71, 0, 0}, {72, 77, 0, 0, 0, 0}, {72, 79, 78, 71, 0, 0}, {72, 79, 85, 0, 0, 0}, {72, 85, 0, 0, 0, 0}, {72, 85, 65, 0, 0, 0}, {72, 85, 65, 73, 0, 0}, {72, 85, 65, 78, 0, 0}, {72, 85, 65, 78, 71, 0}, {72, 85, 73, 0, 0, 0}, {72, 85, 78, 0, 0, 0}, {72, 85, 79, 0, 0, 0}, {74, 73, 0, 0, 0, 0}, {74, 73, 65, 0, 0, 0}, {74, 73, 65, 78, 0, 0}, {74, 73, 65, 78, 71, 0}, {74, 73, 65, 79, 0, 0}, {74, 73, 69, 0, 0, 0}, {74, 73, 78, 0, 0, 0}, {74, 73, 78, 71, 0, 0}, {74, 73, 79, 78, 71, 0}, {74, 73, 85, 0, 0, 0}, {74, 85, 0, 0, 0, 0}, {74, 85, 65, 78, 0, 0}, {74, 85, 69, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, {75, 65, 0, 0, 0, 0}, {75, 65, 73, 0, 0, 0}, {75, 65, 78, 0, 0, 0}, {75, 65, 78, 71, 0, 0}, {75, 65, 79, 0, 0, 0}, {75, 69, 0, 0, 0, 0}, {75, 69, 78, 0, 0, 0}, {75, 69, 78, 71, 0, 0}, {75, 79, 78, 71, 0, 0}, {75, 79, 85, 0, 0, 0}, {75, 85, 0, 0, 0, 0}, {75, 85, 65, 0, 0, 0}, {75, 85, 65, 73, 0, 0}, {75, 85, 65, 78, 0, 0}, {75, 85, 65, 78, 71, 0}, {75, 85, 73, 0, 0, 0}, {75, 85, 78, 0, 0, 0}, {75, 85, 79, 0, 0, 0}, {76, 65, 0, 0, 0, 0}, {76, 65, 73, 0, 0, 0}, {76, 65, 78, 0, 0, 0}, {76, 65, 78, 71, 0, 0}, {76, 65, 79, 0, 0, 0}, {76, 69, 0, 0, 0, 0}, {76, 69, 73, 0, 0, 0}, {76, 69, 78, 71, 0, 0}, {76, 73, 0, 0, 0, 0}, {76, 73, 65, 0, 0, 0}, {76, 73, 65, 78, 0, 0}, {76, 73, 65, 78, 71, 0}, {76, 73, 65, 79, 0, 0}, {76, 73, 69, 0, 0, 0}, {76, 73, 78, 0, 0, 0}, {76, 73, 78, 71, 0, 0}, {76, 73, 85, 0, 0, 0}, {76, 79, 0, 0, 0, 0}, {76, 79, 78, 71, 0, 0}, {76, 79, 85, 0, 0, 0}, {76, 85, 0, 0, 0, 0}, {76, 85, 65, 78, 0, 0}, {76, 85, 69, 0, 0, 0}, {76, 85, 78, 0, 0, 0}, {76, 85, 79, 0, 0, 0}, {77, 0, 0, 0, 0, 0}, {77, 65, 0, 0, 0, 0}, {77, 65, 73, 0, 0, 0}, {77, 65, 78, 0, 0, 0}, {77, 65, 78, 71, 0, 0}, {77, 65, 79, 0, 0, 0}, {77, 69, 0, 0, 0, 0}, {77, 69, 73, 0, 0, 0}, {77, 69, 78, 0, 0, 0}, {77, 69, 78, 71, 0, 0}, {77, 73, 0, 0, 0, 0}, {77, 73, 65, 78, 0, 0}, {77, 73, 65, 79, 0, 0}, {77, 73, 69, 0, 0, 0}, {77, 73, 78, 0, 0, 0}, {77, 73, 78, 71, 0, 0}, {77, 73, 85, 0, 0, 0}, {77, 79, 0, 0, 0, 0}, {77, 79, 85, 0, 0, 0}, {77, 85, 0, 0, 0, 0}, {78, 0, 0, 0, 0, 0}, {78, 65, 0, 0, 0, 0}, {78, 65, 73, 0, 0, 0}, {78, 65, 78, 0, 0, 0}, {78, 65, 78, 71, 0, 0}, {78, 65, 79, 0, 0, 0}, {78, 69, 0, 0, 0, 0}, {78, 69, 73, 0, 0, 0}, {78, 69, 78, 0, 0, 0}, {78, 69, 78, 71, 0, 0}, {78, 73, 0, 0, 0, 0}, {78, 73, 65, 78, 0, 0}, {78, 73, 65, 78, 71, 0}, {78, 73, 65, 79, 0, 0}, {78, 73, 69, 0, 0, 0}, {78, 73, 78, 0, 0, 0}, {78, 73, 78, 71, 0, 0}, {78, 73, 85, 0, 0, 0}, {78, 79, 78, 71, 0, 0}, {78, 79, 85, 0, 0, 0}, {78, 85, 0, 0, 0, 0}, {78, 85, 65, 78, 0, 0}, {78, 85, 69, 0, 0, 0}, {78, 85, 78, 0, 0, 0}, {78, 85, 79, 0, 0, 0}, {79, 0, 0, 0, 0, 0}, {79, 85, 0, 0, 0, 0}, {80, 65, 0, 0, 0, 0}, {80, 65, 73, 0, 0, 0}, {80, 65, 78, 0, 0, 0}, {80, 65, 78, 71, 0, 0}, {80, 65, 79, 0, 0, 0}, {80, 69, 73, 0, 0, 0}, {80, 69, 78, 0, 0, 0}, {80, 69, 78, 71, 0, 0}, {80, 73, 0, 0, 0, 0}, {80, 73, 65, 78, 0, 0}, {80, 73, 65, 79, 0, 0}, {80, 73, 69, 0, 0, 0}, {80, 73, 78, 0, 0, 0}, {80, 73, 78, 71, 0, 0}, {80, 79, 0, 0, 0, 0}, {80, 79, 85, 0, 0, 0}, {80, 85, 0, 0, 0, 0}, {81, 73, 0, 0, 0, 0}, {81, 73, 65, 0, 0, 0}, {81, 73, 65, 78, 0, 0}, {81, 73, 65, 78, 71, 0}, {81, 73, 65, 79, 0, 0}, {81, 73, 69, 0, 0, 0}, {81, 73, 78, 0, 0, 0}, {81, 73, 78, 71, 0, 0}, {81, 73, 79, 78, 71, 0}, {81, 73, 85, 0, 0, 0}, {81, 85, 0, 0, 0, 0}, {81, 85, 65, 78, 0, 0}, {81, 85, 69, 0, 0, 0}, {81, 85, 78, 0, 0, 0}, {82, 65, 78, 0, 0, 0}, {82, 65, 78, 71, 0, 0}, {82, 65, 79, 0, 0, 0}, {82, 69, 0, 0, 0, 0}, {82, 69, 78, 0, 0, 0}, {82, 69, 78, 71, 0, 0}, {82, 73, 0, 0, 0, 0}, {82, 79, 78, 71, 0, 0}, {82, 79, 85, 0, 0, 0}, {82, 85, 0, 0, 0, 0}, {82, 85, 65, 0, 0, 0}, {82, 85, 65, 78, 0, 0}, {82, 85, 73, 0, 0, 0}, {82, 85, 78, 0, 0, 0}, {82, 85, 79, 0, 0, 0}, {83, 65, 0, 0, 0, 0}, {83, 65, 73, 0, 0, 0}, {83, 65, 78, 0, 0, 0}, {83, 65, 78, 71, 0, 0}, {83, 65, 79, 0, 0, 0}, {83, 69, 0, 0, 0, 0}, {83, 69, 78, 0, 0, 0}, {83, 69, 78, 71, 0, 0}, {83, 72, 65, 0, 0, 0}, {83, 72, 65, 73, 0, 0}, {83, 72, 65, 78, 0, 0}, {83, 72, 65, 78, 71, 0}, {83, 72, 65, 79, 0, 0}, {83, 72, 69, 0, 0, 0}, {83, 72, 69, 78, 0, 0}, {88, 73, 78, 0, 0, 0}, {83, 72, 69, 78, 0, 0}, {83, 72, 69, 78, 71, 0}, {83, 72, 73, 0, 0, 0}, {83, 72, 79, 85, 0, 0}, {83, 72, 85, 0, 0, 0}, {83, 72, 85, 65, 0, 0}, {83, 72, 85, 65, 73, 0}, {83, 72, 85, 65, 78, 0}, {83, 72, 85, 65, 78, 71}, {83, 72, 85, 73, 0, 0}, {83, 72, 85, 78, 0, 0}, {83, 72, 85, 79, 0, 0}, {83, 73, 0, 0, 0, 0}, {83, 79, 78, 71, 0, 0}, {83, 79, 85, 0, 0, 0}, {83, 85, 0, 0, 0, 0}, {83, 85, 65, 78, 0, 0}, {83, 85, 73, 0, 0, 0}, {83, 85, 78, 0, 0, 0}, {83, 85, 79, 0, 0, 0}, {84, 65, 0, 0, 0, 0}, {84, 65, 73, 0, 0, 0}, {84, 65, 78, 0, 0, 0}, {84, 65, 78, 71, 0, 0}, {84, 65, 79, 0, 0, 0}, {84, 69, 0, 0, 0, 0}, {84, 69, 78, 71, 0, 0}, {84, 73, 0, 0, 0, 0}, {84, 73, 65, 78, 0, 0}, {84, 73, 65, 79, 0, 0}, {84, 73, 69, 0, 0, 0}, {84, 73, 78, 71, 0, 0}, {84, 79, 78, 71, 0, 0}, {84, 79, 85, 0, 0, 0}, {84, 85, 0, 0, 0, 0}, {84, 85, 65, 78, 0, 0}, {84, 85, 73, 0, 0, 0}, {84, 85, 78, 0, 0, 0}, {84, 85, 79, 0, 0, 0}, {87, 65, 0, 0, 0, 0}, {87, 65, 73, 0, 0, 0}, {87, 65, 78, 0, 0, 0}, {87, 65, 78, 71, 0, 0}, {87, 69, 73, 0, 0, 0}, {87, 69, 78, 0, 0, 0}, {87, 69, 78, 71, 0, 0}, {87, 79, 0, 0, 0, 0}, {87, 85, 0, 0, 0, 0}, {88, 73, 0, 0, 0, 0}, {88, 73, 65, 0, 0, 0}, {88, 73, 65, 78, 0, 0}, {88, 73, 65, 78, 71, 0}, {88, 73, 65, 79, 0, 0}, {88, 73, 69, 0, 0, 0}, {88, 73, 78, 0, 0, 0}, {88, 73, 78, 71, 0, 0}, {88, 73, 79, 78, 71, 0}, {88, 73, 85, 0, 0, 0}, {88, 85, 0, 0, 0, 0}, {88, 85, 65, 78, 0, 0}, {88, 85, 69, 0, 0, 0}, {88, 85, 78, 0, 0, 0}, {89, 65, 0, 0, 0, 0}, {89, 65, 78, 0, 0, 0}, {89, 65, 78, 71, 0, 0}, {89, 65, 79, 0, 0, 0}, {89, 69, 0, 0, 0, 0}, {89, 73, 0, 0, 0, 0}, {89, 73, 78, 0, 0, 0}, {89, 73, 78, 71, 0, 0}, {89, 79, 0, 0, 0, 0}, {89, 79, 78, 71, 0, 0}, {89, 79, 85, 0, 0, 0}, {89, 85, 0, 0, 0, 0}, {89, 85, 65, 78, 0, 0}, {89, 85, 69, 0, 0, 0}, {89, 85, 78, 0, 0, 0}, {74, 85, 78, 0, 0, 0}, {89, 85, 78, 0, 0, 0}, {90, 65, 0, 0, 0, 0}, {90, 65, 73, 0, 0, 0}, {90, 65, 78, 0, 0, 0}, {90, 65, 78, 71, 0, 0}, {90, 65, 79, 0, 0, 0}, {90, 69, 0, 0, 0, 0}, {90, 69, 73, 0, 0, 0}, {90, 69, 78, 0, 0, 0}, {90, 69, 78, 71, 0, 0}, {90, 72, 65, 0, 0, 0}, {90, 72, 65, 73, 0, 0}, {90, 72, 65, 78, 0, 0}, {90, 72, 65, 78, 71, 0}, {67, 72, 65, 78, 71, 0}, {90, 72, 65, 78, 71, 0}, {90, 72, 65, 79, 0, 0}, {90, 72, 69, 0, 0, 0}, {90, 72, 69, 78, 0, 0}, {90, 72, 69, 78, 71, 0}, {90, 72, 73, 0, 0, 0}, {83, 72, 73, 0, 0, 0}, {90, 72, 73, 0, 0, 0}, {90, 72, 79, 78, 71, 0}, {90, 72, 79, 85, 0, 0}, {90, 72, 85, 0, 0, 0}, {90, 72, 85, 65, 0, 0}, {90, 72, 85, 65, 73, 0}, {90, 72, 85, 65, 78, 0}, {90, 72, 85, 65, 78, 71}, {90, 72, 85, 73, 0, 0}, {90, 72, 85, 78, 0, 0}, {90, 72, 85, 79, 0, 0}, {90, 73, 0, 0, 0, 0}, {90, 79, 78, 71, 0, 0}, {90, 79, 85, 0, 0, 0}, {90, 85, 0, 0, 0, 0}, {90, 85, 65, 78, 0, 0}, {90, 85, 73, 0, 0, 0}, {90, 85, 78, 0, 0, 0}, {90, 85, 79, 0, 0, 0}, {0, 0, 0, 0, 0, 0}, {83, 72, 65, 78, 0, 0}, {0, 0, 0, 0, 0, 0},}; private static final String TAG = "HanziToPinyin"; // Turn on this flag when we want to check internal data structure. private static final boolean DEBUG = false; /** * First and last Chinese character with known Pinyin according to zh collation */ private static final String FIRST_PINYIN_UNIHAN = "\u963F"; private static final String LAST_PINYIN_UNIHAN = "\u9FFF"; private static final Collator COLLATOR = Collator.getInstance(Locale.CHINA); private static HanziToPinyin sInstance; private final boolean mHasChinaCollator; protected HanziToPinyin(boolean hasChinaCollator) { mHasChinaCollator = hasChinaCollator; } public static HanziToPinyin getInstance() { synchronized (HanziToPinyin.class) { if (sInstance != null) { return sInstance; } // Check if zh_CN collation data is available final Locale[] locale = Collator.getAvailableLocales(); for (Locale value : locale) { if (value.equals(Locale.CHINA) || value.getLanguage().contains("zh")) { // Do self validation just once. if (DEBUG) { Log.d(TAG, "Self validation. Result: " + doSelfValidation()); } sInstance = new HanziToPinyin(true); return sInstance; } } if (sInstance == null) {//这个判断是用于处理国产ROM的兼容性问题 if (Locale.CHINA.equals(Locale.getDefault())) { sInstance = new HanziToPinyin(true); return sInstance; } } Log.w(TAG, "There is no Chinese collator, HanziToPinyin is disabled"); sInstance = new HanziToPinyin(false); return sInstance; } } /** * Validate if our internal table has some wrong value. * * @return true when the table looks correct. */ private static boolean doSelfValidation() { char lastChar = UNIHANS[0]; String lastString = Character.toString(lastChar); for (char c : UNIHANS) { if (lastChar == c) { continue; } final String curString = Character.toString(c); int cmp = COLLATOR.compare(lastString, curString); if (cmp >= 0) { Log.e(TAG, "Internal error in Unihan table. " + "The last string \"" + lastString + "\" is greater than current string \"" + curString + "\"."); return false; } lastString = curString; } return true; } private Token getToken(char character) { Token token = new Token(); final String letter = Character.toString(character); token.source = letter; int offset = -1; int cmp; if (character < 256) { token.type = Token.LATIN; token.target = letter; return token; } else { cmp = COLLATOR.compare(letter, FIRST_PINYIN_UNIHAN); if (cmp < 0) { token.type = Token.UNKNOWN; token.target = letter; return token; } else if (cmp == 0) { token.type = Token.PINYIN; offset = 0; } else { cmp = COLLATOR.compare(letter, LAST_PINYIN_UNIHAN); if (cmp > 0) { token.type = Token.UNKNOWN; token.target = letter; return token; } else if (cmp == 0) { token.type = Token.PINYIN; offset = UNIHANS.length - 1; } } } token.type = Token.PINYIN; if (offset < 0) { int begin = 0; int end = UNIHANS.length - 1; while (begin <= end) { offset = (begin + end) / 2; final String unihan = Character.toString(UNIHANS[offset]); cmp = COLLATOR.compare(letter, unihan); if (cmp == 0) { break; } else if (cmp > 0) { begin = offset + 1; } else { end = offset - 1; } } } if (cmp < 0) { offset--; } StringBuilder pinyin = new StringBuilder(); for (int j = 0; j < PINYINS[offset].length && PINYINS[offset][j] != 0; j++) { pinyin.append((char) PINYINS[offset][j]); } token.target = pinyin.toString(); if (TextUtils.isEmpty(token.target)) { token.type = Token.UNKNOWN; token.target = token.source; } return token; } /** * Convert the input to a array of tokens. The sequence of ASCII or Unknown characters without * space will be put into a Token, One Hanzi character which has pinyin will be treated as a * Token. If these is no China collator, the empty token array is returned. */ public ArrayList get(final String input) { ArrayList tokens = new ArrayList<>(); if (!mHasChinaCollator || TextUtils.isEmpty(input)) { // return empty tokens. return tokens; } final int inputLength = input.length(); final StringBuilder sb = new StringBuilder(); int tokenType = Token.LATIN; // Go through the input, create a new token when // a. Token type changed // b. Get the Pinyin of current charater. // c. current character is space. for (int i = 0; i < inputLength; i++) { final char character = input.charAt(i); if (character == ' ') { if (sb.length() > 0) { addToken(sb, tokens, tokenType); } } else if (character < 256) { if (tokenType != Token.LATIN && sb.length() > 0) { addToken(sb, tokens, tokenType); } tokenType = Token.LATIN; sb.append(character); } else { Token t = getToken(character); if (t.type == Token.PINYIN) { if (sb.length() > 0) { addToken(sb, tokens, tokenType); } tokens.add(t); tokenType = Token.PINYIN; } else { if (tokenType != t.type && sb.length() > 0) { addToken(sb, tokens, tokenType); } tokenType = t.type; sb.append(character); } } } if (sb.length() > 0) { addToken(sb, tokens, tokenType); } return tokens; } private void addToken( final StringBuilder sb, final ArrayList tokens, final int tokenType) { String str = sb.toString(); tokens.add(new Token(tokenType, str, str)); sb.setLength(0); } public String toPinyinString(String string) { if (string == null) { return null; } StringBuilder sb = new StringBuilder(); ArrayList tokens = get(string); for (Token token : tokens) { sb.append(token.target); } return sb.toString().toLowerCase(); } public static class Token { /** * Separator between target string for each source char */ public static final String SEPARATOR = " "; public static final int LATIN = 1; public static final int PINYIN = 2; public static final int UNKNOWN = 3; /** * Type of this token, ASCII, PINYIN or UNKNOWN. */ public int type; /** * Original string before translation. */ public String source; /** * Translated string of source. For Han, target is corresponding Pinyin. Otherwise target is * original string in source. */ public String target; public Token() { } public Token(int type, String source, String target) { this.type = type; this.source = source; this.target = target; } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/HazeExt.kt ================================================ package me.weishu.kernelsu.ui.util import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.ExperimentalHazeApi import dev.chrisbanes.haze.HazeInputScale import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.HazeStyle import dev.chrisbanes.haze.hazeEffect @OptIn(ExperimentalHazeApi::class) fun Modifier.defaultHazeEffect( hazeState: HazeState, hazeStyle: HazeStyle, ): Modifier = this.hazeEffect( state = hazeState, style = hazeStyle ) { blurRadius = 20.dp inputScale = HazeInputScale.Fixed(0.25f) noiseFactor = 0f forceInvalidateOnPreDraw = false } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/KsuCli.kt ================================================ package me.weishu.kernelsu.ui.util import android.content.ContentResolver import android.content.Context import android.database.Cursor import android.net.Uri import android.os.Environment import android.os.Parcelable import android.os.SystemClock import android.provider.OpenableColumns import android.system.Os import android.util.Log import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp import org.json.JSONArray import java.io.File /** * @author weishu * @date 2023/1/1. */ private const val TAG = "KsuCli" private fun getKsuDaemonPath(): String { return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so" } data class FlashResult(val code: Int, val err: String, val showReboot: Boolean) { constructor(result: Shell.Result, showReboot: Boolean) : this(result.code, result.err.joinToString("\n"), showReboot) constructor(result: Shell.Result) : this(result, result.isSuccess) } object KsuCli { val SHELL: Shell = createRootShell() val GLOBAL_MNT_SHELL: Shell = createRootShell(true) } fun getRootShell(globalMnt: Boolean = false): Shell { return if (globalMnt) KsuCli.GLOBAL_MNT_SHELL else { KsuCli.SHELL } } inline fun withNewRootShell( globalMnt: Boolean = false, block: Shell.() -> T ): T { return createRootShell(globalMnt).use(block) } fun Uri.getFileName(context: Context): String? { var fileName: String? = null val contentResolver: ContentResolver = context.contentResolver val cursor: Cursor? = contentResolver.query(this, null, null, null, null) cursor?.use { if (it.moveToFirst()) { fileName = it.getString(it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)) } } return fileName } fun createRootShell(globalMnt: Boolean = false): Shell { Shell.enableVerboseLogging = BuildConfig.DEBUG val builder = Shell.Builder.create() return try { if (globalMnt) { builder.build(getKsuDaemonPath(), "debug", "su", "-g") } else { builder.build(getKsuDaemonPath(), "debug", "su") } } catch (e: Throwable) { Log.w(TAG, "ksu failed: ", e) try { if (globalMnt) { builder.build("su", "-mm") } else { builder.build("su") } } catch (e: Throwable) { Log.e(TAG, "su failed: ", e) builder.build("sh") } } } fun execKsud(args: String, newShell: Boolean = false): Boolean { return if (newShell) { withNewRootShell { ShellUtils.fastCmdResult(this, "${getKsuDaemonPath()} $args") } } else { ShellUtils.fastCmdResult(getRootShell(), "${getKsuDaemonPath()} $args") } } suspend fun getFeatureStatus(feature: String): String = withContext(Dispatchers.IO) { val shell = getRootShell() val out = shell.newJob() .add("${getKsuDaemonPath()} feature check $feature").to(ArrayList(), null).exec().out out.firstOrNull()?.trim().orEmpty() } suspend fun getFeaturePersistValue(feature: String): Long? = withContext(Dispatchers.IO) { val shell = getRootShell() val out = shell.newJob() .add("${getKsuDaemonPath()} feature get --config $feature").to(ArrayList(), null).exec().out val valueLine = out.firstOrNull { it.trim().startsWith("Value:") } ?: return@withContext null valueLine.substringAfter("Value:").trim().toLongOrNull() } fun install() { val start = SystemClock.elapsedRealtime() val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so").absolutePath val result = execKsud("install --magiskboot $magiskboot", true) Log.w(TAG, "install result: $result, cost: ${SystemClock.elapsedRealtime() - start}ms") } fun listModules(): String { val shell = getRootShell() val out = shell.newJob() .add("${getKsuDaemonPath()} module list").to(ArrayList(), null).exec().out return out.joinToString("\n").ifBlank { "[]" } } fun getModuleCount(): Int { val result = listModules() runCatching { val array = JSONArray(result) return array.length() }.getOrElse { return 0 } } fun getSuperuserCount(): Int { return Natives.getSuperuserCount() } fun toggleModule(id: String, enable: Boolean): Boolean { val cmd = if (enable) { "module enable $id" } else { "module disable $id" } val result = execKsud(cmd, true) Log.i(TAG, "$cmd result: $result") return result } fun undoUninstallModule(id: String): Boolean { val cmd = "module undo-uninstall $id" val result = execKsud(cmd, true) Log.i(TAG, "undo uninstall module $id result: $result") return result } fun uninstallModule(id: String): Boolean { val cmd = "module uninstall $id" val result = execKsud(cmd, true) Log.i(TAG, "uninstall module $id result: $result") return result } private fun flashWithIO( cmd: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Shell.Result { val stdoutCallback: CallbackList = object : CallbackList() { override fun onAddElement(s: String?) { onStdout(s ?: "") } } val stderrCallback: CallbackList = object : CallbackList() { override fun onAddElement(s: String?) { onStderr(s ?: "") } } return withNewRootShell { newJob().add(cmd).to(stdoutCallback, stderrCallback).exec() } } fun flashModule( uri: Uri, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): FlashResult { val resolver = ksuApp.contentResolver with(resolver.openInputStream(uri)) { val file = File(ksuApp.cacheDir, "module.zip") file.outputStream().use { output -> this?.copyTo(output) } val cmd = "module install ${file.absolutePath}" val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) Log.i("KernelSU", "install module $uri result: $result") file.delete() return FlashResult(result) } } fun runModuleAction( moduleId: String, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { val shell = createRootShell(true) val stdoutCallback: CallbackList = object : CallbackList() { override fun onAddElement(s: String?) { onStdout(s ?: "") } } val stderrCallback: CallbackList = object : CallbackList() { override fun onAddElement(s: String?) { onStderr(s ?: "") } } val result = shell.newJob().add("${getKsuDaemonPath()} module action $moduleId") .to(stdoutCallback, stderrCallback).exec() Log.i("KernelSU", "Module runAction result: $result") return result.isSuccess } fun restoreBoot( onStdout: (String) -> Unit, onStderr: (String) -> Unit ): FlashResult { val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO("${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, onStderr) return FlashResult(result) } fun uninstallPermanently( onStdout: (String) -> Unit, onStderr: (String) -> Unit ): FlashResult { val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) return FlashResult(result) } @Parcelize sealed class LkmSelection : Parcelable { @Parcelize data class LkmUri(val uri: Uri) : LkmSelection() @Parcelize data class KmiString(val value: String) : LkmSelection() @Parcelize data object KmiNone : LkmSelection() } fun installBoot( bootUri: Uri?, lkm: LkmSelection, ota: Boolean, partition: String?, allowShell: Boolean, enableAdb: Boolean, onStdout: (String) -> Unit, onStderr: (String) -> Unit, ): FlashResult { val resolver = ksuApp.contentResolver val bootFile = bootUri?.let { uri -> with(resolver.openInputStream(uri)) { val bootFile = File(ksuApp.cacheDir, "boot.img") bootFile.outputStream().use { output -> this?.copyTo(output) } bootFile } } val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" cmd += if (bootFile == null) { // no boot.img, use -f to flash " -f" } else { " -b ${bootFile.absolutePath}" } if (allowShell) { cmd += " --allow-shell" } if (enableAdb) { cmd += " --enable-adbd" } if (ota) { cmd += " -u" } var lkmFile: File? = null when (lkm) { is LkmSelection.LkmUri -> { lkmFile = with(resolver.openInputStream(lkm.uri)) { val file = File(ksuApp.cacheDir, "kernelsu-tmp-lkm.ko") file.outputStream().use { output -> this?.copyTo(output) } file } cmd += " -m ${lkmFile.absolutePath}" } is LkmSelection.KmiString -> { cmd += " --kmi ${lkm.value}" } LkmSelection.KmiNone -> { // do nothing } } // output dir if (bootFile != null) { val downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS) cmd += " -o $downloadsDir" } partition?.let { part -> cmd += " --partition $part" } val result = flashWithIO("${getKsuDaemonPath()} $cmd", onStdout, onStderr) Log.i("KernelSU", "install boot result: ${result.isSuccess}") bootFile?.delete() lkmFile?.delete() // if boot uri is empty, it is direct install, when success, we should show reboot button val showReboot = bootUri == null && result.isSuccess // we create a temporary val here, to avoid calc showReboot double if (showReboot) { // because we decide do not update ksud when startActivity install() // install ksud here } return FlashResult(result, showReboot) } fun reboot(reason: String = "") { val shell = getRootShell() if (reason == "soft_reboot") { ShellUtils.fastCmd(shell, "setprop ctl.restart zygote") return } if (reason == "recovery") { // KEYCODE_POWER = 26, hide incorrect "Factory data reset" message ShellUtils.fastCmd(shell, "/system/bin/input keyevent 26") } ShellUtils.fastCmd(shell, "/system/bin/svc power reboot $reason || /system/bin/reboot $reason") } fun rootAvailable(): Boolean { val shell = getRootShell() return shell.isRoot } suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = "boot-info current-kmi" ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd") } suspend fun getSupportedKmis(): List = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = "boot-info supported-kmis" val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out out.filter { it.isNotBlank() }.map { it.trim() } } suspend fun isAbDevice(): Boolean = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = "boot-info is-ab-device" ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim().toBoolean() } suspend fun getDefaultPartition(): String = withContext(Dispatchers.IO) { val shell = getRootShell() if (shell.isRoot) { val cmd = "boot-info default-partition" ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() } else { if (!Os.uname().release.contains("android12-")) "init_boot" else "boot" } } suspend fun getSlotSuffix(ota: Boolean): String = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = if (ota) { "boot-info slot-suffix --ota" } else { "boot-info slot-suffix" } ShellUtils.fastCmd(shell, "${getKsuDaemonPath()} $cmd").trim() } suspend fun getAvailablePartitions(): List = withContext(Dispatchers.IO) { val shell = getRootShell() val cmd = "boot-info available-partitions" val out = shell.newJob().add("${getKsuDaemonPath()} $cmd").to(ArrayList(), null).exec().out out.filter { it.isNotBlank() }.map { it.trim() } } fun hasMagisk(): Boolean { val shell = getRootShell(true) val result = shell.newJob().add("which magisk").exec() Log.i(TAG, "has magisk: ${result.isSuccess}") return result.isSuccess } fun isSepolicyValid(rules: String?): Boolean { if (rules == null) { return true } val shell = getRootShell() val result = shell.newJob().add("${getKsuDaemonPath()} sepolicy check '$rules'").to(ArrayList(), null) .exec() return result.isSuccess } fun getSepolicy(pkg: String): String { val shell = getRootShell() val result = shell.newJob().add("${getKsuDaemonPath()} profile get-sepolicy $pkg").to(ArrayList(), null) .exec() Log.i(TAG, "code: ${result.code}, out: ${result.out}, err: ${result.err}") return result.out.joinToString("\n") } fun setSepolicy(pkg: String, rules: String): Boolean { val shell = getRootShell() val result = shell.newJob().add("${getKsuDaemonPath()} profile set-sepolicy $pkg '$rules'") .to(ArrayList(), null).exec() Log.i(TAG, "set sepolicy result: ${result.code}") return result.isSuccess } fun listAppProfileTemplates(): List { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile list-templates").to(ArrayList(), null) .exec().out } fun getAppProfileTemplate(id: String): String { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile get-template '${id}'") .to(ArrayList(), null).exec().out.joinToString("\n") } fun setAppProfileTemplate(id: String, template: String): Boolean { val shell = getRootShell() val escapedTemplate = template.replace("\"", "\\\"") val cmd = """${getKsuDaemonPath()} profile set-template "$id" "$escapedTemplate'"""" return shell.newJob().add(cmd) .to(ArrayList(), null).exec().isSuccess } fun deleteAppProfileTemplate(id: String): Boolean { val shell = getRootShell() return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") .to(ArrayList(), null).exec().isSuccess } fun forceStopApp(packageName: String, userId: Int? = null) { val shell = getRootShell() val userArg = userId?.let { " --user $it" } ?: "" val result = shell.newJob().add("am force-stop$userArg $packageName").exec() Log.i(TAG, "force stop $packageName result: $result") } fun launchApp(packageName: String, userId: Int? = null) { val shell = getRootShell() val userArg = userId?.let { " --user $it" } ?: "" val result = shell.newJob() .add("cmd package resolve-activity --brief$userArg $packageName | tail -n 1 | xargs cmd activity start-activity$userArg -n") .exec() Log.i(TAG, "launch $packageName result: $result") } fun restartApp(packageName: String, userId: Int? = null) { forceStopApp(packageName, userId) launchApp(packageName, userId) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/LogEvent.kt ================================================ package me.weishu.kernelsu.ui.util import android.content.Context import android.os.Build import android.system.Os import com.topjohnwu.superuser.ShellUtils import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ui.screen.home.getManagerVersion import java.io.File import java.io.FileWriter import java.io.PrintWriter import java.time.LocalDateTime import java.time.format.DateTimeFormatter fun getBugreportFile(context: Context): File { val bugreportDir = File(context.cacheDir, "bugreport") bugreportDir.mkdirs() val processFile = File(bugreportDir, "process.txt") val dmesgFile = File(bugreportDir, "dmesg.txt") val logcatFile = File(bugreportDir, "logcat.txt") val tombstonesFile = File(bugreportDir, "tombstones.tar.gz") val dropboxFile = File(bugreportDir, "dropbox.tar.gz") val pstoreFile = File(bugreportDir, "pstore.tar.gz") // Xiaomi/Readmi devices have diag in /data/vendor/diag val diagFile = File(bugreportDir, "diag.tar.gz") val oplusFile = File(bugreportDir, "oplus.tar.gz") val bootlogFile = File(bugreportDir, "bootlog.tar.gz") val mountsFile = File(bugreportDir, "mounts.txt") val fileSystemsFile = File(bugreportDir, "filesystems.txt") val adbFileTree = File(bugreportDir, "adb_tree.txt") val adbFileDetails = File(bugreportDir, "adb_details.txt") val ksuFileSize = File(bugreportDir, "ksu_size.txt") val appListFile = File(bugreportDir, "packages.txt") val propFile = File(bugreportDir, "props.txt") val allowListFile = File(bugreportDir, "allowlist.bin") val procModules = File(bugreportDir, "proc_modules.txt") val bootConfig = File(bugreportDir, "boot_config.txt") val kernelConfig = File(bugreportDir, "defconfig.gz") val shell = getRootShell(true) // busybox ps has very few features for embed devices shell.newJob().add("toybox ps -T -A -w -o PID,TID,UID,COMM,CMDLINE,CMD,LABEL,STAT,WCHAN > ${processFile.absolutePath}").exec() shell.newJob().add("dmesg -r > ${dmesgFile.absolutePath}").exec() shell.newJob().add("logcat -b all -v uid -d > ${logcatFile.absolutePath}").exec() shell.newJob().add("tar -czf ${tombstonesFile.absolutePath} -C /data/tombstones .").exec() shell.newJob().add("tar -czf ${dropboxFile.absolutePath} -C /data/system/dropbox .").exec() shell.newJob().add("tar -czf ${pstoreFile.absolutePath} -C /sys/fs/pstore .").exec() shell.newJob().add("tar -czf ${diagFile.absolutePath} -C /data/vendor/diag . --exclude=./minidump.gz").exec() shell.newJob().add("tar -czf ${oplusFile.absolutePath} -C /mnt/oplus/op2/media/log/boot_log/ .").exec() shell.newJob().add("tar -czf ${bootlogFile.absolutePath} -C /data/adb/ksu/log .").exec() shell.newJob().add("cat /proc/1/mountinfo > ${mountsFile.absolutePath}").exec() shell.newJob().add("cat /proc/filesystems > ${fileSystemsFile.absolutePath}").exec() shell.newJob().add("busybox tree /data/adb > ${adbFileTree.absolutePath}").exec() shell.newJob().add("ls -alRZ /data/adb > ${adbFileDetails.absolutePath}").exec() shell.newJob().add("du -sh /data/adb/ksu/* > ${ksuFileSize.absolutePath}").exec() shell.newJob().add("cp /data/system/packages.list ${appListFile.absolutePath}").exec() shell.newJob().add("getprop > ${propFile.absolutePath}").exec() shell.newJob().add("cp /data/adb/ksu/.allowlist ${allowListFile.absolutePath}").exec() shell.newJob().add("cp /proc/modules ${procModules.absolutePath}").exec() shell.newJob().add("cp /proc/bootconfig ${bootConfig.absolutePath}").exec() shell.newJob().add("cp /proc/config.gz ${kernelConfig.absolutePath}").exec() val selinux = ShellUtils.fastCmd(shell, "getenforce") // basic information val buildInfo = File(bugreportDir, "basic.txt") PrintWriter(FileWriter(buildInfo)).use { pw -> pw.println("Kernel: ${System.getProperty("os.version")}") pw.println("BRAND: " + Build.BRAND) pw.println("MODEL: " + Build.MODEL) pw.println("PRODUCT: " + Build.PRODUCT) pw.println("MANUFACTURER: " + Build.MANUFACTURER) pw.println("SDK: " + Build.VERSION.SDK_INT) pw.println("PREVIEW_SDK: " + Build.VERSION.PREVIEW_SDK_INT) pw.println("FINGERPRINT: " + Build.FINGERPRINT) pw.println("DEVICE: " + Build.DEVICE) pw.println("Manager: " + getManagerVersion(context)) pw.println("SELinux: $selinux") val uname = Os.uname() pw.println("KernelRelease: ${uname.release}") pw.println("KernelVersion: ${uname.version}") pw.println("Machine: ${uname.machine}") pw.println("Nodename: ${uname.nodename}") pw.println("Sysname: ${uname.sysname}") val ksuKernel = Natives.version pw.println("KernelSU: $ksuKernel") val safeMode = Natives.isSafeMode pw.println("SafeMode: $safeMode") val lkmMode = Natives.isLkmMode pw.println("LKM: $lkmMode") } // modules val modulesFile = File(bugreportDir, "modules.json") modulesFile.writeText(listModules()) val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH_mm") val current = LocalDateTime.now().format(formatter) val targetFile = File(context.cacheDir, "KernelSU_bugreport_${current}.tar.gz") shell.newJob().add("tar czf ${targetFile.absolutePath} -C ${bugreportDir.absolutePath} .").exec() shell.newJob().add("rm -rf ${bugreportDir.absolutePath}").exec() shell.newJob().add("chmod 0644 ${targetFile.absolutePath}").exec() return targetFile } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/Network.kt ================================================ package me.weishu.kernelsu.ui.util import android.content.Context import android.net.ConnectivityManager import android.net.NetworkCapabilities fun isNetworkAvailable(context: Context): Boolean { val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager val network = cm.activeNetwork ?: return false val caps = cm.getNetworkCapabilities(network) ?: return false val hasTransport = caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) || caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) || caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) return hasTransport && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/OemHelper.kt ================================================ package me.weishu.kernelsu.ui.util import android.annotation.SuppressLint @SuppressLint("PrivateApi") private fun getSystemProperty(key: String): String { return try { val props = Class.forName("android.os.SystemProperties") props.getMethod("get", String::class.java).invoke(null, key) as? String ?: "" } catch (_: Throwable) { "" } } fun isMiui(): Boolean { return getSystemProperty("ro.miui.ui.version.name").isNotEmpty() } fun isHyperOS(): Boolean { return getSystemProperty("ro.mi.os.version.name").isNotEmpty() } fun isColorOS(): Boolean { return getSystemProperty("ro.build.version.oplus.api").isNotEmpty() || getSystemProperty("ro.vendor.oplus.market.name").isNotEmpty() } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/SELinuxChecker.kt ================================================ package me.weishu.kernelsu.ui.util import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.topjohnwu.superuser.Shell import me.weishu.kernelsu.R fun isSELinuxPermissive(): Boolean { val shell = Shell.Builder.create().build("sh") val stdoutList = ArrayList() val result = shell.use { it.newJob().add("getenforce").to(stdoutList).exec() } return result.isSuccess && stdoutList.joinToString("").trim() == "Permissive" } @Composable fun getSELinuxStatus(): String { val shell = Shell.Builder.create().build("sh") val stdoutList = ArrayList() val stderrList = ArrayList() val result = shell.use { it.newJob().add("getenforce").to(stdoutList, stderrList).exec() } val stdout = stdoutList.joinToString("\n").trim() val stderr = stderrList.joinToString("\n").trim() if (result.isSuccess) { return when (stdout) { "Enforcing" -> stringResource(R.string.selinux_status_enforcing) "Permissive" -> stringResource(R.string.selinux_status_permissive) "Disabled" -> stringResource(R.string.selinux_status_disabled) else -> stringResource(R.string.selinux_status_unknown) } } return if (stderr.endsWith("Permission denied")) { stringResource(R.string.selinux_status_enforcing) } else { stringResource(R.string.selinux_status_unknown) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/Serialization.kt ================================================ package me.weishu.kernelsu.ui.util import android.os.Parcel import android.os.Parcelable import kotlinx.serialization.KSerializer import kotlinx.serialization.builtins.ByteArraySerializer import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder import me.weishu.kernelsu.ui.screen.flash.FlashIt import me.weishu.kernelsu.ui.screen.modulerepo.RepoModuleArg import me.weishu.kernelsu.ui.viewmodel.TemplateViewModel object FlashItSerializer : BaseParcelableSerializer(FlashIt::class.java) object RepoModuleArgSerializer : BaseParcelableSerializer(RepoModuleArg::class.java) object TemplateInfoSerializer : BaseParcelableSerializer(TemplateViewModel.TemplateInfo::class.java) open class BaseParcelableSerializer( private val clazz: Class ) : KSerializer { private val delegate = ByteArraySerializer() override val descriptor = delegate.descriptor private val creator: Parcelable.Creator by lazy { @Suppress("UNCHECKED_CAST") clazz.getField("CREATOR").get(null) as Parcelable.Creator } override fun serialize(encoder: Encoder, value: T) { val parcel = Parcel.obtain() try { value.writeToParcel(parcel, 0) val bytes = parcel.marshall() encoder.encodeSerializableValue(delegate, bytes) } finally { parcel.recycle() } } override fun deserialize(decoder: Decoder): T { val bytes = decoder.decodeSerializableValue(delegate) val parcel = Parcel.obtain() try { parcel.unmarshall(bytes, 0, bytes.size) parcel.setDataPosition(0) return creator.createFromParcel(parcel) } finally { parcel.recycle() } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/UidGroupUtils.kt ================================================ package me.weishu.kernelsu.ui.util import me.weishu.kernelsu.Natives import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import java.util.concurrent.ConcurrentHashMap private val PREFERRED_PKG_BY_SUID = mapOf( "android.uid.system" to "android", "android.uid.phone" to "com.android.phone", "android.uid.bluetooth" to "com.android.bluetooth", "android.uid.nfc" to "com.android.nfc", ) fun pickPrimary(apps: List): SuperUserViewModel.AppInfo { if (apps.isEmpty()) throw IllegalArgumentException("apps must not be empty") val labeled = apps.filter { it.packageInfo.sharedUserLabel != 0 } if (labeled.isNotEmpty()) { return labeled.minWith(compareBy({ it.packageName.length }, { it.packageName })) } val bySuid = apps.groupBy { it.packageInfo.sharedUserId ?: "" } .filterKeys { it.startsWith("android.uid.") } if (bySuid.isEmpty()) return apps.first() val suid = bySuid.keys.minOf { it } val group = bySuid[suid] ?: apps val preferredPkg = PREFERRED_PKG_BY_SUID[suid] preferredPkg?.let { pkg -> group.firstOrNull { it.packageName == pkg }?.let { return it } } return group.minWith(compareBy({ it.packageName.length }, { it.packageName })) } val ownerNameCache = ConcurrentHashMap() fun ownerNameForUid(uid: Int, appSource: List? = null): String { ownerNameCache[uid]?.let { return it.ifEmpty { uid.toString() } } val apps = (appSource ?: SuperUserViewModel.apps).filter { it.uid == uid } val labeledApp = apps.firstOrNull { it.packageInfo.sharedUserLabel != 0 } val name = if (labeledApp != null) { val pm = ksuApp.packageManager val resId = labeledApp.packageInfo.sharedUserLabel val text = runCatching { pm.getText(labeledApp.packageName, resId, labeledApp.packageInfo.applicationInfo) }.getOrNull() text?.toString() ?: "" } else { Natives.getUserName(uid) ?: "" } val appId = uid % 100000 val isAppRange = appId in 10000..19999 val isUA = name.matches(Regex("u\\d+_a\\d+")) val sharedUserId = apps.firstOrNull { !it.packageInfo.sharedUserId.isNullOrEmpty() }?.packageInfo?.sharedUserId val finalName = if (isAppRange && isUA && !sharedUserId.isNullOrEmpty()) { sharedUserId } else { name } ownerNameCache[uid] = finalName return finalName.ifEmpty { uid.toString() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/LatestVersionInfo.kt ================================================ package me.weishu.kernelsu.ui.util.module data class LatestVersionInfo( val versionCode: Int = 0, val downloadUrl: String = "", val changelog: String = "" ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/ModuleRepoApi.kt ================================================ package me.weishu.kernelsu.ui.util.module import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.util.isNetworkAvailable import okhttp3.Request import org.json.JSONArray import org.json.JSONObject data class ModuleDetail( val readme: String, val readmeHtml: String, val latestTag: String, val latestTime: String, val latestAssetName: String?, val latestAssetUrl: String?, val releases: List, val homepageUrl: String, val sourceUrl: String, val url: String ) data class ReleaseInfo( val name: String, val tagName: String, val publishedAt: String, val descriptionHTML: String, val assets: List ) data class ReleaseAssetInfo( val name: String, val downloadUrl: String, val size: Long, val downloadCount: Int ) fun sanitizeVersionString(version: String): String { return version.replace(Regex("[^a-zA-Z0-9.\\-_]"), "_") } fun stripTicks(s: String): String { val t = s.trim() return if (t.startsWith("`") && t.endsWith("`") && t.length >= 2) t.substring(1, t.length - 1) else t } fun fetchReleaseDescriptionHtml(moduleId: String, latestTag: String): String? { if (!isNetworkAvailable(ksuApp)) return null val url = "https://modules.kernelsu.org/module/$moduleId.json" return runCatching { ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp -> if (!resp.isSuccessful) null else { val body = resp.body.string() val obj = JSONObject(body) val releasesArray = obj.optJSONArray("releases") ?: return@use null var fallbackHtml: String? = null for (i in 0 until releasesArray.length()) { val r = releasesArray.optJSONObject(i) ?: continue val descHtml = r.optString("descriptionHTML", "") if (fallbackHtml == null && descHtml.isNotBlank()) { fallbackHtml = descHtml } val rname = r.optString("name", r.optString("tagName", r.optString("version", ""))) if (rname == latestTag && descHtml.isNotBlank()) { return@use descHtml } } fallbackHtml } } }.getOrNull() } fun fetchModuleDetail(moduleId: String): ModuleDetail? { if (!isNetworkAvailable(ksuApp)) return null val url = "https://modules.kernelsu.org/module/$moduleId.json" return runCatching { ksuApp.okhttpClient.newCall(Request.Builder().url(url).build()).execute().use { resp -> if (!resp.isSuccessful) return@use null val body = resp.body.string() val obj = JSONObject(body) val readme = obj.optString("readme", "") val readmeHtml = obj.optString("readmeHTML", "") val homepageUrl = stripTicks(obj.optString("homepageUrl", "")) val sourceUrl = stripTicks(obj.optString("sourceUrl", "")) val url = stripTicks(obj.optString("url", "")) val lr = obj.optJSONObject("latestRelease") var latestTag: String var latestTime = "" var latestAssetName: String? = null var latestAssetUrl: String? = null if (lr != null) { latestTag = lr.optString("name", lr.optString("version", "")) latestTime = lr.optString("time", "") var urlDl = lr.optString("downloadUrl", "") urlDl = stripTicks(urlDl) if (urlDl.isNotEmpty()) { latestAssetName = urlDl.substringAfterLast('/') latestAssetUrl = urlDl } } else { latestTag = obj.optString("latestRelease", "") } val releasesArray = obj.optJSONArray("releases") val releases = if (releasesArray != null) { (0 until releasesArray.length()).mapNotNull { rIdx -> val r = releasesArray.optJSONObject(rIdx) ?: return@mapNotNull null val rname = r.optString("name", r.optString("tagName", r.optString("version", ""))) val publishedAt = r.optString("publishedAt", "") val descHtml = r.optString("descriptionHTML", "") val assetsArray = r.optJSONArray("releaseAssets") ?: JSONArray() val assets = (0 until assetsArray.length()).mapNotNull { aIdx -> val a = assetsArray.optJSONObject(aIdx) ?: return@mapNotNull null val aname = a.optString("name", "") var adl = a.optString("downloadUrl", "") adl = stripTicks(adl) val asz = a.optLong("size", 0L) val dcnt = when (val dcAny = a.opt("downloadCount")) { is Number -> dcAny.toInt() is String -> dcAny.toIntOrNull() ?: 0 else -> 0 } if (aname.isEmpty() || adl.isEmpty()) null else ReleaseAssetInfo(aname, adl, asz, dcnt) } ReleaseInfo( name = rname, tagName = r.optString("tagName", rname), publishedAt = publishedAt, descriptionHTML = descHtml, assets = assets ) } } else emptyList() return@use ModuleDetail( readme = readme, readmeHtml = readmeHtml, latestTag = latestTag, latestTime = latestTime, latestAssetName = latestAssetName, latestAssetUrl = latestAssetUrl, releases = releases, homepageUrl = homepageUrl, sourceUrl = sourceUrl, url = url ) } }.getOrNull() } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/util/module/Shortcut.kt ================================================ package me.weishu.kernelsu.ui.util.module import android.app.AppOpsManager import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.net.Uri import android.provider.Settings import android.util.Log import android.widget.Toast import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat import androidx.core.graphics.scale import androidx.core.net.toUri import com.topjohnwu.superuser.io.SuFile import com.topjohnwu.superuser.io.SuFileInputStream import me.weishu.kernelsu.R import me.weishu.kernelsu.ui.MainActivity import me.weishu.kernelsu.ui.util.getRootShell import me.weishu.kernelsu.ui.util.isColorOS import me.weishu.kernelsu.ui.util.isHyperOS import me.weishu.kernelsu.ui.util.isMiui import me.weishu.kernelsu.ui.webui.WebUIActivity object Shortcut { private const val TAG = "ModuleShortcut" fun createModuleActionShortcut( context: Context, moduleId: String, name: String, iconUri: String? ) { val shortcutId = "module_action_$moduleId" val shortcutIntent = Intent(context, MainActivity::class.java).apply { action = Intent.ACTION_VIEW putExtra("shortcut_type", "module_action") putExtra("module_id", moduleId) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP) } createModuleShortcut( context = context, moduleId = moduleId, name = name, iconUri = iconUri, shortcutId = shortcutId, shortcutIntent = shortcutIntent, logPrefix = "createModuleActionShortcut" ) } fun createModuleWebUiShortcut( context: Context, moduleId: String, name: String, iconUri: String? ) { val shortcutId = "module_webui_$moduleId" val shortcutIntent = Intent(context, WebUIActivity::class.java).apply { action = Intent.ACTION_VIEW data = "kernelsu://webui/$moduleId".toUri() putExtra("id", moduleId) putExtra("from_webui_shortcut", true) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) } createModuleShortcut( context = context, moduleId = moduleId, name = name, iconUri = iconUri, shortcutId = shortcutId, shortcutIntent = shortcutIntent, logPrefix = "createModuleWebUiShortcut" ) } private fun createModuleShortcut( context: Context, moduleId: String, name: String, iconUri: String?, shortcutId: String, shortcutIntent: Intent, logPrefix: String ) { val hasPinned = hasPinnedShortcut(context, shortcutId) Log.d(TAG, "$logPrefix: shortcutId=$shortcutId, hasPinned=$hasPinned") val iconCompat = createShortcutIcon(context, iconUri) val finalIcon = iconCompat ?: IconCompat.createWithResource(context, R.mipmap.ic_launcher) val shortcut = ShortcutInfoCompat.Builder(context, shortcutId) .setShortLabel(name) .setIntent(shortcutIntent) .setIcon(finalIcon) .build() try { Log.d(TAG, "$logPrefix: pushDynamicShortcut() called for moduleId=$moduleId") ShortcutManagerCompat.pushDynamicShortcut(context, shortcut) } catch (t: Throwable) { Log.w(TAG, "$logPrefix: pushDynamicShortcut() threw exception for moduleId=$moduleId: ${t.message}", t) } if (hasPinned) { Log.d(TAG, "$logPrefix: detected existing pinned shortcut, updating only") Toast.makeText(context, context.getString(R.string.module_shortcut_updated), Toast.LENGTH_SHORT).show() return } val initialState = getShortcutPermissionState(context) Log.d(TAG, "$logPrefix: initial permission state=$initialState") if ((isMiui() || isHyperOS()) && initialState != ShortcutPermissionState.Granted) { Log.d(TAG, "$logPrefix: device is Xiaomi, trying to grant via root shell") val rootSuccess = tryGrantMiuiShortcutPermissionByRoot(context) Log.d(TAG, "$logPrefix: root grant attempt success=$rootSuccess") val afterState = getShortcutPermissionState(context) Log.d(TAG, "$logPrefix: state after root attempt=$afterState") if (afterState != ShortcutPermissionState.Granted) { Log.d(TAG, "$logPrefix: still not Granted after root, showing hint") showShortcutPermissionHint(context) return } } else if (initialState == ShortcutPermissionState.Denied || initialState == ShortcutPermissionState.Ask) { Log.d(TAG, "$logPrefix: permission not granted (state=$initialState), showing hint first") showShortcutPermissionHint(context) return } if (!ShortcutManagerCompat.isRequestPinShortcutSupported(context)) { Log.w(TAG, "$logPrefix: requestPinShortcut not supported on this launcher") Toast.makeText( context, context.getString(R.string.module_shortcut_not_supported), Toast.LENGTH_LONG ).show() return } val pinned = try { Log.d(TAG, "$logPrefix: requestPinShortcut() called for moduleId=$moduleId") val result = ShortcutManagerCompat.requestPinShortcut(context, shortcut, null) Log.d(TAG, "$logPrefix: requestPinShortcut() result=$result") result } catch (t: Throwable) { Log.w(TAG, "$logPrefix: requestPinShortcut() threw exception for moduleId=$moduleId: ${t.message}", t) false } if (pinned) { Log.d(TAG, "$logPrefix: pinned shortcut created successfully for moduleId=$moduleId") Toast.makeText( context, context.getString(R.string.module_shortcut_created), Toast.LENGTH_SHORT ).show() } else { Log.w(TAG, "$logPrefix: pinned shortcut not created, showing permission hint for moduleId=$moduleId") showShortcutPermissionHint(context) } } fun hasModuleActionShortcut(context: Context, moduleId: String): Boolean { val id = "module_action_$moduleId" return hasPinnedShortcut(context, id) } fun hasModuleWebUiShortcut(context: Context, moduleId: String): Boolean { val id = "module_webui_$moduleId" return hasPinnedShortcut(context, id) } fun deleteModuleActionShortcut(context: Context, moduleId: String) { deleteShortcut(context, "module_action_$moduleId") } fun deleteModuleWebUiShortcut(context: Context, moduleId: String) { deleteShortcut(context, "module_webui_$moduleId") } fun loadShortcutBitmap(context: Context, iconUri: String?): Bitmap? { if (iconUri.isNullOrBlank()) { return null } return try { val uri = iconUri.toUri() Log.d(TAG, "loadShortcutBitmap: loading bitmap from uri=$uri") val rawBitmap = if (uri.scheme.equals("su", ignoreCase = true)) { val path = uri.path ?: "" if (path.isNotBlank()) { val shell = getRootShell(true) val suFile = SuFile(path) suFile.shell = shell SuFileInputStream.open(suFile).use { input -> BitmapFactory.decodeStream(input) } } else null } else { context.contentResolver.openInputStream(uri)?.use { input -> BitmapFactory.decodeStream(input) } } if (rawBitmap != null) { Log.d(TAG, "loadShortcutBitmap: decoded bitmap successfully") val w = rawBitmap.width val h = rawBitmap.height val side = minOf(w, h) val x = (w - side) / 2 val y = (h - side) / 2 val square = try { Bitmap.createBitmap(rawBitmap, x, y, side, side) } catch (_: Throwable) { rawBitmap } if (square !== rawBitmap && !rawBitmap.isRecycled) { rawBitmap.recycle() } if (side > 512) { try { val scaled = square.scale(512, 512) if (scaled !== square && !square.isRecycled) { square.recycle() } scaled } catch (_: Throwable) { square } } else { square } } else { Log.w(TAG, "loadShortcutBitmap: bitmap decode returned null") null } } catch (t: Throwable) { Log.w(TAG, "loadShortcutBitmap: exception when loading icon from uri=$iconUri: ${t.message}", t) null } } private fun createShortcutIcon(context: Context, iconUri: String?): IconCompat? { val bitmap = loadShortcutBitmap(context, iconUri) ?: return null return IconCompat.createWithBitmap(bitmap) } private fun hasPinnedShortcut(context: Context, id: String): Boolean { return try { val shortcuts = ShortcutManagerCompat.getShortcuts( context, ShortcutManagerCompat.FLAG_MATCH_PINNED ) val exists = shortcuts.any { it.id == id && it.isEnabled } Log.d(TAG, "hasPinnedShortcut: id=$id, exists=$exists") exists } catch (t: Throwable) { Log.w(TAG, "hasPinnedShortcut: exception for id=$id: ${t.message}", t) false } } private fun deleteShortcut(context: Context, id: String) { try { ShortcutManagerCompat.removeDynamicShortcuts(context, listOf(id)) Log.d(TAG, "deleteShortcut: removed dynamic shortcut id=$id") } catch (t: Throwable) { Log.w(TAG, "deleteShortcut: removeDynamicShortcuts exception for id=$id: ${t.message}", t) } try { ShortcutManagerCompat.disableShortcuts(context, listOf(id), "") Log.d(TAG, "deleteShortcut: disabled shortcut id=$id") } catch (t: Throwable) { Log.w(TAG, "deleteShortcut: disableShortcuts exception for id=$id: ${t.message}", t) } } private enum class ShortcutPermissionState { Granted, Denied, Ask, Unknown } private fun checkMiuiShortcutPermission(context: Context): ShortcutPermissionState { return try { val appOps = context.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager ?: return ShortcutPermissionState.Unknown val pkg = context.applicationContext.packageName val uid = context.applicationInfo.uid Log.d(TAG, "checkMiuiShortcutPermission: pkg=$pkg, uid=$uid") val appOpsClass = Class.forName(AppOpsManager::class.java.name) val method = appOpsClass.getDeclaredMethod( "checkOpNoThrow", Integer.TYPE, Integer.TYPE, String::class.java ) val result = method.invoke(appOps, 10017, uid, pkg)?.toString() if (result == null) { Log.w(TAG, "checkMiuiShortcutPermission: checkOpNoThrow returned null") return ShortcutPermissionState.Unknown } Log.d(TAG, "checkMiuiShortcutPermission: raw result=$result") val state = when (result) { "0" -> ShortcutPermissionState.Granted "1" -> ShortcutPermissionState.Denied "5" -> ShortcutPermissionState.Ask else -> ShortcutPermissionState.Unknown } Log.d(TAG, "checkMiuiShortcutPermission: mapped state=$state") state } catch (t: Throwable) { Log.w(TAG, "checkMiuiShortcutPermission: exception=${t.message}", t) ShortcutPermissionState.Unknown } } private fun checkOppoShortcutPermission(context: Context): ShortcutPermissionState { val resolver = context.contentResolver ?: run { Log.w(TAG, "checkOppoShortcutPermission: contentResolver is null") return ShortcutPermissionState.Unknown } val uri = "content://settings/secure/launcher_shortcut_permission_settings".toUri() val cursor = resolver.query(uri, null, null, null, null) ?: run { Log.w(TAG, "checkOppoShortcutPermission: query returned null cursor, uri=$uri") return ShortcutPermissionState.Unknown } cursor.use { c -> val pkg = context.applicationContext.packageName val index = c.getColumnIndex("value") if (index == -1) { Log.w(TAG, "checkOppoShortcutPermission: 'value' column not found") return ShortcutPermissionState.Unknown } Log.d(TAG, "checkOppoShortcutPermission: pkg=$pkg") while (c.moveToNext()) { val value = c.getString(index) if (!value.isNullOrEmpty()) { Log.d(TAG, "checkOppoShortcutPermission: row value=$value") if (value.contains("$pkg, 1")) { Log.d(TAG, "checkOppoShortcutPermission: detected Granted") return ShortcutPermissionState.Granted } if (value.contains("$pkg, 0")) { Log.d(TAG, "checkOppoShortcutPermission: detected Denied") return ShortcutPermissionState.Denied } } } } return ShortcutPermissionState.Unknown } private fun tryGrantMiuiShortcutPermissionByRoot(context: Context): Boolean { val pkg = context.applicationContext.packageName val cmd = "appops set $pkg 10017 allow" return try { val shell = getRootShell() val result = shell.newJob().add(cmd).exec() Log.d(TAG, "tryGrantMiuiShortcutPermissionByRoot: cmd=$cmd, code=${result.code}, isSuccess=${result.isSuccess}") result.isSuccess } catch (t: Throwable) { Log.w(TAG, "tryGrantMiuiShortcutPermissionByRoot: exception=${t.message}", t) false } } private fun getShortcutPermissionState(context: Context): ShortcutPermissionState { return when { isMiui() || isHyperOS() -> checkMiuiShortcutPermission(context) isColorOS() -> checkOppoShortcutPermission(context) else -> ShortcutPermissionState.Unknown } } private fun showShortcutPermissionHint(context: Context) { val state = getShortcutPermissionState(context) val messageRes = when { isMiui() || isHyperOS() -> R.string.module_shortcut_permission_tip_xiaomi isColorOS() -> R.string.module_shortcut_permission_tip_oppo else -> R.string.module_shortcut_permission_tip_default } Log.d(TAG, "showShortcutPermissionHint: state=$state, messageRes=$messageRes") Toast.makeText(context, context.getString(messageRes), Toast.LENGTH_LONG).show() if (state != ShortcutPermissionState.Granted) { Log.d(TAG, "showShortcutPermissionHint: state is not Granted, opening app details settings") openAppDetailsSettings(context) } } private fun openAppDetailsSettings(context: Context) { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { data = Uri.fromParts("package", context.packageName, null) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } try { Log.d(TAG, "openAppDetailsSettings: launching settings for package=${context.packageName}") context.startActivity(intent) } catch (t: Throwable) { Log.w(TAG, "openAppDetailsSettings: failed to launch settings: ${t.message}", t) } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/HomeViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.content.Context import android.os.Build import android.system.Os import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.BuildConfig import me.weishu.kernelsu.Natives import me.weishu.kernelsu.getKernelVersion import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.screen.home.HomeUiState import me.weishu.kernelsu.ui.screen.home.SystemInfo import me.weishu.kernelsu.ui.screen.home.getManagerVersion import me.weishu.kernelsu.ui.util.checkNewVersion import me.weishu.kernelsu.ui.util.getModuleCount import me.weishu.kernelsu.ui.util.getSuperuserCount import me.weishu.kernelsu.ui.util.isSELinuxPermissive import me.weishu.kernelsu.ui.util.module.LatestVersionInfo import me.weishu.kernelsu.ui.util.rootAvailable class HomeViewModel : ViewModel() { private val _uiState = MutableStateFlow(buildState()) val uiState: StateFlow = _uiState.asStateFlow() fun refresh() { viewModelScope.launch { val baseState = withContext(Dispatchers.IO) { buildState() } _uiState.update { current -> baseState.copy(systemInfo = current.systemInfo) } if (baseState.checkUpdateEnabled) { val latestVersionInfo = withContext(Dispatchers.IO) { checkNewVersion() } _uiState.update { it.copy(latestVersionInfo = latestVersionInfo) } } } } fun updateSystemInfo(info: SystemInfo) = _uiState.update { it.copy(systemInfo = info) } private fun buildState(): HomeUiState { val kernelVersion = getKernelVersion() val isManager = Natives.isManager val ksuVersion = if (isManager) Natives.version else null val lkmMode = ksuVersion?.let { if (kernelVersion.isGKI()) Natives.isLkmMode else null } val isRootAvailable = rootAvailable() val managerVersion = getManagerVersion(ksuApp) return HomeUiState( kernelVersion = kernelVersion, ksuVersion = ksuVersion, lkmMode = lkmMode, isManager = isManager, isManagerPrBuild = BuildConfig.IS_PR_BUILD, isKernelPrBuild = Natives.isPrBuild, requiresNewKernel = isManager && Natives.requireNewKernel(), isRootAvailable = isRootAvailable, isSafeMode = Natives.isSafeMode, isLateLoadMode = Natives.isLateLoadMode, isSELinuxPermissive = isSELinuxPermissive(), checkUpdateEnabled = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE) .getBoolean("check_update", true), latestVersionInfo = LatestVersionInfo(), currentManagerVersionCode = managerVersion.versionCode, superuserCount = getSuperuserCount(), moduleCount = getModuleCount(), systemInfo = SystemInfo( kernelVersion = Os.uname().release, managerVersion = "${managerVersion.versionName} (${managerVersion.versionCode})", fingerprint = Build.FINGERPRINT, selinuxStatus = "Unknown", ), ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/MainActivityUiState.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.theme.AppSettings data class MainActivityUiState( val appSettings: AppSettings, val pageScale: Float, val enableBlur: Boolean, val enableFloatingBottomBar: Boolean, val enableFloatingBottomBarBlur: Boolean, val uiMode: UiMode, ) ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/MainActivityViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.content.Context import android.content.SharedPreferences import androidx.lifecycle.ViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import me.weishu.kernelsu.data.repository.SettingsRepository import me.weishu.kernelsu.data.repository.SettingsRepositoryImpl import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.theme.ThemeController class MainActivityViewModel : ViewModel() { private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE) private val settingRepo: SettingsRepository = SettingsRepositoryImpl() private val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key == null || key in observedKeys) { _uiState.value = readUiState() } } private val _uiState = MutableStateFlow(readUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { prefs.registerOnSharedPreferenceChangeListener(listener) } override fun onCleared() { prefs.unregisterOnSharedPreferenceChangeListener(listener) super.onCleared() } private fun readUiState(): MainActivityUiState { return MainActivityUiState( appSettings = ThemeController.getAppSettings(ksuApp), pageScale = settingRepo.pageScale, enableBlur = settingRepo.enableBlur, enableFloatingBottomBar = settingRepo.enableFloatingBottomBar, enableFloatingBottomBarBlur = settingRepo.enableFloatingBottomBarBlur, uiMode = UiMode.fromValue(settingRepo.uiMode), ) } private companion object { val observedKeys = setOf( "color_mode", "key_color", "color_style", "color_spec", "page_scale", "enable_blur", "enable_floating_bottom_bar", "enable_floating_bottom_bar_blur", "ui_mode", ) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleRepoViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.content.Context import android.util.Log import android.widget.Toast import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.data.repository.ModuleRepoRepository import me.weishu.kernelsu.data.repository.ModuleRepoRepositoryImpl import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.component.SearchStatus import me.weishu.kernelsu.ui.screen.modulerepo.ModuleRepoUiState import me.weishu.kernelsu.ui.util.isNetworkAvailable class ModuleRepoViewModel( private val repo: ModuleRepoRepository = ModuleRepoRepositoryImpl() ) : ViewModel() { companion object { private const val TAG = "ModuleRepoViewModel" } typealias RepoModule = me.weishu.kernelsu.data.model.RepoModule private val _uiState = MutableStateFlow(ModuleRepoUiState()) val uiState: StateFlow = _uiState.asStateFlow() private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE) private val searchQuery = MutableStateFlow("") init { _uiState.update { it.copy( sortByName = prefs.getBoolean("module_repo_sort_name", false), offline = !isNetworkAvailable(ksuApp) ) } viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText) } private fun filterModules(modules: List, text: String): List { if (text.isEmpty()) return emptyList() return modules.filter { it.moduleId.contains(text, true) || it.moduleName.contains(text, true) || it.authors.contains(text, true) || it.summary.contains(text, true) || me.weishu.kernelsu.ui.util.HanziToPinyin.getInstance().toPinyinString(it.moduleName) .contains(text, true) } } private suspend fun applySearchText(text: String) { _uiState.update { it.copy( searchStatus = it.searchStatus.copy( resultStatus = searchLoadingStatusFor(text) ) ) } if (text.isEmpty()) { _uiState.update { state -> state.copy( searchResults = emptyList(), searchStatus = state.searchStatus.copy(resultStatus = SearchStatus.ResultStatus.DEFAULT) ) } return } val result = withContext(Dispatchers.IO) { filterModules(_uiState.value.modules, text) } _uiState.update { it.copy( searchResults = result, searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, result.isEmpty())) ) } } private fun refreshSearchResults() { val state = _uiState.value val text = state.searchStatus.searchText val results = filterModules(state.modules, text) _uiState.update { it.copy( searchResults = results, searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, results.isEmpty())) ) } } fun refresh() { viewModelScope.launch { _uiState.update { it.copy( isRefreshing = true, error = null, offline = !isNetworkAvailable(ksuApp) ) } val result = repo.fetchModules() withContext(Dispatchers.Main) { result.onSuccess { modules -> _uiState.update { it.copy( modules = modules, isRefreshing = false, offline = !isNetworkAvailable(ksuApp) ) } refreshSearchResults() }.onFailure { e -> Log.e(TAG, "fetch modules failed", e) Toast.makeText( ksuApp, ksuApp.getString(R.string.network_offline), Toast.LENGTH_SHORT ).show() _uiState.update { it.copy( isRefreshing = false, error = e, offline = !isNetworkAvailable(ksuApp) ) } } } } } fun toggleSortByName() { val newValue = !_uiState.value.sortByName prefs.edit { putBoolean("module_repo_sort_name", newValue) } _uiState.update { it.copy(sortByName = newValue) } } fun updateSearchStatus(status: SearchStatus) { val previous = _uiState.value.searchStatus _uiState.update { it.copy(searchStatus = status) } if (previous.searchText != status.searchText) { searchQuery.value = status.searchText } } fun updateSearchText(text: String) { updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text)) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/ModuleViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.os.SystemClock import android.util.Log import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.model.Module import me.weishu.kernelsu.data.model.ModuleUpdateInfo import me.weishu.kernelsu.data.repository.ModuleRepository import me.weishu.kernelsu.data.repository.ModuleRepositoryImpl import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.component.SearchStatus import me.weishu.kernelsu.ui.screen.module.ModuleUiState import me.weishu.kernelsu.ui.util.hasMagisk import java.text.Collator import java.util.Locale class ModuleViewModel( private val repo: ModuleRepository = ModuleRepositoryImpl() ) : ViewModel() { companion object { private const val TAG = "ModuleViewModel" } private data class ModuleUpdateSignature( val updateJson: String, val versionCode: Int, val enabled: Boolean, val update: Boolean, val remove: Boolean ) private data class ModuleUpdateCache( val signature: ModuleUpdateSignature, val info: ModuleUpdateInfo ) private val _uiState = MutableStateFlow(ModuleUiState()) val uiState: StateFlow = _uiState.asStateFlow() private val updateInfoMutex = Mutex() private var updateInfoCache: MutableMap = mutableMapOf() private val updateInfoInFlight = mutableSetOf() private val searchQuery = MutableStateFlow("") var isNeedRefresh = false private set init { viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText) } fun markNeedRefresh() { isNeedRefresh = true } fun initializePreferences() { val prefs = ksuApp.getSharedPreferences("settings", 0) _uiState.update { it.copy( checkModuleUpdate = prefs.getBoolean("module_check_update", true), sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false), sortActionFirst = prefs.getBoolean("module_sort_action_first", false), ) } updateModuleList() } fun toggleSortActionFirst() { val newValue = !_uiState.value.sortActionFirst ksuApp.getSharedPreferences("settings", 0).edit { putBoolean("module_sort_action_first", newValue) } _uiState.update { it.copy(sortActionFirst = newValue) } updateModuleList() } fun toggleSortEnabledFirst() { val newValue = !_uiState.value.sortEnabledFirst ksuApp.getSharedPreferences("settings", 0).edit { putBoolean("module_sort_enabled_first", newValue) } _uiState.update { it.copy(sortEnabledFirst = newValue) } updateModuleList() } fun refreshEnvironmentState() { viewModelScope.launch { val magiskInstalled = withContext(Dispatchers.IO) { hasMagisk() } val isSafeMode = Natives.isSafeMode _uiState.update { it.copy( magiskInstalled = magiskInstalled, isSafeMode = isSafeMode, ) } } } fun updateSearchStatus(status: SearchStatus) { val previous = _uiState.value.searchStatus _uiState.update { it.copy(searchStatus = status) } if (previous.searchText != status.searchText) { searchQuery.value = status.searchText } } fun updateSearchText(text: String) { updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text)) } private fun filterModules(modules: List, text: String): List { if (text.isEmpty()) return emptyList() return modules.filter { it.id.contains(text, true) || it.name.contains(text, true) || it.description.contains(text, true) || it.author.contains(text, true) || me.weishu.kernelsu.ui.util.HanziToPinyin.getInstance().toPinyinString(it.name) .contains(text, true) } } private suspend fun applySearchText(text: String) { _uiState.update { it.copy( searchStatus = it.searchStatus.copy( resultStatus = searchLoadingStatusFor(text) ) ) } if (text.isEmpty()) { updateModuleList() return } val result = withContext(Dispatchers.IO) { val state = _uiState.value filterModules(state.modules, text).sortedWith(moduleComparator(state)) } _uiState.update { it.copy( searchResults = result, searchStatus = it.searchStatus.copy( resultStatus = searchResultStatusFor(text, result.isEmpty()) ) ) } } private fun updateModuleList() { viewModelScope.launch(Dispatchers.IO) { val state = _uiState.value val searchText = state.searchStatus.searchText val shorted = state.modules.sortedWith(moduleComparator(state)) val searchResults = filterModules(shorted, searchText) _uiState.update { it.copy( moduleList = shorted, searchResults = searchResults, searchStatus = it.searchStatus.copy( resultStatus = searchResultStatusFor(searchText, searchResults.isEmpty()) ) ) } } } private fun moduleComparator(state: ModuleUiState): Comparator { return compareBy( { val executable = it.hasWebUi || it.hasActionScript when { it.metamodule && it.enabled -> 0 state.sortEnabledFirst && state.sortActionFirst -> when { it.enabled && executable -> 1 it.enabled -> 2 executable -> 3 else -> 4 } state.sortEnabledFirst && !state.sortActionFirst -> if (it.enabled) 1 else 2 !state.sortEnabledFirst && state.sortActionFirst -> if (executable) 1 else 2 else -> 1 } }, { if (state.sortEnabledFirst) !it.enabled else 0 }, { if (state.sortActionFirst) !(it.hasWebUi || it.hasActionScript) else 0 }, ).thenBy(Collator.getInstance(Locale.getDefault()), Module::id) } suspend fun loadModuleList() { val parsedModules = withContext(Dispatchers.IO) { repo.getModules().getOrElse { Log.e(TAG, "fetchModuleList: ", it) emptyList() } } withContext(Dispatchers.Main) { _uiState.update { it.copy( modules = parsedModules, ) } // Trigger recalculation of moduleList updateModuleList() isNeedRefresh = false } } fun fetchModuleList(checkUpdate: Boolean = false) { viewModelScope.launch { _uiState.update { it.copy(isRefreshing = true) } val start = SystemClock.elapsedRealtime() loadModuleList() if (checkUpdate) syncModuleUpdateInfo(_uiState.value.modules) _uiState.update { it.copy(isRefreshing = false) } Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}, modules: ${_uiState.value.modules}") } } private fun Module.toSignature(): ModuleUpdateSignature { return ModuleUpdateSignature( updateJson = updateJson, versionCode = versionCode, enabled = enabled, update = update, remove = remove ) } suspend fun syncModuleUpdateInfo(modules: List) { if (!_uiState.value.checkModuleUpdate) return val modulesToFetch = mutableListOf>() val removedIds = mutableSetOf() updateInfoMutex.withLock { val ids = modules.map { it.id }.toSet() updateInfoCache.keys.filter { it !in ids }.forEach { removedId -> removedIds += removedId updateInfoCache.remove(removedId) updateInfoInFlight.remove(removedId) } modules.forEach { module -> val signature = module.toSignature() val cached = updateInfoCache[module.id] if ((cached == null || cached.signature != signature) && updateInfoInFlight.add(module.id)) { modulesToFetch += Triple(module.id, module, signature) } } } val fetchedEntries = coroutineScope { modulesToFetch.map { (id, module, signature) -> async(Dispatchers.IO) { id to ModuleUpdateCache(signature, checkUpdate(module)) } }.awaitAll() } val changedEntries = mutableListOf>() updateInfoMutex.withLock { fetchedEntries.forEach { (id, entry) -> val existing = updateInfoCache[id] if (existing == null || existing.signature != entry.signature || existing.info != entry.info) { updateInfoCache[id] = entry changedEntries += id to entry.info } updateInfoInFlight.remove(id) } } if (removedIds.isEmpty() && changedEntries.isEmpty()) { return } withContext(Dispatchers.Main) { _uiState.update { state -> val newMap = state.updateInfo.toMutableMap() removedIds.forEach { newMap.remove(it) } changedEntries.forEach { (id, info) -> newMap[id] = info } state.copy(updateInfo = newMap) } } } private suspend fun checkUpdate(m: Module): ModuleUpdateInfo { return repo.checkUpdate(m).getOrDefault(ModuleUpdateInfo.Empty) } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SearchViewModelHelper.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import me.weishu.kernelsu.ui.component.SearchStatus private const val SEARCH_DEBOUNCE_MILLIS = 150L @OptIn(FlowPreview::class) fun CoroutineScope.launchSearchQueryCollector( searchQuery: StateFlow, onQuery: suspend (String) -> Unit, ): Job { return launch { searchQuery .debounce(SEARCH_DEBOUNCE_MILLIS) .distinctUntilChanged() .collectLatest(onQuery) } } fun searchLoadingStatusFor(text: String): SearchStatus.ResultStatus { return if (text.isEmpty()) { SearchStatus.ResultStatus.DEFAULT } else { SearchStatus.ResultStatus.LOAD } } fun searchResultStatusFor(text: String, isEmpty: Boolean): SearchStatus.ResultStatus { return when { text.isEmpty() -> SearchStatus.ResultStatus.DEFAULT isEmpty -> SearchStatus.ResultStatus.EMPTY else -> SearchStatus.ResultStatus.SHOW } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SettingsViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.repository.SettingsRepository import me.weishu.kernelsu.data.repository.SettingsRepositoryImpl import me.weishu.kernelsu.ui.screen.settings.SettingsUiState import me.weishu.kernelsu.ui.theme.ColorMode class SettingsViewModel( private val repo: SettingsRepository = SettingsRepositoryImpl() ) : ViewModel() { private val _uiState = MutableStateFlow(SettingsUiState()) val uiState: StateFlow = _uiState.asStateFlow() init { refresh() } fun refresh() { viewModelScope.launch { val checkUpdate = repo.checkUpdate val checkModuleUpdate = repo.checkModuleUpdate val themeMode = repo.themeMode val miuixMonet = repo.miuixMonet val keyColor = repo.keyColor val enablePredictiveBack = repo.enablePredictiveBack val enableBlur = repo.enableBlur val enableFloatingBottomBar = repo.enableFloatingBottomBar val enableFloatingBottomBarBlur = repo.enableFloatingBottomBarBlur val pageScale = repo.pageScale val enableWebDebugging = repo.enableWebDebugging val colorStyle = repo.colorStyle val colorSpec = repo.colorSpec val isLkmMode = repo.isLkmMode() // Async loading for natives/features val suCompatStatus = repo.getSuCompatStatus() val suCompatPersistValue = repo.getSuCompatPersistValue() val isSuEnabled = repo.isSuEnabled() val suCompatMode = if (suCompatPersistValue == 0L) 2 else if (!isSuEnabled) 1 else 0 val kernelUmountStatus = repo.getKernelUmountStatus() val isKernelUmountEnabled = repo.isKernelUmountEnabled() val isDefaultUmountModules = repo.isDefaultUmountModules() val uiMode = repo.uiMode val autoJailbreak = repo.autoJailbreak val isLateLoadMode = Natives.isLateLoadMode _uiState.update { it.copy( uiMode = uiMode, checkUpdate = checkUpdate, checkModuleUpdate = checkModuleUpdate, themeMode = themeMode, miuixMonet = miuixMonet, keyColor = keyColor, enablePredictiveBack = enablePredictiveBack, enableBlur = enableBlur, enableFloatingBottomBar = enableFloatingBottomBar, enableFloatingBottomBarBlur = enableFloatingBottomBarBlur, pageScale = pageScale, enableWebDebugging = enableWebDebugging, colorStyle = colorStyle, colorSpec = colorSpec, suCompatStatus = suCompatStatus, suCompatMode = suCompatMode, isSuEnabled = isSuEnabled, kernelUmountStatus = kernelUmountStatus, isKernelUmountEnabled = isKernelUmountEnabled, isDefaultUmountModules = isDefaultUmountModules, isLkmMode = isLkmMode, autoJailbreak = autoJailbreak, isLateLoadMode = isLateLoadMode, ) } } } fun setCheckUpdate(enabled: Boolean) { repo.checkUpdate = enabled _uiState.update { it.copy(checkUpdate = enabled) } } fun setUiMode(mode: String) { val oldMode = repo.uiMode val currentThemeMode = repo.themeMode val newThemeMode = when (oldMode) { "material" if mode == "miuix" -> { val colorMode = ColorMode.fromValue(currentThemeMode) val baseMode = if (colorMode == ColorMode.DARK_AMOLED) 2 else currentThemeMode if (repo.miuixMonet && !colorMode.isMonet) { ColorMode.fromValue(baseMode).toMonetMode() } else if (!repo.miuixMonet && colorMode.isMonet) { ColorMode.fromValue(baseMode).toNonMonetMode() } else baseMode } "miuix" if mode == "material" -> { val colorMode = ColorMode.fromValue(currentThemeMode) if (colorMode.isMonet) { colorMode.toNonMonetMode() } else currentThemeMode } else -> currentThemeMode } repo.uiMode = mode repo.themeMode = newThemeMode _uiState.update { it.copy(uiMode = mode, themeMode = newThemeMode) } } fun setCheckModuleUpdate(enabled: Boolean) { repo.checkModuleUpdate = enabled _uiState.update { it.copy(checkModuleUpdate = enabled) } } fun setThemeMode(mode: Int) { val currentUiMode = repo.uiMode val effectiveMode = if (currentUiMode == "miuix" && _uiState.value.miuixMonet) { mode + 3 } else { mode } repo.themeMode = effectiveMode _uiState.update { it.copy(themeMode = effectiveMode) } } fun setColorMode(mode: ColorMode) { repo.themeMode = mode.value _uiState.update { it.copy(themeMode = mode.value) } } fun setMiuixMonet(enabled: Boolean) { val currentThemeMode = repo.themeMode val colorMode = ColorMode.fromValue(currentThemeMode) val newThemeMode = if (enabled) { if (!colorMode.isMonet) colorMode.toMonetMode() else currentThemeMode } else { if (colorMode.isMonet) colorMode.toNonMonetMode() else currentThemeMode } repo.miuixMonet = enabled repo.themeMode = newThemeMode _uiState.update { it.copy(miuixMonet = enabled, themeMode = newThemeMode) } } fun setKeyColor(color: Int) { repo.keyColor = color _uiState.update { it.copy(keyColor = color) } } fun setColorStyle(style: String) { repo.colorStyle = style _uiState.update { it.copy(colorStyle = style) } } fun setColorSpec(spec: String) { repo.colorSpec = spec _uiState.update { it.copy(colorSpec = spec) } } fun setEnablePredictiveBack(enabled: Boolean) { repo.enablePredictiveBack = enabled _uiState.update { it.copy(enablePredictiveBack = enabled) } } fun setEnableBlur(enabled: Boolean) { repo.enableBlur = enabled _uiState.update { it.copy(enableBlur = enabled) } } fun setEnableFloatingBottomBar(enabled: Boolean) { repo.enableFloatingBottomBar = enabled _uiState.update { it.copy(enableFloatingBottomBar = enabled) } } fun setEnableFloatingBottomBarBlur(enabled: Boolean) { repo.enableFloatingBottomBarBlur = enabled _uiState.update { it.copy(enableFloatingBottomBarBlur = enabled) } } fun setPageScale(scale: Float) { repo.pageScale = scale _uiState.update { it.copy(pageScale = scale) } } fun setEnableWebDebugging(enabled: Boolean) { repo.enableWebDebugging = enabled _uiState.update { it.copy(enableWebDebugging = enabled) } } fun setSuCompatMode(mode: Int) { viewModelScope.launch(Dispatchers.IO) { when (mode) { 0 -> if (repo.setSuEnabled(true)) { repo.execKsudFeatureSave() repo.setSuCompatModePref(0) _uiState.update { it.copy(suCompatMode = 0, isSuEnabled = true) } } 1 -> if (repo.setSuEnabled(true)) { repo.execKsudFeatureSave() if (repo.setSuEnabled(false)) { // "Disable until reboot" implies it should be enabled on next boot. // We set the preference to 0 (Enabled) to match the persistent state. repo.setSuCompatModePref(0) _uiState.update { it.copy(suCompatMode = 1, isSuEnabled = false) } } } 2 -> if (repo.setSuEnabled(false)) { repo.execKsudFeatureSave() repo.setSuCompatModePref(2) _uiState.update { it.copy(suCompatMode = 2, isSuEnabled = false) } } } } } fun setKernelUmountEnabled(enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { if (repo.setKernelUmountEnabled(enabled)) { repo.execKsudFeatureSave() _uiState.update { it.copy(isKernelUmountEnabled = enabled) } } } } fun setAutoJailbreak(enabled: Boolean) { repo.autoJailbreak = enabled _uiState.update { it.copy(autoJailbreak = enabled) } } fun setDefaultUmountModules(enabled: Boolean) { viewModelScope.launch(Dispatchers.IO) { if (repo.setDefaultUmountModules(enabled)) { _uiState.update { it.copy(isDefaultUmountModules = enabled) } } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/SuperUserViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.content.Context import android.content.pm.ApplicationInfo import android.graphics.drawable.Drawable import android.util.Log import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.repository.SuperUserRepository import me.weishu.kernelsu.data.repository.SuperUserRepositoryImpl import me.weishu.kernelsu.ksuApp import me.weishu.kernelsu.ui.component.SearchStatus import me.weishu.kernelsu.ui.screen.superuser.GroupedApps import me.weishu.kernelsu.ui.screen.superuser.SuperUserUiState import me.weishu.kernelsu.ui.util.HanziToPinyin import me.weishu.kernelsu.ui.util.ownerNameForUid import me.weishu.kernelsu.ui.util.pickPrimary import java.text.Collator import java.util.Locale class SuperUserViewModel( private val repo: SuperUserRepository = SuperUserRepositoryImpl() ) : ViewModel() { companion object { private const val TAG = "SuperUserViewModel" // Cache to support getAppIconDrawable static method private val appsLock = Any() private var cachedApps: List = emptyList() private val groupedAppsLock = Any() private var cachedGroupedApps: List = emptyList() val apps: List get() = synchronized(appsLock) { cachedApps } @JvmStatic fun getGroupedApp(uid: Int): GroupedApps? { return synchronized(groupedAppsLock) { cachedGroupedApps.find { it.uid == uid } } } @JvmStatic fun getAppIconDrawable(context: Context, packageName: String): Drawable? { val appList = synchronized(appsLock) { cachedApps } val appDetail = appList.find { it.packageName == packageName } return appDetail?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) } } typealias AppInfo = me.weishu.kernelsu.data.model.AppInfo private val _uiState = MutableStateFlow(SuperUserUiState()) val uiState: StateFlow = _uiState.asStateFlow() private val prefs = ksuApp.getSharedPreferences("settings", Context.MODE_PRIVATE) private val refreshMutex = Mutex() private val searchQuery = MutableStateFlow("") var isNeedRefresh = false private set init { viewModelScope.launchSearchQueryCollector(searchQuery, ::applySearchText) } fun markNeedRefresh() { isNeedRefresh = true } fun initializePreferences() { val showSystemApps = prefs.getBoolean("show_system_apps", false) val showOnlyPrimaryUserApps = prefs.getBoolean("show_only_primary_user_apps", false) _uiState.update { it.copy( showSystemApps = showSystemApps, showOnlyPrimaryUserApps = showOnlyPrimaryUserApps, ) } } fun toggleShowSystemApps(): Job { val newValue = !_uiState.value.showSystemApps prefs.edit { putBoolean("show_system_apps", newValue) } _uiState.update { it.copy(showSystemApps = newValue) } return viewModelScope.launch { // Re-filter when setting changes val grouped = withContext(Dispatchers.IO) { buildGroups(filterAndSort(apps)) } updateVisibleApps(grouped) } } fun toggleShowOnlyPrimaryUserApps(): Job { val newValue = !_uiState.value.showOnlyPrimaryUserApps prefs.edit { putBoolean("show_only_primary_user_apps", newValue) } _uiState.update { it.copy(showOnlyPrimaryUserApps = newValue) } return viewModelScope.launch { // Re-filter when setting changes val grouped = withContext(Dispatchers.IO) { buildGroups(filterAndSort(apps)) } updateVisibleApps(grouped) } } fun updateSearchStatus(status: SearchStatus) { val previous = _uiState.value.searchStatus _uiState.update { it.copy(searchStatus = status) } if (previous.searchText != status.searchText) { searchQuery.value = status.searchText } } fun updateSearchText(text: String) { updateSearchStatus(_uiState.value.searchStatus.copy(searchText = text)) } private fun filterSearchResults(groups: List, text: String): List { if (text.isEmpty()) return emptyList() return groups.mapNotNull { group -> val matchedPackageNames = group.apps.filter { it.label.contains(text, true) || it.packageName.contains(text, true) || HanziToPinyin.getInstance().toPinyinString(it.label).contains(text, true) }.mapTo(linkedSetOf()) { it.packageName } if (matchedPackageNames.isEmpty()) { null } else { val sortedApps = group.apps.sortedWith( compareByDescending { it.packageName in matchedPackageNames } ) group.copy( apps = sortedApps, matchedPackageNames = matchedPackageNames, ) } } } private suspend fun applySearchText(text: String) { _uiState.update { it.copy( searchStatus = it.searchStatus.copy( resultStatus = searchLoadingStatusFor(text) ) ) } if (text.isEmpty()) { _uiState.update { state -> state.copy( searchResults = emptyList(), searchStatus = state.searchStatus.copy(resultStatus = SearchStatus.ResultStatus.DEFAULT) ) } return } val result = withContext(Dispatchers.IO) { filterSearchResults(_uiState.value.groupedApps, text) } _uiState.update { it.copy( searchResults = result, searchStatus = it.searchStatus.copy(resultStatus = searchResultStatusFor(text, result.isEmpty())) ) } } private fun updateCachedGroupedApps(grouped: List) { synchronized(groupedAppsLock) { cachedGroupedApps = grouped.map { it.copy(matchedPackageNames = emptySet()) } } } private fun updateVisibleApps(grouped: List) { val searchText = _uiState.value.searchStatus.searchText val searchResults = filterSearchResults(grouped, searchText) _uiState.update { it.copy( groupedApps = grouped.map { group -> group.copy(matchedPackageNames = emptySet()) }, searchResults = searchResults, searchStatus = it.searchStatus.copy( resultStatus = searchResultStatusFor(searchText, searchResults.isEmpty()) ) ) } } private fun filterAndSort(list: List): List { val comparator = compareBy { when { it.allowSu -> 0 it.hasCustomProfile -> 1 else -> 2 } }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) val currentState = _uiState.value return list.filter { if (it.packageName == ksuApp.packageName) return@filter false if (it.allowSu || it.hasCustomProfile) { return@filter true } val userFilter = !currentState.showOnlyPrimaryUserApps || it.uid / 100000 == 0 val isSystemApp = it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) != 0 val typeFilter = it.uid == 2000 || currentState.showSystemApps || !isSystemApp userFilter && typeFilter }.sortedWith(comparator) } private fun buildCachedGroups(apps: List): List { return buildGroups(apps.filter { it.packageName != ksuApp.packageName }) } private fun buildGroups(apps: List): List { val comparator = compareBy { when { it.allowSu -> 0 it.hasCustomProfile -> 1 else -> 2 } }.thenBy { it.label.lowercase() } val groups = apps.groupBy { it.uid }.map { (uid, list) -> val sorted = list.sortedWith(comparator) val primary = pickPrimary(sorted) val shouldUmount = Natives.uidShouldUmount(uid) val ownerName = if (sorted.size > 1) ownerNameForUid(uid, sorted) else null GroupedApps( uid = uid, apps = sorted, primary = primary, anyAllowSu = sorted.any { it.allowSu }, anyCustom = sorted.any { it.hasCustomProfile }, shouldUmount = shouldUmount, ownerName = ownerName ) } return groups.sortedWith(Comparator { a, b -> fun rank(g: GroupedApps): Int = when { g.anyAllowSu -> 0 g.anyCustom -> 1 g.apps.size > 1 -> 2 g.shouldUmount -> 4 else -> 3 } val ra = rank(a) val rb = rank(b) if (ra != rb) return@Comparator ra - rb return@Comparator when (ra) { 2 -> a.uid.compareTo(b.uid) else -> a.primary.label.lowercase().compareTo(b.primary.label.lowercase()) } }) } suspend fun fetchAppList() { refreshMutex.withLock { _uiState.update { it.copy(isRefreshing = true, error = null) } repo.getAppList().onSuccess { (newApps, ids) -> val (cachedGroups, grouped) = withContext(Dispatchers.IO) { buildCachedGroups(newApps) to buildGroups(filterAndSort(newApps)) } // Update cache for static method synchronized(appsLock) { cachedApps = newApps } updateCachedGroupedApps(cachedGroups) updateVisibleApps(grouped) _uiState.update { it.copy(userIds = ids, isRefreshing = false) } }.onFailure { e -> Log.e(TAG, "fetchAppList failed", e) _uiState.update { it.copy( isRefreshing = false, error = e ) } } isNeedRefresh = false } } private suspend fun refreshAppList(resort: Boolean = true) { refreshMutex.withLock { val currentApps = synchronized(appsLock) { cachedApps } if (currentApps.isEmpty()) return repo.refreshProfiles(currentApps).onSuccess { updatedApps -> // Update cache for static method synchronized(appsLock) { cachedApps = updatedApps } val cachedGroups = withContext(Dispatchers.IO) { buildCachedGroups(updatedApps) } updateCachedGroupedApps(cachedGroups) val grouped = if (resort) { withContext(Dispatchers.IO) { buildGroups(filterAndSort(updatedApps)) } } else { val updatedGroups = buildGroups(filterAndSort(updatedApps)).associateBy { it.uid } _uiState.value.groupedApps.map { group -> val newApps = updatedGroups[group.uid]?.apps ?: group.apps val primary = pickPrimary(newApps) val shouldUmount = Natives.uidShouldUmount(group.uid) val ownerName = if (newApps.size > 1) ownerNameForUid(group.uid, newApps) else null group.copy( apps = newApps, primary = primary, anyAllowSu = newApps.any { it.allowSu }, anyCustom = newApps.any { it.hasCustomProfile }, shouldUmount = shouldUmount, ownerName = ownerName ) } } updateVisibleApps(grouped) _uiState.update { it.copy(isRefreshing = false) } isNeedRefresh = false } } } fun loadAppList(force: Boolean = false, resort: Boolean = true): Job { return viewModelScope.launch { if (force || _uiState.value.groupedApps.isEmpty()) { fetchAppList() } else { refreshAppList(resort) } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/viewmodel/TemplateViewModel.kt ================================================ package me.weishu.kernelsu.ui.viewmodel import android.util.Log import androidx.lifecycle.ViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.withContext import me.weishu.kernelsu.Natives import me.weishu.kernelsu.data.repository.TemplateRepository import me.weishu.kernelsu.data.repository.TemplateRepositoryImpl import me.weishu.kernelsu.profile.Capabilities import me.weishu.kernelsu.profile.Groups import me.weishu.kernelsu.ui.screen.template.TemplateUiState import me.weishu.kernelsu.ui.util.getAppProfileTemplate import org.json.JSONArray import org.json.JSONObject import java.text.Collator import java.util.Locale const val TAG = "TemplateViewModel" class TemplateViewModel( private val repo: TemplateRepository = TemplateRepositoryImpl() ) : ViewModel() { typealias TemplateInfo = me.weishu.kernelsu.data.model.TemplateInfo private val _uiState = MutableStateFlow(TemplateUiState()) val uiState: StateFlow = _uiState.asStateFlow() suspend fun fetchTemplates(sync: Boolean = false) { _uiState.update { it.copy(isRefreshing = true, error = null) } val result = repo.getTemplates(sync) withContext(Dispatchers.Main) { result.onSuccess { templates -> val comparator = compareBy(TemplateInfo::local).reversed().then( compareBy( Collator.getInstance(Locale.getDefault()), TemplateInfo::id ) ) val sorted = templates.sortedWith(comparator) _uiState.update { it.copy( templates = templates, templateList = sorted, isRefreshing = false ) } }.onFailure { e -> _uiState.update { it.copy( isRefreshing = false, error = e ) } } } } suspend fun importTemplates( templates: String, onSuccess: suspend () -> Unit, onFailure: suspend (String) -> Unit ) { repo.importTemplates(templates) .onSuccess { onSuccess() } .onFailure { onFailure(it.message ?: "Unknown error") } } suspend fun exportTemplates(onTemplateEmpty: suspend () -> Unit, callback: suspend (String) -> Unit) { repo.exportTemplates() .onSuccess { callback(it) } .onFailure { onTemplateEmpty() } } } fun getTemplateInfoById(id: String): me.weishu.kernelsu.data.model.TemplateInfo? { return runCatching { me.weishu.kernelsu.data.model.TemplateInfo.fromJSON(JSONObject(getAppProfileTemplate(id))) }.onFailure { Log.e(TAG, "ignore invalid template: $it", it) }.getOrNull() } @Suppress("unused") fun generateTemplates() { val templateJson = JSONObject() templateJson.put("id", "com.example") templateJson.put("name", "Example") templateJson.put("description", "This is an example template") templateJson.put("local", true) templateJson.put("namespace", Natives.Profile.Namespace.INHERITED.name) templateJson.put("uid", 0) templateJson.put("gid", 0) templateJson.put("groups", JSONArray().apply { put(Groups.INET.name) }) templateJson.put("capabilities", JSONArray().apply { put(Capabilities.CAP_NET_RAW.name) }) templateJson.put("context", "u:r:su:s0") Log.i(TAG, "$templateJson") } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/AppIconUtil.kt ================================================ package me.weishu.kernelsu.ui.webui import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.util.LruCache import androidx.core.graphics.createBitmap import androidx.core.graphics.scale import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel.Companion.getAppIconDrawable object AppIconUtil { // Limit cache size to 200 icons private const val CACHE_SIZE = 200 private val iconCache = LruCache(CACHE_SIZE) @Synchronized fun loadAppIconSync(context: Context, packageName: String, sizePx: Int): Bitmap? { val cached = iconCache.get(packageName) if (cached != null) return cached try { val drawable = getAppIconDrawable(context, packageName) ?: return null val raw = drawableToBitmap(drawable, sizePx) val icon = raw.scale(sizePx, sizePx) iconCache.put(packageName, icon) return icon } catch (_: Exception) { return null } } private fun drawableToBitmap(drawable: Drawable, size: Int): Bitmap { if (drawable is BitmapDrawable) return drawable.bitmap val width = if (drawable.intrinsicWidth > 0) drawable.intrinsicWidth else size val height = if (drawable.intrinsicHeight > 0) drawable.intrinsicHeight else size val bmp = createBitmap(width, height) val canvas = Canvas(bmp) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bmp } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/Insets.kt ================================================ package me.weishu.kernelsu.ui.webui /** * Insets data class from GitHub@MMRLApp/WebUI-X-Portable * * Data class representing insets (top, bottom, left, right) for a view. * * This class provides methods to generate CSS code that can be injected into a WebView * to apply these insets as CSS variables. This is useful for adapting web content * to the safe areas of a device screen, considering notches, status bars, and navigation bars. * * @property top The top inset value in pixels. * @property bottom The bottom inset value in pixels. * @property left The left inset value in pixels. * @property right The right inset value in pixels. */ data class Insets( val top: Int, val bottom: Int, val left: Int, val right: Int, ) { val css get() = buildString { appendLine(":root {") appendLine("\t--safe-area-inset-top: ${top}px;") appendLine("\t--safe-area-inset-right: ${right}px;") appendLine("\t--safe-area-inset-bottom: ${bottom}px;") appendLine("\t--safe-area-inset-left: ${left}px;") appendLine("\t--window-inset-top: var(--safe-area-inset-top, 0px);") appendLine("\t--window-inset-bottom: var(--safe-area-inset-bottom, 0px);") appendLine("\t--window-inset-left: var(--safe-area-inset-left, 0px);") appendLine("\t--window-inset-right: var(--safe-area-inset-right, 0px);") appendLine("\t--f7-safe-area-top: var(--window-inset-top, 0px) !important;") appendLine("\t--f7-safe-area-bottom: var(--window-inset-bottom, 0px) !important;") appendLine("\t--f7-safe-area-left: var(--window-inset-left, 0px) !important;") appendLine("\t--f7-safe-area-right: var(--window-inset-right, 0px) !important;") append("}") } val js get() = buildString { append("(function() {") append(" var s = document.documentElement.style;") append(" s.setProperty('--safe-area-inset-top', '${top}px');") append(" s.setProperty('--safe-area-inset-right', '${right}px');") append(" s.setProperty('--safe-area-inset-bottom', '${bottom}px');") append(" s.setProperty('--safe-area-inset-left', '${left}px');") append("})();") } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MimeUtil.java ================================================ /* * Copyright 2023 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package me.weishu.kernelsu.ui.webui; import java.net.URLConnection; class MimeUtil { public static String getMimeFromFileName(String fileName) { if (fileName == null) { return null; } // Copying the logic and mapping that Chromium follows. // First we check against the OS (this is a limited list by default) // but app developers can extend this. // We then check against a list of hardcoded mime types above if the // OS didn't provide a result. String mimeType = URLConnection.guessContentTypeFromName(fileName); if (mimeType != null) { return mimeType; } return guessHardcodedMime(fileName); } // We should keep this map in sync with the lists under // //net/base/mime_util.cc in Chromium. // A bunch of the mime types don't really apply to Android land // like word docs so feel free to filter out where necessary. private static String guessHardcodedMime(String fileName) { int finalFullStop = fileName.lastIndexOf('.'); if (finalFullStop == -1) { return null; } final String extension = fileName.substring(finalFullStop + 1).toLowerCase(); return switch (extension) { case "webm" -> "video/webm"; case "mpeg", "mpg" -> "video/mpeg"; case "mp3" -> "audio/mpeg"; case "wasm" -> "application/wasm"; case "xhtml", "xht", "xhtm" -> "application/xhtml+xml"; case "flac" -> "audio/flac"; case "ogg", "oga", "opus" -> "audio/ogg"; case "wav" -> "audio/wav"; case "m4a" -> "audio/x-m4a"; case "gif" -> "image/gif"; case "jpeg", "jpg", "jfif", "pjpeg", "pjp" -> "image/jpeg"; case "png" -> "image/png"; case "apng" -> "image/apng"; case "svg", "svgz" -> "image/svg+xml"; case "webp" -> "image/webp"; case "mht", "mhtml" -> "multipart/related"; case "css" -> "text/css"; case "html", "htm", "shtml", "shtm", "ehtml" -> "text/html"; case "js", "mjs" -> "application/javascript"; case "xml" -> "text/xml"; case "mp4", "m4v" -> "video/mp4"; case "ogv", "ogm" -> "video/ogg"; case "ico" -> "image/x-icon"; case "woff" -> "application/font-woff"; case "gz", "tgz" -> "application/gzip"; case "json" -> "application/json"; case "pdf" -> "application/pdf"; case "zip" -> "application/zip"; case "bmp" -> "image/bmp"; case "tiff", "tif" -> "image/tiff"; default -> null; }; } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/MonetColorsProvider.kt ================================================ package me.weishu.kernelsu.ui.webui import androidx.compose.material3.MaterialTheme import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import top.yukonga.miuix.kmp.theme.MiuixTheme import java.util.concurrent.atomic.AtomicReference /** * @author rifsxd * @date 2025/6/2. */ object MonetColorsProvider { private val colorsCss: AtomicReference = AtomicReference(null) fun getColorsCss(): String { return colorsCss.get() ?: "" } @Composable fun UpdateCss() { when (LocalUiMode.current) { UiMode.Miuix -> UpdateCssMiuix() UiMode.Material -> UpdateCssMaterial() } } @Composable private fun UpdateCssMiuix() { val colorScheme = MiuixTheme.colorScheme LaunchedEffect(colorScheme) { val monetColors = mapOf( "primary" to colorScheme.primary.toCssValue(), "onPrimary" to colorScheme.onPrimary.toCssValue(), "primaryContainer" to colorScheme.primaryContainer.toCssValue(), "onPrimaryContainer" to colorScheme.onPrimaryContainer.toCssValue(), "inversePrimary" to colorScheme.primaryVariant.toCssValue(), "secondary" to colorScheme.secondary.toCssValue(), "onSecondary" to colorScheme.onSecondary.toCssValue(), "secondaryContainer" to colorScheme.secondaryContainer.toCssValue(), "onSecondaryContainer" to colorScheme.onSecondaryContainer.toCssValue(), "tertiary" to colorScheme.tertiaryContainerVariant.toCssValue(), "onTertiary" to colorScheme.tertiaryContainer.toCssValue(), "tertiaryContainer" to colorScheme.tertiaryContainer.toCssValue(), "onTertiaryContainer" to colorScheme.onTertiaryContainer.toCssValue(), "background" to colorScheme.background.toCssValue(), "onBackground" to colorScheme.onBackground.toCssValue(), "surface" to colorScheme.surface.toCssValue(), "tonalSurface" to colorScheme.surfaceContainer.toCssValue(), "onSurface" to colorScheme.onSurface.toCssValue(), "surfaceVariant" to colorScheme.surfaceVariant.toCssValue(), "onSurfaceVariant" to colorScheme.onSurfaceVariantSummary.toCssValue(), "surfaceTint" to colorScheme.primary.toCssValue(), "inverseSurface" to colorScheme.disabledOnSurface.toCssValue(), "inverseOnSurface" to colorScheme.surfaceContainer.toCssValue(), "error" to colorScheme.error.toCssValue(), "onError" to colorScheme.onError.toCssValue(), "errorContainer" to colorScheme.errorContainer.toCssValue(), "onErrorContainer" to colorScheme.onErrorContainer.toCssValue(), "outline" to colorScheme.outline.toCssValue(), "outlineVariant" to colorScheme.dividerLine.toCssValue(), "scrim" to colorScheme.windowDimming.toCssValue(), "surfaceBright" to colorScheme.surface.toCssValue(), "surfaceDim" to colorScheme.surface.toCssValue(), "surfaceContainer" to colorScheme.surfaceContainer.toCssValue(), "surfaceContainerHigh" to colorScheme.surfaceContainerHigh.toCssValue(), "surfaceContainerHighest" to colorScheme.surfaceContainerHighest.toCssValue(), "surfaceContainerLow" to colorScheme.surfaceContainer.toCssValue(), "surfaceContainerLowest" to colorScheme.surfaceContainer.toCssValue(), "filledTonalButtonContentColor" to colorScheme.onPrimaryContainer.toCssValue(), "filledTonalButtonContainerColor" to colorScheme.secondaryContainer.toCssValue(), "filledTonalButtonDisabledContentColor" to colorScheme.onSurfaceVariantSummary.toCssValue(), "filledTonalButtonDisabledContainerColor" to colorScheme.surfaceVariant.toCssValue(), "filledCardContentColor" to colorScheme.onPrimaryContainer.toCssValue(), "filledCardContainerColor" to colorScheme.primaryContainer.toCssValue(), "filledCardDisabledContentColor" to colorScheme.onSurfaceVariantSummary.toCssValue(), "filledCardDisabledContainerColor" to colorScheme.surfaceVariant.toCssValue() ) colorsCss.set(monetColors.toCssVars()) } } @Composable private fun UpdateCssMaterial() { val colorScheme = MaterialTheme.colorScheme LaunchedEffect(colorScheme) { val monetColors = mapOf( "primary" to colorScheme.primary.toCssValue(), "onPrimary" to colorScheme.onPrimary.toCssValue(), "primaryContainer" to colorScheme.primaryContainer.toCssValue(), "onPrimaryContainer" to colorScheme.onPrimaryContainer.toCssValue(), "inversePrimary" to colorScheme.inversePrimary.toCssValue(), "secondary" to colorScheme.secondary.toCssValue(), "onSecondary" to colorScheme.onSecondary.toCssValue(), "secondaryContainer" to colorScheme.secondaryContainer.toCssValue(), "onSecondaryContainer" to colorScheme.onSecondaryContainer.toCssValue(), "tertiary" to colorScheme.tertiary.toCssValue(), "onTertiary" to colorScheme.onTertiary.toCssValue(), "tertiaryContainer" to colorScheme.tertiaryContainer.toCssValue(), "onTertiaryContainer" to colorScheme.onTertiaryContainer.toCssValue(), "background" to colorScheme.background.toCssValue(), "onBackground" to colorScheme.onBackground.toCssValue(), "surface" to colorScheme.surface.toCssValue(), "tonalSurface" to colorScheme.surfaceColorAtElevation(1.dp).toCssValue(), "onSurface" to colorScheme.onSurface.toCssValue(), "surfaceVariant" to colorScheme.surfaceVariant.toCssValue(), "onSurfaceVariant" to colorScheme.onSurfaceVariant.toCssValue(), "surfaceTint" to colorScheme.surfaceTint.toCssValue(), "inverseSurface" to colorScheme.inverseSurface.toCssValue(), "inverseOnSurface" to colorScheme.inverseOnSurface.toCssValue(), "error" to colorScheme.error.toCssValue(), "onError" to colorScheme.onError.toCssValue(), "errorContainer" to colorScheme.errorContainer.toCssValue(), "onErrorContainer" to colorScheme.onErrorContainer.toCssValue(), "outline" to colorScheme.outline.toCssValue(), "outlineVariant" to colorScheme.outlineVariant.toCssValue(), "scrim" to colorScheme.scrim.toCssValue(), "surfaceBright" to colorScheme.surfaceBright.toCssValue(), "surfaceDim" to colorScheme.surfaceDim.toCssValue(), "surfaceContainer" to colorScheme.surfaceContainer.toCssValue(), "surfaceContainerHigh" to colorScheme.surfaceContainerHigh.toCssValue(), "surfaceContainerHighest" to colorScheme.surfaceContainerHighest.toCssValue(), "surfaceContainerLow" to colorScheme.surfaceContainerLow.toCssValue(), "surfaceContainerLowest" to colorScheme.surfaceContainerLowest.toCssValue(), "filledTonalButtonContentColor" to colorScheme.onPrimaryContainer.toCssValue(), "filledTonalButtonContainerColor" to colorScheme.secondaryContainer.toCssValue(), "filledTonalButtonDisabledContentColor" to colorScheme.onSurfaceVariant.toCssValue(), "filledTonalButtonDisabledContainerColor" to colorScheme.surfaceVariant.toCssValue(), "filledCardContentColor" to colorScheme.onPrimaryContainer.toCssValue(), "filledCardContainerColor" to colorScheme.primaryContainer.toCssValue(), "filledCardDisabledContentColor" to colorScheme.onSurfaceVariant.toCssValue(), "filledCardDisabledContainerColor" to colorScheme.surfaceVariant.toCssValue() ) colorsCss.set(monetColors.toCssVars()) } } private fun Map.toCssVars(): String { return buildString { append(":root {\n") for ((k, v) in this@toCssVars) { append(" --$k: $v;\n") } append("}\n") } } private fun Color.toCssValue(): String { fun Float.toHex(): String { return (this * 255).toInt().coerceIn(0, 255).toString(16).padStart(2, '0') } return if (alpha == 1f) { "#${red.toHex()}${green.toHex()}${blue.toHex()}" } else { "#${red.toHex()}${green.toHex()}${blue.toHex()}${alpha.toHex()}" } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/SuFilePathHandler.java ================================================ package me.weishu.kernelsu.ui.webui; import android.content.Context; import android.content.SharedPreferences; import android.util.Log; import android.webkit.WebResourceResponse; import androidx.annotation.NonNull; import androidx.annotation.WorkerThread; import androidx.webkit.WebViewAssetLoader; import com.topjohnwu.superuser.Shell; import com.topjohnwu.superuser.io.SuFile; import com.topjohnwu.superuser.io.SuFileInputStream; import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.zip.GZIPInputStream; /** * Handler class to open files from file system by root access * For more information about android storage please refer to * Android Developers * Docs: Data and file storage overview. *

* To avoid leaking user or app data to the web, make sure to choose {@code directory} * carefully, and assume any file under this directory could be accessed by any web page subject * to same-origin rules. *

* A typical usage would be like: *

 * File publicDir = new File(context.getFilesDir(), "public");
 * // Host "files/public/" in app's data directory under:
 * // http://appassets.androidplatform.net/public/...
 * WebViewAssetLoader assetLoader = new WebViewAssetLoader.Builder()
 *          .addPathHandler("/public/", new InternalStoragePathHandler(context, publicDir))
 *          .build();
 * 
*/ public final class SuFilePathHandler implements WebViewAssetLoader.PathHandler { /** * Default value to be used as MIME type if guessing MIME type failed. */ public static final String DEFAULT_MIME_TYPE = "text/plain"; private static final String TAG = "SuFilePathHandler"; /** * Forbidden subdirectories of {@link Context#getDataDir} that cannot be exposed by this * handler. They are forbidden as they often contain sensitive information. *

* Note: Any future addition to this list will be considered breaking changes to the API. */ private static final String[] FORBIDDEN_DATA_DIRS = new String[]{"/data/data", "/data/system"}; @NonNull private final File mDirectory; private final Shell mShell; private final InsetsSupplier mInsetsSupplier; private final OnInsetsRequestedListener mOnInsetsRequestedListener; private final Context mContext; /** * Creates PathHandler for app's internal storage. * The directory to be exposed must be inside either the application's internal data * directory {@link Context#getDataDir} or cache directory {@link Context#getCacheDir}. * External storage is not supported for security reasons, as other apps with * {@link android.Manifest.permission#WRITE_EXTERNAL_STORAGE} may be able to modify the * files. *

* Exposing the entire data or cache directory is not permitted, to avoid accidentally * exposing sensitive application files to the web. Certain existing subdirectories of * {@link Context#getDataDir} are also not permitted as they are often sensitive. * These files are ({@code "app_webview/"}, {@code "databases/"}, {@code "lib/"}, * {@code "shared_prefs/"} and {@code "code_cache/"}). *

* The application should typically use a dedicated subdirectory for the files it intends to * expose and keep them separate from other files. * * @param context {@link Context} that is used to access app's internal storage. * @param directory the absolute path of the exposed app internal storage directory from * which files can be loaded. * @param rootShell {@link Shell} instance with root access to read files. * @param insetsSupplier {@link InsetsSupplier} to provide window insets for styling web content. * @param onInsetsRequestedListener {@link OnInsetsRequestedListener} to notify when insets are requested. * @throws IllegalArgumentException if the directory is not allowed. */ public SuFilePathHandler(@NonNull Context context, @NonNull File directory, Shell rootShell, @NonNull InsetsSupplier insetsSupplier, OnInsetsRequestedListener onInsetsRequestedListener) { try { mContext = context; mInsetsSupplier = insetsSupplier; mOnInsetsRequestedListener = onInsetsRequestedListener; mDirectory = new File(getCanonicalDirPath(directory)); if (!isAllowedInternalStorageDir(context)) { throw new IllegalArgumentException("The given directory \"" + directory + "\" doesn't exist under an allowed app internal storage directory"); } mShell = rootShell; } catch (IOException e) { throw new IllegalArgumentException( "Failed to resolve the canonical path for the given directory: " + directory.getPath(), e); } } public static String getCanonicalDirPath(@NonNull File file) throws IOException { String canonicalPath = file.getCanonicalPath(); if (!canonicalPath.endsWith("/")) canonicalPath += "/"; return canonicalPath; } public static File getCanonicalFileIfChild(@NonNull File parent, @NonNull String child) throws IOException { String parentCanonicalPath = getCanonicalDirPath(parent); String childCanonicalPath = new File(parent, child).getCanonicalPath(); if (childCanonicalPath.startsWith(parentCanonicalPath)) { return new File(childCanonicalPath); } return null; } @NonNull private static InputStream handleSvgzStream(@NonNull String path, @NonNull InputStream stream) throws IOException { return path.endsWith(".svgz") ? new GZIPInputStream(stream) : stream; } public static InputStream openFile(@NonNull File file, @NonNull Shell shell) throws IOException { SuFile suFile = new SuFile(file.getAbsolutePath()); suFile.setShell(shell); InputStream fis = SuFileInputStream.open(suFile); return handleSvgzStream(file.getPath(), fis); } /** * Use {@link MimeUtil#getMimeFromFileName} to guess MIME type or return the * {@link #DEFAULT_MIME_TYPE} if it can't guess. * * @param filePath path of the file to guess its MIME type. * @return MIME type guessed from file extension or {@link #DEFAULT_MIME_TYPE}. */ @NonNull public static String guessMimeType(@NonNull String filePath) { String mimeType = MimeUtil.getMimeFromFileName(filePath); return mimeType == null ? DEFAULT_MIME_TYPE : mimeType; } private boolean isAllowedInternalStorageDir(@NonNull Context context) throws IOException { String dir = getCanonicalDirPath(mDirectory); for (String forbiddenPath : FORBIDDEN_DATA_DIRS) { if (dir.startsWith(forbiddenPath)) { return false; } } return true; } /** * Opens the requested file from the exposed data directory. *

* The matched prefix path used shouldn't be a prefix of a real web path. Thus, if the * requested file cannot be found or is outside the mounted directory a * {@link WebResourceResponse} object with a {@code null} {@link InputStream} will be * returned instead of {@code null}. This saves the time of falling back to network and * trying to resolve a path that doesn't exist. A {@link WebResourceResponse} with * {@code null} {@link InputStream} will be received as an HTTP response with status code * {@code 404} and no body. *

* The MIME type for the file will be determined from the file's extension using * {@link java.net.URLConnection#guessContentTypeFromName}. Developers should ensure that * files are named using standard file extensions. If the file does not have a * recognised extension, {@code "text/plain"} will be used by default. * * @param path the suffix path to be handled. * @return {@link WebResourceResponse} for the requested file. */ @Override @WorkerThread @NonNull public WebResourceResponse handle(@NonNull String path) { if ("internal/insets.css".equals(path)) { if (mOnInsetsRequestedListener != null) { mOnInsetsRequestedListener.onInsetsRequested(true); } String css = mInsetsSupplier.get().getCss(); return new WebResourceResponse( "text/css", "utf-8", new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8)) ); } if ("internal/colors.css".equals(path)) { SharedPreferences prefs = mContext.getSharedPreferences("settings", Context.MODE_PRIVATE); int colorMode = prefs.getInt("color_mode", 0); String uiMode = prefs.getString("ui_mode", "miuix"); String css = ""; if ((colorMode >= 3 && colorMode <= 6) || "material".equals(uiMode)) { css = MonetColorsProvider.INSTANCE.getColorsCss(); } return new WebResourceResponse( "text/css", "utf-8", new ByteArrayInputStream(css.getBytes(StandardCharsets.UTF_8)) ); } try { File file = getCanonicalFileIfChild(mDirectory, path); if (file != null) { InputStream is = openFile(file, mShell); String mimeType = guessMimeType(path); return new WebResourceResponse(mimeType, null, is); } else { Log.e(TAG, String.format( "The requested file: %s is outside the mounted directory: %s", path, mDirectory)); } } catch (IOException e) { Log.e(TAG, "Error opening the requested path: " + path, e); } return new WebResourceResponse(null, null, null); } public interface InsetsSupplier { @NonNull Insets get(); } public interface OnInsetsRequestedListener { void onInsetsRequested(boolean enable); } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIActivity.kt ================================================ package me.weishu.kernelsu.ui.webui import android.annotation.SuppressLint import android.content.SharedPreferences import android.os.Bundle import android.widget.Toast import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.Crossfade import androidx.compose.animation.core.tween import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode import me.weishu.kernelsu.ui.theme.KernelSUTheme import me.weishu.kernelsu.ui.theme.ThemeController import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator @SuppressLint("SetJavaScriptEnabled") class WebUIActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() window.isNavigationBarContrastEnforced = false super.onCreate(savedInstanceState) setContent { val context = LocalContext.current val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) var appSettings by remember { mutableStateOf(ThemeController.getAppSettings(context)) } var uiModeValue by remember { mutableStateOf(prefs.getString("ui_mode", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE) } val uiMode = remember(uiModeValue) { UiMode.fromValue(uiModeValue) } DisposableEffect(prefs) { val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key -> if (key in listOf("color_mode", "key_color", "color_style", "color_spec")) { appSettings = ThemeController.getAppSettings(context) } else if (key == "ui_mode") { uiModeValue = prefs.getString("ui_mode", UiMode.DEFAULT_VALUE) ?: UiMode.DEFAULT_VALUE } } prefs.registerOnSharedPreferenceChangeListener(listener) onDispose { prefs.unregisterOnSharedPreferenceChangeListener(listener) } } CompositionLocalProvider(LocalUiMode provides uiMode) { KernelSUTheme(appSettings = appSettings, uiMode = uiMode) { MainContent(activity = this, onFinish = { finish() }) } } } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun MainContent(activity: ComponentActivity, onFinish: () -> Unit) { val moduleId = remember { activity.intent.getStringExtra("id") } val webUIState = remember { WebUIState() } LaunchedEffect(moduleId) { if (moduleId == null) { onFinish() return@LaunchedEffect } prepareWebView(activity, moduleId, webUIState) } DisposableEffect(Unit) { onDispose { webUIState.dispose(activity) } } when (val event = webUIState.uiEvent) { is WebUIEvent.Error -> { LaunchedEffect(event) { Toast.makeText(activity, event.message, Toast.LENGTH_SHORT).show() onFinish() } } is WebUIEvent.Close -> { LaunchedEffect(event) { onFinish() } } else -> {} } val isLoading = webUIState.uiEvent is WebUIEvent.Loading Crossfade(targetState = isLoading, animationSpec = tween(300)) { loading -> if (loading) { LoadingContent() } else { WebUIScreen(webUIState = webUIState) } } } @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable private fun LoadingContent() { when (LocalUiMode.current) { UiMode.Miuix -> { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { InfiniteProgressIndicator() } } UiMode.Material -> { Box( modifier = Modifier .fillMaxSize() .background(MaterialTheme.colorScheme.background), contentAlignment = Alignment.Center ) { androidx.compose.material3.LoadingIndicator() } } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIMaterial.kt ================================================ package me.weishu.kernelsu.ui.webui import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.AlertDialog import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import me.weishu.kernelsu.R @Composable fun HandleWebUIEventMaterial( webUIState: WebUIState, fileLauncher: ActivityResultLauncher ) { when (val event = webUIState.uiEvent) { is WebUIEvent.ShowAlert -> { val showDialog = remember(event) { mutableStateOf(true) } if (showDialog.value) { AlertDialog( onDismissRequest = { webUIState.onAlertResult() showDialog.value = false }, confirmButton = { TextButton( onClick = { webUIState.onAlertResult() showDialog.value = false }, ) { Text(stringResource(R.string.confirm)) } }, text = { Text(event.message) } ) } } is WebUIEvent.ShowConfirm -> { val showDialog = remember(event) { mutableStateOf(true) } if (showDialog.value) { AlertDialog( onDismissRequest = { webUIState.onConfirmResult(false) showDialog.value = false }, confirmButton = { TextButton( onClick = { webUIState.onConfirmResult(true) showDialog.value = false }, ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { TextButton( onClick = { webUIState.onConfirmResult(false) showDialog.value = false }, ) { Text(stringResource(android.R.string.cancel)) } }, text = { Text(event.message) } ) } } is WebUIEvent.ShowPrompt -> { val showDialog = remember(event) { mutableStateOf(true) } val state = remember(event) { mutableStateOf(event.defaultValue) } if (showDialog.value) { AlertDialog( onDismissRequest = { webUIState.onPromptResult(null) showDialog.value = false }, confirmButton = { TextButton( onClick = { webUIState.onPromptResult(state.value) showDialog.value = false }, ) { Text(stringResource(R.string.confirm)) } }, dismissButton = { TextButton( onClick = { webUIState.onPromptResult(null) showDialog.value = false }, ) { Text(stringResource(android.R.string.cancel)) } }, text = { Column { OutlinedTextField( label = { Text(event.message) }, value = state.value, onValueChange = { state.value = it }, modifier = Modifier.fillMaxWidth() ) } } ) } } is WebUIEvent.ShowFileChooser -> { LaunchedEffect(event) { try { fileLauncher.launch(event.intent) } catch (_: Exception) { webUIState.onFileChooserResult(null) } } } else -> {} } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIMiuix.kt ================================================ package me.weishu.kernelsu.ui.webui import android.content.Intent import androidx.activity.result.ActivityResultLauncher import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import me.weishu.kernelsu.R import top.yukonga.miuix.kmp.basic.ButtonDefaults 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.extra.WindowDialog @Composable fun HandleWebUIEventMiuix( webUIState: WebUIState, fileLauncher: ActivityResultLauncher ) { when (val event = webUIState.uiEvent) { is WebUIEvent.ShowAlert -> { val showDialog = remember(event) { mutableStateOf(true) } WindowDialog( show = showDialog.value, content = { Column { Text(event.message) Spacer(Modifier.height(12.dp)) TextButton( modifier = Modifier.fillMaxWidth(), onClick = { webUIState.onAlertResult() showDialog.value = false }, text = stringResource(R.string.confirm), colors = ButtonDefaults.textButtonColorsPrimary() ) } } ) } is WebUIEvent.ShowConfirm -> { val showDialog = remember(event) { mutableStateOf(true) } WindowDialog( show = showDialog.value, onDismissRequest = { webUIState.onConfirmResult(false) }, content = { Column { Text(event.message) Spacer(Modifier.height(12.dp)) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { webUIState.onConfirmResult(false) showDialog.value = false }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f), ) Spacer(modifier = Modifier.width(20.dp)) TextButton( onClick = { webUIState.onConfirmResult(true) showDialog.value = false }, text = stringResource(R.string.confirm), modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) } is WebUIEvent.ShowPrompt -> { val showDialog = remember(event) { mutableStateOf(true) } val state = rememberTextFieldState(event.defaultValue) WindowDialog( show = showDialog.value, onDismissRequest = { webUIState.onPromptResult(null) }, content = { Column { Text(event.message) Spacer(Modifier.height(12.dp)) TextField( modifier = Modifier.padding(bottom = 16.dp), state = state ) Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween ) { TextButton( onClick = { webUIState.onPromptResult(null) showDialog.value = false }, text = stringResource(android.R.string.cancel), modifier = Modifier.weight(1f), ) Spacer(modifier = Modifier.width(20.dp)) TextButton( onClick = { webUIState.onPromptResult(state.text.toString()) showDialog.value = false }, text = stringResource(R.string.confirm), modifier = Modifier.weight(1f), colors = ButtonDefaults.textButtonColorsPrimary() ) } } } ) } is WebUIEvent.ShowFileChooser -> { LaunchedEffect(event) { try { fileLauncher.launch(event.intent) } catch (_: Exception) { webUIState.onFileChooserResult(null) } } } else -> {} } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIScreen.kt ================================================ package me.weishu.kernelsu.ui.webui import android.app.Activity import android.content.Intent import android.net.Uri import android.view.View import android.view.ViewGroup import android.webkit.WebView import androidx.activity.compose.BackHandler import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.compose.LocalLifecycleOwner import me.weishu.kernelsu.ui.LocalUiMode import me.weishu.kernelsu.ui.UiMode @Composable fun rememberFileLauncher(webUIState: WebUIState): ActivityResultLauncher { return rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> val uris: Array? = if (result.resultCode == Activity.RESULT_OK) { result.data?.let { data -> data.clipData?.let { clipData -> Array(clipData.itemCount) { i -> clipData.getItemAt(i).uri } } ?: data.data?.let { arrayOf(it) } } } else null webUIState.onFileChooserResult(uris) } } @Composable fun WebUIScreen(webUIState: WebUIState) { val density = LocalDensity.current val layoutDirection = LocalLayoutDirection.current val windowInsets = WindowInsets.safeDrawing val innerPadding = if (webUIState.isInsetsEnabled) PaddingValues(0.dp) else windowInsets.asPaddingValues() val fileLauncher = rememberFileLauncher(webUIState) LaunchedEffect(density, layoutDirection, windowInsets, webUIState.isInsetsEnabled) { if (!webUIState.isInsetsEnabled) { return@LaunchedEffect } snapshotFlow { val top = (windowInsets.getTop(density) / density.density).toInt() val bottom = (windowInsets.getBottom(density) / density.density).toInt() val left = (windowInsets.getLeft(density, layoutDirection) / density.density).toInt() val right = (windowInsets.getRight(density, layoutDirection) / density.density).toInt() Insets(top, bottom, left, right) }.collect { newInsets -> if (webUIState.currentInsets != newInsets) { webUIState.currentInsets = newInsets webUIState.webView?.evaluateJavascript(newInsets.js, null) } } } BackHandler(enabled = webUIState.webCanGoBack) { webUIState.webView?.goBack() } Box( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { webUIState.webView?.let { webView -> AndroidView( modifier = Modifier.fillMaxSize(), factory = { _ -> webView.apply { layoutParams = ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) if (!webUIState.isUrlLoaded) { val homePage = "https://mui.kernelsu.org/index.html" if (width > 0 && height > 0) { loadUrl(homePage) webUIState.isUrlLoaded = true } else { val listener = object : View.OnLayoutChangeListener { override fun onLayoutChange( v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int, oldRight: Int, oldBottom: Int ) { if (v.width > 0 && v.height > 0) { (v as WebView).loadUrl(homePage) webUIState.isUrlLoaded = true v.removeOnLayoutChangeListener(this) } } } addOnLayoutChangeListener(listener) } } } } ) } } when (LocalUiMode.current) { UiMode.Miuix -> HandleWebUIEventMiuix(webUIState, fileLauncher) UiMode.Material -> HandleWebUIEventMaterial(webUIState, fileLauncher) } HandleWebViewLifecycle(webUIState) HandleConfigurationChanges(webUIState) } @Composable private fun HandleWebViewLifecycle(webUIState: WebUIState) { val lifecycleOwner = LocalLifecycleOwner.current DisposableEffect(lifecycleOwner, webUIState) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_RESUME -> webUIState.webView?.onResume() Lifecycle.Event.ON_PAUSE -> webUIState.webView?.onPause() else -> {} } } lifecycleOwner.lifecycle.addObserver(observer) onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } } } @Composable private fun HandleConfigurationChanges(webUIState: WebUIState) { val configuration = LocalConfiguration.current LaunchedEffect(configuration.fontScale, webUIState.webView) { webUIState.webView?.settings?.textZoom = (configuration.fontScale * 100).toInt() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebUIState.kt ================================================ package me.weishu.kernelsu.ui.webui import android.app.Activity import android.content.Intent import android.net.Uri import android.webkit.JsPromptResult import android.webkit.JsResult import android.webkit.WebView import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import com.topjohnwu.superuser.Shell import me.weishu.kernelsu.R sealed class WebUIEvent { data object Loading : WebUIEvent() data object WebViewReady : WebUIEvent() data class Error(val message: String) : WebUIEvent() data object Close : WebUIEvent() data class ShowAlert(val message: String, val result: JsResult) : WebUIEvent() data class ShowConfirm(val message: String, val result: JsResult) : WebUIEvent() data class ShowPrompt(val message: String, val defaultValue: String, val result: JsPromptResult) : WebUIEvent() data class ShowFileChooser(val intent: Intent) : WebUIEvent() } class WebUIState { var webView: WebView? = null var rootShell: Shell? = null lateinit var modDir: String var moduleName: String = "" var uiEvent by mutableStateOf(WebUIEvent.Loading) var isUrlLoaded = false var currentInsets: Insets = Insets(0, 0, 0, 0) var isInsetsEnabled by mutableStateOf(false) var webCanGoBack by mutableStateOf(false) var filePathCallback: android.webkit.ValueCallback>? = null fun onAlertResult() { val event = uiEvent if (event is WebUIEvent.ShowAlert) { event.result.confirm() uiEvent = WebUIEvent.WebViewReady } } fun onConfirmResult(confirmed: Boolean) { val event = uiEvent if (event is WebUIEvent.ShowConfirm) { if (confirmed) event.result.confirm() else event.result.cancel() uiEvent = WebUIEvent.WebViewReady } } fun onPromptResult(result: String?) { val event = uiEvent if (event is WebUIEvent.ShowPrompt) { if (result != null) event.result.confirm(result) else event.result.cancel() uiEvent = WebUIEvent.WebViewReady } } fun onFileChooserResult(uris: Array?) { filePathCallback?.onReceiveValue(uris) filePathCallback = null uiEvent = WebUIEvent.WebViewReady } fun requestExit() { uiEvent = WebUIEvent.Close } fun dispose(activity: Activity) { activity.setTaskDescription(activity.getString(R.string.app_name)) webView?.let { view -> (view.parent as? android.view.ViewGroup)?.removeView(view) view.destroy() } webView = null rootShell?.close() } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewHelper.kt ================================================ package me.weishu.kernelsu.ui.webui import android.annotation.SuppressLint import android.app.Activity import android.app.ActivityManager import android.content.Context import android.content.Intent import android.graphics.Color import android.net.Uri import android.os.Build import android.webkit.JsPromptResult import android.webkit.JsResult import android.webkit.ValueCallback import android.webkit.WebChromeClient import android.webkit.WebResourceRequest import android.webkit.WebResourceResponse import android.webkit.WebView import android.webkit.WebViewClient import androidx.webkit.WebViewAssetLoader import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import me.weishu.kernelsu.R import me.weishu.kernelsu.data.repository.ModuleRepositoryImpl import me.weishu.kernelsu.ui.util.createRootShell import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import java.io.File fun Activity.setTaskDescription(label: String) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { @Suppress("DEPRECATION") setTaskDescription(ActivityManager.TaskDescription(label)) } else { val taskDescription = ActivityManager.TaskDescription.Builder() .setLabel(label) .build() setTaskDescription(taskDescription) } } @SuppressLint("SetJavaScriptEnabled") internal suspend fun prepareWebView( activity: Activity, moduleId: String, webUIState: WebUIState, ) { withContext(Dispatchers.IO) { val repo = ModuleRepositoryImpl() val modules = repo.getModules().getOrDefault(emptyList()) val moduleInfo = modules.find { info -> info.id == moduleId } if (moduleInfo == null) { withContext(Dispatchers.Main) { webUIState.uiEvent = WebUIEvent.Error(activity.getString(R.string.no_such_module, moduleId)) } return@withContext } if (!moduleInfo.hasWebUi || !moduleInfo.enabled || moduleInfo.update || moduleInfo.remove) { withContext(Dispatchers.Main) { webUIState.uiEvent = WebUIEvent.Error(activity.getString(R.string.module_unavailable, moduleInfo.name)) } return@withContext } webUIState.moduleName = moduleInfo.name webUIState.modDir = "/data/adb/modules/${moduleId}" if (SuperUserViewModel.apps.isEmpty()) { SuperUserViewModel().fetchAppList() } val shell = createRootShell(true) webUIState.rootShell = shell withContext(Dispatchers.Main) { activity.setTaskDescription(activity.getString(R.string.app_name) + " - ${moduleInfo.name}") val webView = WebView(activity) webView.setBackgroundColor(Color.TRANSPARENT) val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) WebView.setWebContentsDebuggingEnabled(prefs.getBoolean("enable_web_debugging", false)) webView.settings.apply { javaScriptEnabled = true domStorageEnabled = true allowFileAccess = false } val webRoot = File("${webUIState.modDir}/webroot") val webViewAssetLoader = WebViewAssetLoader.Builder() .setDomain("mui.kernelsu.org") .addPathHandler( "/", SuFilePathHandler( activity, webRoot, shell, { webUIState.currentInsets }, { enable -> webUIState.isInsetsEnabled = enable }) ) .build() // WebViewClient webView.webViewClient = object : WebViewClient() { override fun shouldInterceptRequest(view: WebView, request: WebResourceRequest): WebResourceResponse? { val url = request.url if (url.scheme.equals("ksu", ignoreCase = true) && url.host.equals("icon", ignoreCase = true)) { val packageName = url.path?.substring(1) if (!packageName.isNullOrEmpty()) { val icon = AppIconUtil.loadAppIconSync(activity, packageName, 512) if (icon != null) { val stream = java.io.ByteArrayOutputStream() icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream) return WebResourceResponse("image/png", null, java.io.ByteArrayInputStream(stream.toByteArray())) } } } return webViewAssetLoader.shouldInterceptRequest(url) } override fun doUpdateVisitedHistory(view: WebView?, url: String?, isReload: Boolean) { webUIState.webCanGoBack = view?.canGoBack() ?: false if (webUIState.isInsetsEnabled) webUIState.webView?.evaluateJavascript(webUIState.currentInsets.js, null) super.doUpdateVisitedHistory(view, url, isReload) } } // WebChromeClient webView.webChromeClient = object : WebChromeClient() { override fun onJsAlert(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { if (message == null || result == null) return false webUIState.uiEvent = WebUIEvent.ShowAlert(message, result) return true } override fun onJsConfirm(view: WebView?, url: String?, message: String?, result: JsResult?): Boolean { if (message == null || result == null) return false webUIState.uiEvent = WebUIEvent.ShowConfirm(message, result) return true } override fun onJsPrompt( view: WebView?, url: String?, message: String?, defaultValue: String?, result: JsPromptResult? ): Boolean { if (message == null || result == null || defaultValue == null) return false webUIState.uiEvent = WebUIEvent.ShowPrompt(message, defaultValue, result) return true } override fun onShowFileChooser( webView: WebView?, filePathCallback: ValueCallback>?, fileChooserParams: FileChooserParams? ): Boolean { webUIState.filePathCallback?.onReceiveValue(null) webUIState.filePathCallback = filePathCallback val intent = fileChooserParams?.createIntent() ?: Intent(Intent.ACTION_GET_CONTENT).apply { type = "*/*" } if (fileChooserParams?.mode == FileChooserParams.MODE_OPEN_MULTIPLE) { intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } webUIState.uiEvent = WebUIEvent.ShowFileChooser(intent) return true } } // JS Interface val webviewInterface = WebViewInterface(webUIState) webUIState.webView = webView webView.addJavascriptInterface(webviewInterface, "ksu") webUIState.uiEvent = WebUIEvent.WebViewReady } } } ================================================ FILE: manager/app/src/main/java/me/weishu/kernelsu/ui/webui/WebViewInterface.kt ================================================ package me.weishu.kernelsu.ui.webui import android.app.Activity import android.content.pm.ApplicationInfo import android.os.Handler import android.os.Looper import android.text.TextUtils import android.view.Window import android.webkit.JavascriptInterface import android.widget.Toast import androidx.core.content.pm.PackageInfoCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.ShellUtils import com.topjohnwu.superuser.internal.UiThreadHandler import me.weishu.kernelsu.ui.util.createRootShell import me.weishu.kernelsu.ui.util.listModules import me.weishu.kernelsu.ui.util.withNewRootShell import me.weishu.kernelsu.ui.viewmodel.SuperUserViewModel import org.json.JSONArray import org.json.JSONObject import java.io.File import java.util.concurrent.CompletableFuture class WebViewInterface(private val state: WebUIState) { private val webView get() = state.webView private val modDir get() = state.modDir @JavascriptInterface fun exec(cmd: String): String { return withNewRootShell(true) { ShellUtils.fastCmd(this, cmd) } } @JavascriptInterface fun exec(cmd: String, callbackFunc: String) { exec(cmd, null, callbackFunc) } private fun processOptions(sb: StringBuilder, options: String?) { val opts = if (options == null) JSONObject() else { JSONObject(options) } val cwd = opts.optString("cwd") if (!TextUtils.isEmpty(cwd)) { sb.append("cd ${cwd};") } opts.optJSONObject("env")?.let { env -> env.keys().forEach { key -> sb.append("export ${key}=${env.getString(key)};") } } } @JavascriptInterface fun exec( cmd: String, options: String?, callbackFunc: String ) { val finalCommand = StringBuilder() processOptions(finalCommand, options) finalCommand.append(cmd) val result = withNewRootShell(true) { newJob().add(finalCommand.toString()).to(ArrayList(), ArrayList()).exec() } val stdout = result.out.joinToString(separator = "\n") val stderr = result.err.joinToString(separator = "\n") val jsCode = "javascript: (function() { try { ${callbackFunc}(${result.code}, ${ JSONObject.quote( stdout ) }, ${JSONObject.quote(stderr)}); } catch(e) { console.error(e); } })();" webView?.post { webView?.loadUrl(jsCode) } } @JavascriptInterface fun spawn(command: String, args: String, options: String?, callbackFunc: String) { val finalCommand = StringBuilder() processOptions(finalCommand, options) if (!TextUtils.isEmpty(args)) { finalCommand.append(command).append(" ") JSONArray(args).let { argsArray -> for (i in 0 until argsArray.length()) { finalCommand.append(argsArray.getString(i)) finalCommand.append(" ") } } } else { finalCommand.append(command) } val shell = createRootShell(true) val emitData = fun(name: String, data: String) { val jsCode = "javascript: (function() { try { ${callbackFunc}.${name}.emit('data', ${ JSONObject.quote( data ) }); } catch(e) { console.error('emitData', e); } })();" webView?.post { webView?.loadUrl(jsCode) } } val stdout = object : CallbackList(UiThreadHandler::runAndWait) { override fun onAddElement(s: String) { emitData("stdout", s) } } val stderr = object : CallbackList(UiThreadHandler::runAndWait) { override fun onAddElement(s: String) { emitData("stderr", s) } } val future = shell.newJob().add(finalCommand.toString()).to(stdout, stderr).enqueue() val completableFuture = CompletableFuture.supplyAsync { future.get() } completableFuture.thenAccept { result -> val emitExitCode = "javascript: (function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();" webView?.post { webView?.loadUrl(emitExitCode) } if (result.code != 0) { val emitErrCode = "javascript: (function() { try { var err = new Error(); err.exitCode = ${result.code}; err.message = ${ JSONObject.quote( result.err.joinToString( "\n" ) ) };${callbackFunc}.emit('error', err); } catch(e) { console.error('emitErr', e); } })();" webView?.post { webView?.loadUrl(emitErrCode) } } }.whenComplete { _, _ -> runCatching { shell.close() } } } @JavascriptInterface fun toast(msg: String) { webView?.post { webView?.let { Toast.makeText(it.context, msg, Toast.LENGTH_SHORT).show() } } } @JavascriptInterface fun fullScreen(enable: Boolean) { val context = webView?.context if (context is Activity) { Handler(Looper.getMainLooper()).post { if (enable) { hideSystemUI(context.window) } else { showSystemUI(context.window) } } } enableEdgeToEdge(enable) } @JavascriptInterface fun enableEdgeToEdge(enable: Boolean = true) { state.isInsetsEnabled = enable } @JavascriptInterface fun moduleInfo(): String { val moduleInfos = JSONArray(listModules()) val currentModuleInfo = JSONObject() currentModuleInfo.put("moduleDir", modDir) val moduleId = File(modDir).name for (i in 0 until moduleInfos.length()) { val currentInfo = moduleInfos.getJSONObject(i) if (currentInfo.getString("id") != moduleId) { continue } val keys = currentInfo.keys() for (key in keys) { currentModuleInfo.put(key, currentInfo.get(key)) } break } return currentModuleInfo.toString() } @JavascriptInterface fun listPackages(type: String): String { val packageNames = SuperUserViewModel.apps .filter { appInfo -> val flags = appInfo.packageInfo.applicationInfo?.flags ?: 0 when (type.lowercase()) { "system" -> (flags and ApplicationInfo.FLAG_SYSTEM) != 0 "user" -> (flags and ApplicationInfo.FLAG_SYSTEM) == 0 else -> true } } .map { it.packageName } .sorted() val jsonArray = JSONArray() for (pkgName in packageNames) { jsonArray.put(pkgName) } return jsonArray.toString() } @JavascriptInterface fun getPackagesInfo(packageNamesJson: String): String { val packageNames = JSONArray(packageNamesJson) val jsonArray = JSONArray() val appMap = SuperUserViewModel.apps.associateBy { it.packageName } for (i in 0 until packageNames.length()) { val pkgName = packageNames.getString(i) val appInfo = appMap[pkgName] if (appInfo != null) { val pkg = appInfo.packageInfo val app = pkg.applicationInfo val obj = JSONObject() obj.put("packageName", pkg.packageName) obj.put("versionName", pkg.versionName ?: "") obj.put("versionCode", PackageInfoCompat.getLongVersionCode(pkg)) obj.put("appLabel", appInfo.label) obj.put("isSystem", if (app != null) ((app.flags and ApplicationInfo.FLAG_SYSTEM) != 0) else JSONObject.NULL) obj.put("uid", app?.uid ?: JSONObject.NULL) jsonArray.put(obj) } else { val obj = JSONObject() obj.put("packageName", pkgName) obj.put("error", "Package not found or inaccessible") jsonArray.put(obj) } } return jsonArray.toString() } @JavascriptInterface fun exit() { state.requestExit() } } fun hideSystemUI(window: Window) = WindowInsetsControllerCompat(window, window.decorView).let { controller -> controller.hide(WindowInsetsCompat.Type.systemBars()) controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } fun showSystemUI(window: Window) = WindowInsetsControllerCompat(window, window.decorView).show(WindowInsetsCompat.Type.systemBars()) ================================================ FILE: manager/app/src/main/jniLibs/.gitignore ================================================ libksud.so ================================================ FILE: manager/app/src/main/res/drawable/ic_launcher_foreground.xml ================================================ ================================================ FILE: manager/app/src/main/res/drawable/ic_launcher_monochrome.xml ================================================ ================================================ FILE: manager/app/src/main/res/mipmap-anydpi/ic_launcher.xml ================================================ ================================================ FILE: manager/app/src/main/res/resources.properties ================================================ unqualifiedResLocale=en-US ================================================ FILE: manager/app/src/main/res/values/colors.xml ================================================ #FFFFFFFF ================================================ FILE: manager/app/src/main/res/values/strings.xml ================================================ KernelSU Home Not installed Tap to install Working Version: %d Unsupported KernelSU only supports GKI kernels now, but you can patch the image for GKI devices. Manager version (%1$d) and KernelSU driver version (%2$d) mismatch. Kernel version Manager version Fingerprint SELinux status Disabled Enforcing Permissive Unknown Superuser Failed to enable module: %s Failed to disable module: %s No module installed Module The following modules will be installed: %1$s Action first Enabled first Repos Name (A → Z) Source code Confirm Uninstall Install Install Reboot Settings Soft restart Userspace reboot Reboot to Recovery Reboot to Bootloader Reboot to Download Reboot to EDL About Are you sure you want to uninstall module %s? Are you sure you want to uninstall module %s? This action will affect all modules, and certain features provided by the metamodule (such as mounting) will no longer work. %s uninstalled Failed to uninstall: %s Version Author Show system apps Send logs Safe mode Jailbreak mode Jailbreak Jailbreak may have failed, please check logs Module installation is disabled in safe mode Reboot to take effect Modules are unavailable due to a conflict with Magisk! Learn KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Learn how to install KernelSU and use modules. Support Us KernelSU is, and always will be, free, and open source. However, you can show us that you care by making a donation. Starting from v3.0.0, the GKI work mode will be used only in testing environments. We do not recommend it for daily use, and image files will no longer be provided. Join our %2$s channel]]> App Profile Default Template Custom Profile name Mount namespace Inherited Global Individual Groups Capabilities SELinux context Umount modules Failed to update App Profile for %s The current KernelSU version %1$d is too low for the manager to work properly. Please upgrade to version %2$d or higher! Umount modules by default The global default value for \"Umount modules\" in App Profile. If enabled, it will remove all module modifications to the system for apps that don\'t have a profile set. Enabling this option will allow KernelSU to restore any modified files by the modules for this app. Domain Rules Update Downloading module: %s Start downloading: %s Module action executed successfully. New version %s is available, click to upgrade! Launch Force stop Restart Failed to update SELinux rules for %s Couldn\'t grant Superuser access to %s Changelog App Profile template Manage local and online template of App Profile. Create template Edit template ID Invalid template ID Name Description Save Delete View template Template ID already exists! Import/Export Import from clipboard Export to clipboard Cannot find local template to export! Imported successfully Failed to save template Clipboard is empty! Affects the following apps Check for updates Automatically check for updates when opening the app. Predictive back gesture Enable predictive back gesture support. Check for module updates Failed to grant root! This is a PR debug build. Do NOT use it in production! The kernel was built with PR signature support. This is not a production kernel! Action Open WebView debugging Can be used to debug WebUI. Please enable only when needed. Direct install (Recommended) Select a file Install to inactive slot (After OTA) Your device will be **FORCED** to boot to the current inactive slot after a reboot!\nOnly use this option after OTA is done.\nContinue? You are in **Jailbreak mode**. Flashing a partition on a device with a **locked bootloader** will break AVB (Android Verified Boot) and may cause the device to **fail to boot**.\n\nMake sure your bootloader is unlocked before proceeding! Continue (%1$d) Next Select partition Use local LKM file Only .ko files are supported %1$s partition image is recommended Select KMI Uninstall Uninstall temporarily Uninstall permanently Restore stock image Temporarily uninstall KernelSU, restore to original state after next reboot. Uninstalling KernelSU (root and all modules) completely and permanently. Restore the stock factory image (if a backup exists), usually used before OTA; if you need to uninstall KernelSU, please use \"Uninstall permanently\". Flashing Flash success Flash failed Selected LKM: %s Save logs Logs saved Classic SU command Allow root access via /system/bin/su, in new processes. Umount modules (kernel-level) Unmount modules from kernel in App Profile. Kernel does not support this feature This feature is managed by a module Enable (Default) Disable until Reboot Always disable Processing… Pull down to refresh Release to refresh Refreshing… Refreshed successfully Undo Successfully canceled uninstall of %s Failed to undo uninstall: %s Contains %d apps Theme Choose the app theme mode. Follow system Light Dark Enable Monet Key color Default UI Style Choose the interface style. Page Scale Adjust the global display scale. Color Spec Color Style Blue Red Green Purple Orange Teal Pink Brown Deep Purple Indigo Cyan Yellow Amber Blue Grey Sakura Not connected to network Retry README Releases Info Create shortcut Shortcut name Choose custom icon Launcher does not support desktop shortcuts. Shortcut created on desktop. Shortcut updated. Delete shortcut Please enable \"Home screen shortcuts\" permission for this app in Xiaomi settings. Please enable \"Create Home screen shortcuts\" permission for this app in OPPO settings. If shortcut creation fails, please enable desktop shortcut permission for this app in system settings. Module %s does not exist Module %s is disabled, updating, or pending removal Please select the GKI device image file you want to patch KMI version of this device: %s This device KMI Blur Enable blur effect for top and bottom bars Floating bottom bar Use Apple style floating bottom bar Liquid glass Enable liquid glass effect for floating bottom bar Show only primary user apps Auto jailbreak Automatically use Magica to escalate privileges when Permissive SELinux is detected at boot. Requires granting autostart permission to this app. Always grant root to Shell Always allow adb shell to call su. Do not enable this unless absolutely necessary. Force enable adb on boot Force enable USB debugging and disable adb authentication. Do not enable this unless absolutely necessary. Advanced Options Expand ================================================ FILE: manager/app/src/main/res/values/themes.xml ================================================ ================================================ FILE: manager/app/src/main/res/values-ar/strings.xml ================================================ الرئيسية غير مثبت إضغط للتثبيت يعمل الإصدار: %d غير مدعوم KernelSU يدعم GKI kernels فقط إصدار النواة إصدار المدير البصمة وضع SELinux معطل مفروض متساهل مجهول مستخدم خارق فشل في تمكين الوحدة %s فشل تعطيل الإضافة : %s لا توجد إضافات مثبتة الإضافات إلغاء التثبيت تثبيت الوحدة تثبيت إعادة تشغيل الإعدادات إعادة تشغيل سريعة إعادة تشغيل إلى وضع Recovery إعادة تشغيل إلى وضع Bootloader إعادة تشغيل إلى وضع Download إعادة تشغيل إلى وضع EDL من نحن هل أنت متأكد أنك تريد إلغاء تثبيت الإضافة %s ? تم إلغاء تثبيتها %s فشل إلغاء تثبيت %s الإصدار المطور إظهار تطبيقات النظام إرسال السجلات الوضع الآمن إعادة التشغيل لتطبيق التغييرات الوحدات غير متاحة بسبب تعارضها مع Magisk! تعلم KernelSU https://kernelsu.org/guide/what-is-kernelsu.html تعرف على كيفية تثبيت KernelSU واستخدام الإضافات إدعمنا KernelSU سيظل دائماً مجانياً ومفتوح المصدر. مع ذلك، يمكنك متى ما استطعت أن تظهر لنا أنك تهتم بالتبرع. إنضم إلى قناتنا في %2$s ]]> القدرات تحديث تحميل الإضافة: %s ابدأ التنزيل: %s الإصدار الجديد: %s متاح ، انقر للتحديث. تشغيل الإفتراضي نموذج موروث عالمي فردي مجموعات مُخصّص تركيب مساحة الاسم الغاء تحميل الإضافات فشل تحديث ملف تعريف التطبيق لـ %s سياق SELinux ايقاف إجباري الغاء تحميل الإضافات بشكل افتراضي القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. المجال القواعد إعادة تشغيل التطبيق فشل تحديث قواعد SELinux لـ %s اسم الملف الشخصي إصدار KernelSU الحالي %1$d منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %2$d أو أعلى! سجل التغييرات تم الاستيراد بنجاح تصدير إلى الحافظة لا يمكن العثور على القالب المحلي للتصدير! معرف القالب موجود بالفعل! استيراد من الحافظة الاسم معرف القالب غير صالح إنشاء قالب استيراد / تصدير فشل في حفظ القالب تحرير القالب المعرف قالب ملف تعريف التطبيق الوصف حفظ إدارة القالب المحلي وعبر الإنترنت لملف تعريف التطبيق حذف الحافظة فارغة! عرض القالب فشل في منح صلاحية الجذر! فتح التحقق تلقائيًا من وجود تحديثات عند فتح التطبيق التحقق من التحديث تمكين تصحيح أخطاء WebView يمكن استخدامه لتصحيح أخطاء WebUI، يرجى تمكينه فقط عند الحاجة. التالي اختيار ملف تثبيت مباشر (موصى به) التثبيت على فتحة غير نشطة (بعد OTA) سيتم **إجبار** جهازك على التمهيد إلى الفتحة غير النشطة الحالية بعد إعادة التشغيل! \nاستخدم هذا الخيار فقط بعد انتهاء التحديث. \nأستمرار؟ اختر KMI يوصى باستخدام صورة القسم %1$s إلغاء التثبيت إلغاء التثبيت مؤقتًا إلغاء التثبيت بشكل دائم استعادة الصورة الاصلية ‬إلغاء تثبيت KernelSU .(الجذر وجميع الوحدات) بشكل كامل ودائم. تركيب نجح التركيب فشل التركيب LKM المحددة: %s استعادة صورة المصنع المخزنة (في حالة وجود نسخة احتياطية)، والتي تُستخدم عادة قبل OTA؛ إذا كنت بحاجة إلى إلغاء تثبيت KernelSU، فيرجى استخدام \"إلغاء التثبيت الدائم\". قم بإلغاء تثبيت KernelSU مؤقتًا، واستعد إلى حالته الأصلية بعد إعادة التشغيل التالية. حفظ السجلات إجراء السجلات محفوظة فرز (الممكن أولاً) فرز (الإجراء أولاً) الحزم الاتيه سيتم تثبيتها %1$s تأكيد من الغير ممكن اعطاء صلاحيات (المسخدم الخارق) لـ %s إعادة توجيه ثنائي su يسمح للتطبيقات التي تم منحها صلاحيات Superuser في ملف تعريف التطبيق بالحصول على shell superuser من خلال تنفيذ /system/bin/su؛ فعال فقط للعمليات الجديدة. إلغاء تحميل النواة سلوك إلغاء تحميل مستوى النواة الذي يسيطر عليه KernelSU ================================================ FILE: manager/app/src/main/res/values-az/strings.xml ================================================ Ana səhifə Nüvə Yüklənmədi Yükləmək üçün toxunun İşləyir Versiya: %d Hal-hazırda KernelSU yalnız GKI nüvələrini dəstəkləyir Dəstəklənmir Yüklə Yüklə Naməlum Barmaq izi Menecer versiyası Qeyri-aktiv SELinux vəziyyəti Sərbəst Məcburi Super istifadəçi Sil Modulu aktiv etmək mümkün olmadı: %s Modulu deaktiv etmək mümkün olmadı: %s Heç bir modul quraşdırılmayıb Modul Yenidən başlat Parametrlər Bərpa rejimində yenidən başlat Yüngül vəziyyətdə yenodən başlat Bootloader rejimində yenidən başlat Yükləmə rejimində yenidən başlat Versiya Sahib Modulu silmək istədiyinizdən əminsiniz %s\? Sistem proqramlarını göstər Haqqında EDL rejimində yenidən başlat Silmək mümkün olmadı: %s %s silindi Log-u göndər Təhlükəsiz rejimi Qüvvəyə minməsi üçün yenidən başlat Modular deaktiv edilir,çünki o Magisk-in modulları ilə toqquşur! KernelSU-yu öyrən https://kernelsu.org/guide/what-is-kernelsu.html Bizi dəstəkləyin KernelSU-yu necə quraşdırılacağını və modulların necə istifadə ediləcəyini öyrən Şablon Defolt Özəl KernelSU pulsuz və açıq mənbəlidir,həmişə belə olacaqdır. Bununla belə, ianə etməklə bizə qayğı göstərdiyinizi göstərə bilərsiniz. Mənbə kodlarımıza baxın %1$s
Kanalımıza %2$s qoşulun
Profil adı Bacarıqlar Modulları umount et Miras qalmış Qlobal Bölmənin ad sahəsi Fərdi Qruplar Defolt olaraq modulları umount et SELinux konteksi %s görə tətbiq profillərini güncəlləmək mümkün olmadı Tətbiq Profillərində \"Umount modulları\" üçün qlobal standart dəyər. Aktivləşdirilərsə, o, Profil dəsti olmayan proqramlar üçün sistemdəki bütün modul dəyişikliklərini siləcək. Domen Qaydalar Güncəllə Endirməni başlat: %s Yeni versiya: %s əlçatandır, endirmək üçün toxunun Modul yüklənir: %s Bu seçimi aktivləşdirmək KernelSU-ya bu proqram üçün modullar tərəfindən hər hansı dəyişdirilmiş faylları bərpa etməyə imkan verəcək. Məcburi dayandır Təkrar başlat %s görə SELinux qaydalarını güncəlləmək mümkün olmadı Girişləri Saxla Cari KernelSU versiyası %1$d menecerin düzgün işləməsi üçün çox aşağıdır. Lütfən, %2$d və ya daha yüksək versiyaya təkmilləşdirin! Aşağıdakı modullar quraşdırılacaq: %1$s Təsdiq edin Sıralama (ilk hərəkət) Sıralama (Əvvəlcə aktivdir) Dəyişikliklər jurnalı Tətbiq Profil Şablonu Tətbiq Profilinə aid yerli və onlayn şablonların idarə olunması Şablon yarat Şablonu redaktə et ID Etibarsız şablon ID-si Ad Açıqlama %s üçün Superistifadəçi girişi vermək mümkün olmadı. Yadda saxla Sil Şablonu göstər Şablon ID-si artıq mövcuddur! su binary yönləndirmə Tətbiq Profilinə Superuser icazəsi verilmiş proqramları /system/bin/su vasitəsilə superuser shell əldə etməyə icazə verir; yalnız yeni proseslər üçün effektivdir. Nüvə umount KernelSU tərəfindən idarə olunan nüvə səviyyəsində umount davranışı
================================================ FILE: manager/app/src/main/res/values-bg/strings.xml ================================================ Начало Не е инсталирано Натиснете да инсталирате Работи Версия: %d Неподдържано KernelSU само поддържа GKI кернели за сега. Версия на кернела Версия на мениджъра Пръстов отпечатък SELinux статус Деактивиран Налагащ Разрешителен Неизвестен Суперпотребител Неуспешно включване на модула: %s Неуспешно изключване на модула: %s Няма инсталирани модули Модул Следните модули ще бъдат инсталирани: %1$s Първо действие Първо включено Потвърди Деинсталиране Инсалирай Инсталирай Рестартирай Настройки Меко рестартиране Рестартирай във възстановяване Рестартирай в Booloader Рестартирай в Download Рестартирай в EDL Относно Сигурни ли сте че искате да премахнете модула %s? Сигурни ли сте, че искате да деинсталирате модула %s? Това действие ще засегне всички модули и някои функции, предоставяни от метамодула (като например \"mounting\"), вече няма да работят. %s премахнат Неуспешно премахване на: %s Версия Автор Покажи системно приложения Прати логове Безопасен режим Рестартирай за да приеме ефект Модулите не са налични поради конфликт с Magisk! Научи KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Научи как да изтеглите KernelSU и да използвате модули. Помогнете Ни KernelSU си е, и винаги ще е безплатен, и с отворен код. Обаче, можете да ни покажете че се интересувате като ни направите дарение. Присъединете се към нашия%2$s канал]]> По подразбиране Шаблон По избор Име на профил Прикачи Namespace Наследени Глобален Индивид Групи Възможности SELinux контекст Разкачи модули по подразбиране Неуспешно актуализиране на App Profile за %s Текущата версия %1$d на KernelSU е твърде ниска, за да работи правилно мениджърът. Моля, актуализирайте към версия %2$d или по-висока! Разкачи модули Глобалната стойност по подразбиране за „Разкачи модули“ в профила на приложението. Ако е активирана, ще премахне всички модификации на модулите в системата за приложения, които нямат зададен профил. Активирането на тази опция ще позволи на KernelSU да възстанови всички модифицирани файлове от модулите за това приложение. Домейн Правила Актуализиране Изтегляне на модул: %s Започни изтегляне: %s Нова версия %s е налична, натисни да актуализирате! Стартирай Принудително спиране Радтартирай Неуспешно актуализиране на SELinux правила за %s Даването на Суперпотребителски права на %s не беше успешно Списък с промени Шаблон за профил на приложение Управлявайте локален и онлайн шаблон на профил на приложението. Създаване на шаблон Модифициране на шаблон ID Невалидно шаблонено ID Име Описание Запази Премахни Разгледай шаблон Шаблонно ID вече съществува! Внасяне/Изкарване Внасяне от клипборда Изнасяне във клипборда Не може да се намери локален шаблон за изнасяне! Внасяне успешно Неуспешно запазване на шаблон Клипборда е празен! Засегва следните приложения Провери за актуализаций Автоматично провери за актуализаций когато отворите приложението. Провери за актуализаций на модулите Неуспешно даване на root права! Действие Отвори Отстраняване на грешки на WebView Може да се използва за отстраняване на грешки в WebUI. Моля активирайте само когато е необходимо. Директно изтегляне (Препоръчано) Изберете файл Изтеглете в неактивния слот (След софтуерна актуализация) Устройството ви ще бъде **ПРИНУДЕНО** да се стартира от неактивния слот след рестартиране!\nИзползвайте тази опция само след като софтуерната актуализация приключи.\nИскате ли да продължите? Следваш Избери раздел Използвай локален LKM файл Само .ko файлове се поддържат Препоръчва се образ на дял %1$s Изберете KMI Премахнете Премахнете временно Премахнете завинаги Възстановяване на стоков .img фаил Временно премахнете KernelSU, ще се върне в нормално състояние след следващото рестартиране. Премахване на KernelSU (root и всички модули) напълно, завинаги. Възстановете фабричното копие (ако съществува резервно копие), обикновено използвано преди софтуерна актуализация; ако трябва да премахнете KernelSU, моля, използвайте „Премахване завинаги“. Флашване Флашване успешно Флашване не бе успешно Избран LKM: %s Запази логове Логовете бяха запазени Кърнълът не поддържа тази функция. Тази функция е управлявана от модул. Пренасочване на su бинарния файл Позволяет приложениям с предоставени права на Superuser в профила на приложението да получат superuser shell чрез изпълнение на /system/bin/su; в сила само за нови процеси. Kernel umount Поведение за umount на ниво ядро, контролирано от KernelSU Обработва се… Опънете надолу за да обновите Пуснете за да обновите Обновяване… Обновено успешно Откажи Успешно отказано премахването на %s Неуспешно отказване на премахване: %s Съдържа %d приложения Тема Изберете режима на темата на приложението. Следвай системата Светъл Тъмен Цвят на копчето По подразбиране Син Червен Зелен Лилав Оранжево Тил Розово Кафяво Репота По име (А → Я) Изходен код Започвайки от v3.0.0, работният режим на GKI ще се използва само в тестови среди. Не го препоръчваме за ежедневна употреба и файлове с изображения вече няма да се предоставят. Не е свързан с интернет Пробвай отново ПРОЧЕТИ МЕ Версии Информация ================================================ FILE: manager/app/src/main/res/values-bn/strings.xml ================================================ হোম ইনস্টল করা হয়নি ইনস্টল করার জন্য ক্লিক করুন ওয়ার্কিং ওয়ার্কিং সংস্করণ: %d অসমর্থিত KernelSU শুধুমাত্র GKI কার্নেল সমর্থন করে কার্নেল সংস্করণ ম্যানেজার সংস্করণ ফিঙ্গারপ্রিন্ট SELinux স্টেটাস ডিজেবল কার্যকর অনুমতিমূলক অজানা সুপার ইউজার মডিউল সক্ষম করতে ব্যর্থ হয়েছে: %s মডিউল নিষ্ক্রিয় করতে ব্যর্থ হয়েছে: %s কোন মডিউল ইনস্টল করা নেই মডিউল আনইন্সটল মডিউল ইনস্টল ইনস্টল রিবুট সেটিংস সফট রিবুট রিবুট রিকোভারি রিবুট বুটলোডার রিবুট ডাউনলোড রিবুট ইডিএল এবাউট মডিউল আনইনস্টল নিশ্চিত করুন %s? %s আনইনস্টল সফল আনইন্সটল ব্যর্থ: %s ভার্সন লেখক শো সিস্টেম অ্যাপস সেন্ড লগ সেইফ মোড রিবুট এপ্লাই মডিউলগুলি অক্ষম কারণ তারা ম্যাজিস্কের সাথে বিরোধিতা করে! লার্ন কার্নেলএসইউ https://kernelsu.org/guide/what-is-kernelsu.html কিভাবে কার্নেলএসইউ ইনস্টল করতে হয় এবং মডিউল ব্যবহার করতে হয় তা শিখুন। সাপোর্ট টাইটেল কার্নেলএসইউ ফ্রি ও ওপেন সোর্স, এবং সবসময় এমনই থাকবে। তবে, আপনি অনুদান দিয়ে আপনার কৃতজ্ঞতা প্রদর্শন করতে পারেন। আমাদের %2$s চ্যানেল মার্জ করুন]]> প্রফাইলের নাম নেমস্পেস মাউন্ট গ্রুপস যোগ্যতা এসই লিনাক্স কনটেক্সট ডিফল্ট টেমপ্লেট কাস্টম গ্লোবাল আলাদাভাবে আনমাউন্ট মোডিউল ম্যানেজার সঠিকভাবে কাজ করার জন্য বর্তমান KernelSU সংস্করণ %1$d খুবই কম। অনুগ্রহ করে %2$d বা উচ্চতর সংস্করণে আপগ্রেড করুন! লগ সংরক্ষণ করুন এই মডিউলগুলো ইনস্টল করা হবেঃ %1$s চালুগুলো আগে সোর্স কোড নিশ্চিত আপনি কি নিশ্চিত আপনি মডিউল %s আনইনস্টল করতে চান? এটি সকল মডিউলকে প্রভাবিত করবে, এবং মেটামডিউল দ্বারা প্রদত্ত কিছু বৈশিষ্ট্য (যেমন মাউন্ট করা) আর কাজ করবে না। v3.0.0 থেকে শুরু করে, GKI মোড শুধুমাত্র নিরীক্ষণ এর জন্য ব্যবহার করা যাবে। আমরা দৈনন্দিন ব্যবহারের জন্য এটি সুপারিশ করি না, এবং ইমেজ ফাইল আর প্রদান করা হবে না। %s এর জন্য অ্যাপ প্রোফাইল আপডেট করতে ব্যর্থ হয়েছে su বাইনারি রি-রুট করুন অ্যাপ প্রোফাইলে সুপার ইউজার অনুমতি প্রাপ্ত অ্যাপগুলির জন্য /system/bin/su কে ksud এ রিডাইরেক্ট করুন; শুধুমাত্র নতুন প্রসেসের জন্য কার্যকর। কার্নেল আনমাউন্ট KernelSU দ্বারা নিয়ন্ত্রিত কার্নেল-লেভেল আনমাউন্ট আচরণ ================================================ FILE: manager/app/src/main/res/values-bn-rBD/strings.xml ================================================ কর্নেল এস ইউ কেবল মাত্র জিকআই কর্নেল সাপোর্ট করে এসইলিনাক্স স্টেটাস আননোন মোডিউল ইনেবল করা যায়নি: %s ইন্সটল করটে চাপুন কাজ করছে অমূলক কর্নেল ম্যানেজার ভারসন ফিঙ্গারপ্রিন্ট ডিসেবল এনফোর্সিং সুপার ইউজার মোডিউল আনইন্সটল ইন্সটল ইন্সটল রিবুট সেটিংস সফট রিবুট গ্লোবাল গ্রুপস এসইলিনাক্স কন্টেক্সট %s এর জন্য অ্যাপ প্রফাইল আপডেট করা যায়নি বাইডিফল্ট মোডিউল আনমাউন্ট হোম ইন্সটল হয়নী পারমিসিভ মোডিউল ডিসেবল করা যায়নি: %s কোনো মোডিউল ইন্সটল করা নেই সংস্করণ: %d নেইম স্পেস মাউন্ট ইনহেরিটেড ইন্ডিভিজুয়াল ক্যাপাবিলিটিস আনমাউন্ট মোডিউলস রিকভারিতে বুট বুটলোডারে বুট ডাউনলোড মডে বুট ইমারজেন্সি ডাউনলোড মডে বুট অ্যাবাউট %s মোডিউল আনইনস্টলের বেপারে নিশ্চিৎ\? %s আনইনস্টলড %s আনইনস্টল করা যায়নি ভার্সন অথার লগ সংরক্ষণ করুন su বাইনারি পুনর্নির্দেশ করুন অ্যাপ প্রোফাইলে সুপারইউজার অনুমতি সহ অ্যাপের জন্য /system/bin/su কে ksud-এ পুনর্নির্দেশ করুন; শুধুমাত্র নতুন প্রক্রিয়ার জন্য কার্যকর। কার্নেল আনমাউন্ট KernelSU দ্বারা নিয়ন্ত্রিত কার্নেল-লেভেল আনমাউন্ট আচরণ ================================================ FILE: manager/app/src/main/res/values-bs/strings.xml ================================================ Imenski prostor nosača Naslijeđen Globalan Pojedinačan Grupe Sposobnosti SELinux kontekst Umount module Ažuriranje Profila Aplikacije za %s nije uspjelo Trenutna KernelSU verzija %1$d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %2$d ili noviju! Umount module po zadanom Globalna zadana vrijednost za \"Umount module\" u Profilima Aplikacije. Ako je omogućeno, uklonit će sve izmjene modula na sistemu za aplikacije koje nemaju postavljen Profil. Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. Ažuriranje Skidanje module: %s Započnite sa skidanjem: %s Nova verzija: %s je dostupna, kliknite da skinete Pokrenite Prisilno Zaustavite Resetujte U Provođenju Početna Nije instalirano Kliknite da instalirate Nepodržano KernelSU samo podržava GKI kernele sad Verzija Upravitelja Otisak prsta SELinux stanje Instalirajte Instalirajte Ponovo pokrenite Podešavanja Verzija Autor Prikažite sistemske aplikacije Sigurnosni mod Ponovo pokrenite da bi proradilo "Moduli su nedostupni jer su u sukobu sa Magisk-om!" https://kernelsu.org/guide/what-is-kernelsu.html Naučite kako da instalirate KernelSU i da koristite module Podržite Nas Pošaljite Izvještaj Naučite KernelSU Pogledajte izvornu kodu na %1$s
Pridružite nam se na %2$s kanalu
Domena Pravila Neuspješno ažuriranje SELinux pravila za: %s Radi Verzija: %d Kernel verzija Permisivno Deinstalirajte Nepoznato Nema instaliranih modula Superkorisnik Modula Ponovo pokrenite u Pogonski Učitavatelj Ponovo pokrenite u Oporavu %s deinstalirana Lagano Ponovo pokretanje Neuspješno uključivanje module: %s Ponovo pokrenite u Preuzimanje Neuspješno isključivanje module: %s Ponovo pokrenite u EDL Neuspješna deinstalacija: %s Isključeno O Jeste li sigurni da želite deinstalirati modulu %s\? KernelSU je, i uvijek če biti, besplatan, i otvorenog izvora. Možete nam međutim pokazati da vas je briga s time da napravite donaciju. Zadano Šablon Prilagođeno Naziv profila Sačuvaj Dnevnike Modul će biti instaliran Sortiraj Potvrdi Preusmjeri su binarnu datoteku Preusmjeri /system/bin/su na ksud za aplikacije kojima je dodijeljeno dopuštenje Superuser u Profilu aplikacije; učinkovito samo za nove procese. Kernel umount Ponašanje umount na razini kernela kontrolira KernelSU
================================================ FILE: manager/app/src/main/res/values-da/strings.xml ================================================ Arbejder Ikke understøttet Kernel-version KernelSU understøtter nu kun GKI-kerner. Manager version SELinux-status Deaktiveret Tilladende Superbruger Håndhævende Deaktivering af modul fejlede: %s Intet modul installeret Afinstaller Installer Installer Genstart Indstillinger Blød genstart Genstart til Download Genstart til EDL Om Er du sikker på, at du vil afinstallere modulet %s\? %s afinstalleret Afinstallation af: %s fejlede Send logfiler Sikker tilstand Genstart for at tage effekt Lær KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Lær hvordan man installerer KernelSU og moduler. Join our %2$s channel]]> Standard Skabelon Monter namespace Arvet Global Grupper Evner SELinux-kontext Afmonteret moduler Afmontere moduler Hvis denne indstilling aktiveres, kan KernelSU gendanne alle ændrede filer af modulerne til denne app. Opdatering Downloader modulet: %s Ny version %s er tilgængelig, klik for at opgradere! Start Tving stop Opdatering af SELinux-regler mislykkedes for %s Start download: %s Klik for at installere Version: %d Hjem Ikke installeret Fingeraftryk Ukendt Aktivering af modul fejlede: %s Genstart til Recovery Modul Forfatter Genstart til Bootloader Version Vis system-apps Moduler er utilgængelige på grund af en konflikt med Magisk! Støt Os KernelSU er, og vil altid være, gratis og åben kildekode. Du kan dog vise os, at du holder af os, ved at give en donation. Brugerdefineret Profilnavn Individuel Opdatering af App Profil for %s fejlede Den globale standardværdi for \"Umount moduler\" i App Profile. Hvis aktiveret, fjernes alle modulændringer til systemet for apps, der ikke har en profil angivet. Domæne Regler Genstart Den nuværende KernelSU-version %1$d er for lav til, at manageren fungerer korrekt. Opgrader venligst til version %2$d eller højere! Gem logs Følgende moduler installeres: %1$s Sorter (Handling først) Sorter (Aktiveret først) Bekræft Kunne ikke tildele superbruger-adgang til %s Ændringslog App-profilskabelon Administrer lokale og online skabeloner til App-profil. Opret skabelon Rediger skabelon ID Ugyldigt skabelon-ID Navn Beskrivelse Gem Slet Visningsskabelon Skabelon-ID findes allerede! Import/Eksport Importér fra udklipsholder Eksporter til udklipsholder Kan ikke finde lokal skabelon til eksport! Importér med succes Kunne ikke gemme skabelon Udklipsholderen er tom! Check for opdateringer Søg automatisk efter opdateringer, når appen åbnes. Kunne ikke tildelle root! Handling Åbn WebView-fejlsøgning Kan bruges til at fejlfinde af WebUI. Aktiver kun når det er nødvendigt. Direkte installation (Anbefalet) Vælg en fil Installer på inaktiv slot (efter OTA-opdatering) Din enhed vil blive **TVUNGET** til at starte op fra det aktuelt inaktive slot efter en genstart!\nBrug kun denne mulighed, når OTA er færdig.\nFortsæt? Næste %1$s partitionsbillede anbefales Vælg KMI Afinstaller Afinstaller midlertidigt Afinstaller permanent Gendan systemets standardbillede Afinstaller KernelSU midlertidigt, og gendan til den oprindelige tilstand efter næste genstart. Afinstallation af KernelSU (root og alle moduler) fuldstændigt og permanent. Gendan det originale fabriksbillede (hvis en sikkerhedskopi eksisterer), hvilket normalt bruges før en OTA-opdatering; hvis du skal afinstallere KernelSU, skal du bruge \"Afinstaller permanent\". Flashing Flash-succes Flash mislykkedes Valgt LKM: %s Logs gemt Søg efter modulopdateringer Brug lokal KLM-fil Kun .ko-filer understøttes Omdiriger su binary Tillader apps med Superuser-tilladelse i App Profile at få superuser shell ved at udføre /system/bin/su; gælder kun for nye processer. Kernel umount Kernel-niveau umount adfærd kontrolleret af KernelSU Behandler… Træk ned for at opdatere Slip for at opdatere Opdaterer… Opdateret ================================================ FILE: manager/app/src/main/res/values-de/strings.xml ================================================ Startseite Nicht installiert Permissiv Funktioniert Version: %d Superuser Tippe zum Installieren Unbekannt Erzwingen In den Bootloader-Modus neustarten In den Download-Modus neustarten In den EDL-Modus neustarten Autor Über KernelSU Module sind aufgrund eines Konfliktes mit Magisk nicht verfügbar! https://kernelsu.org/guide/what-is-kernelsu.html Erfahre, wie KernelSU installiert wird und wie Module verwendet werden. Unterstütze uns KernelSU ist und wird immer frei und quelloffen sein. Du kannst uns jedoch deine Unterstützung zeigen, indem du eine Spende tätigst. SELinux-Kontext Module standardmäßig aushängen Globaler Standardwert für \"Module aushängen\" im App-Profil. Falls er aktiviert ist, werden alle Moduländerungen im System für alle Apps entfernt, für die kein Profil festgelegt ist. Standard Vorlage Benutzerdefiniert App-Profilaktualisierung für %s fehlgeschlagen Geerbt Global Individuell Domäne Aktualisieren Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. Regeln Starte Download: %s Aktualisieren der SELinux-Regeln schlug fehl für %s Starten Neue Version %s verfügbar, tippen zum Aktualisieren! Stopp erzwingen Neustarten Manager-Version SELinux Status Deaktiviert Modulaktivierung fehlgeschlagen: %s Moduldeaktivierung fehlgeschlagen: %s Keine Modul installiert Modul Deinstallieren Installieren Neustarten Einstellungen In den Recovery-Modus neustarten %s deinstalliert Version System-Apps anzeigen Protokoll senden KernelSU verstehen Sicherer Modus Neustarten, damit Änderungen wirksam werden Unserem %2$s-Kanal beitreten]]> Profilname Namespace einhängen Gruppen Fähigkeiten Module aushängen Lädt Modul %s herunter Nicht unterstützt KernelSU unterstützt derzeit nur GKI-Kernel. Kernelversion Fingerabdruck Installieren Soft Reboot Möchtest du wirklich Modul %s deinstallieren? Deinstallation fehlgeschlagen: %s Die aktuelle KernelSU-Version %1$d ist zu alt für diese Manager-Version. Bitte auf Version %2$d oder höher aktualisieren! Änderungsprotokoll Erfolgreich importiert In Zwischenablage exportieren Kann lokale Vorlage nicht finden! Vorlagen-ID existiert bereits! Aus Zwischenablage importieren Name Ungültige Vorlagen-ID Vorlage erstellen Import/Export Schlug beim Speichern der Vorlage fehl Vorlage bearbeiten ID App Profil-Vorlage Beschreibung Speichern Verwalte die lokale und online Vorlage des App-Profils. Löschen Zwischenablage ist leer! Vorlage ansehen WebView-Debugging Kann zum Fehlerbeheben der WebUI verwendet werden. Bitte nur im Notfall aktivieren. %1$s Partitionsabbild empfohlen KMI auswählen Weiter Direkte Installation (empfohlen) Datei auswählen In inaktiven Slot installieren (nach OTA) Nach einem Neustart wird dein Gerät **GEZWUNGEN** in den derzeit inaktiven Slot zu starten! \nBenutze dies nur nach Fertigstellung des OTA. \nFortfahren? Root-Zugriff konnte nicht gewährt werden! Öffnen Auf Aktualisierungen prüfen Prüfe automatisch auf Aktualisierungen, wenn die App geöffnet wird. Temporär deinstallieren Deinstallieren KernelSU (Root und alle Module) vollständig und dauerhaft deinstallieren. Protokolle Speichern Permanent deinstallieren Standard-Abbild wiederherstellen KernelSU temporär deinstallieren, originalen Status nach dem nächsten Neustart wiederherstellen. Das Standard Werksabbild wiederherstellen (falls ein Backup existiert), normalerweise vor einem OTA zu verwenden; falls Sie KernelSU deinstallieren müssen, nutzen Sie bitte \"Permanent deinstallieren\". Schreibt Schreiben erfolgreich Schreiben fehlgeschlagen Wähle LKM: %s Aktion Protokolle gespeichert Folgende Module werden installiert: %1$s Bestätigen Aktion zuerst Aktiviert zuerst Repos Name (A → Z) Quellcode Sind Sie sich sicher, das Modul %s zu deinstallieren? Diese Aktion wirkt sich auf alle Module aus und spezielle Funktionen, welche von dem Metamodul bereitgestellt werden (sowie montieren), werden nicnt mehr funktioneren. Modulinstallation ist im sicheren Modus deaktiviert Ab v3.0.0 wird der GKI Arbeitsmodus nur in Testumgebungen genutzt werden. Wir empfehlen es nicht für die tägliche Nutzung und Bilddateien werden nicht mehr bereitgestellt werden. Es konnte für %s keine Superuser-Berechtigung gegeben werden Wirkt sich auf folgende Apps aus Su-Binärdatei umleiten Ermöglicht Anwendungen mit Superuser-Berechtigung im App-Profil, eine Superuser-Shell durch Ausführung von /system/bin/su zu erhalten; wirksam nur für neue Prozesse. Auf Modulaktualisierungen prüfen Partition wählen Nutze lokale LKM Datei Nur .ko Dateien unterstützt ================================================ FILE: manager/app/src/main/res/values-es/strings.xml ================================================ Inicio No instalado Haz clic para instalar Funcionando Versión: %d Sin soporte KernelSU solo admite kernels GKI por ahora Versión del kernel Versión del gestor Huella del dispositivo Estado de SELinux Desactivado Estricto Permisivo Desconocido Superusuario Error al activar el módulo: %s Error al desactivar el módulo: %s Ningún módulo instalado Módulo Desinstalar Instalar Instalar Reiniciar Ajustes Reinicio suave Reiniciar en modo de recuperación Reiniciar en modo de arranque Reiniciar en modo Download Reiniciar en modo EDL Acerca de ¿Está seguro de que desea desinstalar el módulo %s? %s desinstalado Fallo al desinstalar: %s Versión Autor Mostrar aplicaciones del sistema Enviar registros Modo seguro Reinicia para aplicar cambios ¡Los módulos no están disponibles debido a un conflicto con Magisk! Aprende KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Aprende a instalar KernelSU y a utilizar módulos Apóyanos KernelSU es, y siempre será, gratuito y de código abierto. Sin embargo, puedes demostrarnos que te importamos haciendo una donación. Ver código fuente en %1$s
Únete a nuestro canal %2$s
Predeterminado Plantilla Personalizado Nombre de perfil Montaje del espacio de nombres Heredado Global Individual Grupos Capacidades Contexto SELinux Desmontar módulos Error al actualizar el perfil de la aplicación para %s Desmontar módulos por defecto El valor global predeterminado para \"Umount modules\" en App Profile. Si está activado, eliminará todas las modificaciones de módulos del sistema para las apps que no tengan un perfil establecido. Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. Dominio Reglas Actualizar Descargando módulo: %s Iniciar descarga: %s La nueva versión %s está disponible, haga clic para actualizar. Iniciar Forzar detención Reiniciar Error al actualizar las reglas SELinux para: %s La versión %1$d actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %2$d o superior! Registro de cambios Importado con éxito Exportar al portapapeles ¡No se encuentra la plantilla local para exportar! ¡El ID de plantilla ya existe! Importar desde el portapapeles Nombre ID de plantilla no válida Crear plantilla Importar/Exportar No se ha podido guardar la plantilla Editar plantilla ID Plantilla de perfil de aplicación Descripción Guardar Gestionar la plantilla local y en línea de App Profile Eliminar ¡El portapapeles está vacío! Ver plantilla Guardar registros Activar la depuración de WebView Se recomienda la imagen de partición %1$s Selecciona KMI Siguiente Instalación directa (Recomendada) ¡Su dispositivo será **FORZADO** a arrancar en la ranura inactiva actual después de un reinicio!\nUtilice esta opción sólo después de que la OTA se haya realizado.\n¿Continuar? Desinstalar Restaurar imagen de archivo Desinstalar temporalmente KernelSU, restaurar al estado original tras el siguiente reinicio. LKM seleccionado: %s Flash falló Éxito de Flash ¡No se ha podido conceder el acceso root! Abrir Seleccione un archivo Instalar en ranura inactiva (Después de OTA) Desinstalar temporalmente Desinstalar permanentemente Desinstalar KernelSU (Root y todos los módulos) completa y permanentemente. Comprobar actualización Comprobación automática de actualizaciones al abrir la aplicación Puede ser usado para depurar WebUI, por favor habilítalo sólo cuando sea necesario. Restaurar la imagen de fábrica stock (Si existe una copia de seguridad), por lo general se utiliza antes de OTA; si necesita desinstalar KernelSU, por favor, utilice \"Desinstalar permanentemente\". Redirigir binario su Permite que las aplicaciones con permiso de Superusuario en el perfil de la aplicación obtengan un shell de superusuario ejecutando /system/bin/su; efectivo solo para nuevos procesos. Desmontaje del kernel Comportamiento de desmontaje a nivel de kernel controlado por KernelSU
================================================ FILE: manager/app/src/main/res/values-et/strings.xml ================================================ Töötamine Versioon: %d Tuum Manageri versioon Sõrmejälg Lubav Mooduli lubamine ebaõnnestus: %s Mooduleid pole paigaldatud Taaskäivita Taaskäivita taastusesse Kas soovid kindlasti eemaldada mooduli %s? %s eemaldatud Saada logid Turvarežiim Muudatuste rakendamiseks taaskäivita Õpi KernelSUd https://kernelsu.org/guide/what-is-kernelsu.html Vaikimisi Haagi nimeruum Lahtihaagitud moodulid Rakenduseprofiili uuendamine %s jaoks ebaõnnestus Haagi moodulid vaikimisi lahti Allalaadimise alustamine: %s SELinux reeglite uuendamine ebaõnnestus: %s Muuda malli Rakenduseprofiili mall ID Malli ID juba eksisteerib! Ekspordi lõikelauale Kodu Klõpsa paigaldamiseks Pole paigaldatud Mittetoetatud KernelSU toetab hetkel vaid GSI tuumasid SELinuxi olek Keelatud Jõustav Teadmata Superkasutaja Mooduli keelamine ebaõnnestus: %s Moodul Taaskäivita käivituslaadurisse Eemalda Paigalda Teave Paigalda Seaded Pehme taaskäivitus Taaskäivita allalaadimisrežiimi Taaskäivita EDL-i Autor Eemaldamine ebaõnnestus: %s Versioon Kuva süsteemirakendused Moodulid pole saadaval Magiski konflikti tõttu! Õpi KernelSUd paigaldama ja mooduleid kasutama Toeta meid Grupid KernelSU on, ja alati jääb, tasuta ning avatud lähtekoodiga kättesaadavaks. Sellegipoolest võid sa näidata, et hoolid, ning teha annetuse. Mall Vaata lähtekoodi %1$sis
Liitu meie %2$si kanaliga
Profiili nimi Kohandatud Päritud Globaalne Individuaalne Võimekused Sobimatu malli ID SELinux kontekst Praegune KernelSU versioon %1$d on liiga madal, haldur ei saa korrektselt töötada. Palun täienda versioonile %2$d või kõrgem! Domeen Käivita Sundpeata Reeglid Uuenda Mooduli allalaadimine: %s Uus versioon %s on saadaval, klõpsa täiendamiseks. Taaskäivita Muudatuste logi Nimi Kirjeldus Edukalt imporditud Salvesta Lõikelaud on tühi! Kustuta Vaata malli Impordi/ekspordi Impordi lõikelaualt Malli salvestamine ebaõnnestus Loo mall Halda kohalikke ja võrgusolevaid rakenduseprofiili malle Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. Ei saa eksportida, kohalikku malli ei leitud! Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. Saab kasutada WebUI silumiseks, palun luba ainult vajadusel. Juurkasutaja andmine ebaõnnestus! Kontrolli uuendusi Rakenduse avamisel kontrolli automaatselt uuendusi Ava Luba WebView silumine Salvesta Logid Vali KMI %1$s partitsioonitõmmis on soovitatud Edasi Sinu seade **SUNNITAKSE** pärast taaskäivitust ebaaktiivsesse lahtrisse käivituma!\nKasuta seda valikut vaid siis, kui tegid üle-õhu uuenduse.\nJätkad? Eemalda Eemalda KernelSU ajutiselt, taasta pärast taaskäivitust algseisu. KernelSU eemaldamine (juurkasutaja ja kõik moodulid) täielikult ja püsivalt. Taasta tehase-vaiketõmmis (kui varundus eksisteerib), tavaliselt kasutatakse enne üle-õhu uuendust; kui soovid KernelSU-d eemaldada, palun kasuta \"Eemalda püsivalt\". Välgutamine Välgutamine õnnestus Välgutamine ebaõnnestus Valitud LKM: %s Otsene paigaldus (soovitatud) Vali fail Paigalda ebaaktiivsesse lahtrisse (pärast üle-õhu uuendust) Eemalda ajutiselt Eemalda püsivalt Taasta vaikimisi tõmmis Suuna su binaarfail ümber Võimaldab rakendustel, kellele on antud Superuser luba rakenduse profiilis, saada superuser shell, käivitades /system/bin/su; kehtib ainult uute protsesside puhul. Kernel umount Kerneli tasemel lahtihaakimise käitumine, mida kontrollib KernelSU
================================================ FILE: manager/app/src/main/res/values-fa/strings.xml ================================================ خانه نصب نشده است برای نصب ضربه بزنید به درستی کار می‌کند نسخه: %d پشتیبانی نشده کرنل اس یو فقط هسته های gki را پشتیبانی میکند هسته نسخه برنامه اثرانگشت وضعیت SELinux غیرفعال قانونمند آزاد ناشناخته دسترسی روت فعال کردن ماژول ناموفق بود: %s غیرفعال کردن ماژول ناموفق بود: %s هیچ ماژولی نصب نشده است ماژول لغو نصب نصب نصب راه اندازی دوباره تنظیمات راه اندازی نرم راه اندازی به ریکاوری راه اندازی به بوتلودر راه اندازی به حالت دانلود راه اندازی به EDL درباره آیا مطمئنید که میخواهید ماژول %s را پاک کنید؟ %s پاک شد پاک کردن ناموفق بود: %s نسخه سازنده نمایش برنامه های سیستمی ارسال وقایع حالت امن راه‌اندازی مجدد برای تاثیرگذاری مازول به دلیل تعارض با مجیسک غیرفعال شده اند\'s! یادگیری کرنل اس یو https://kernelsu.org/guide/what-is-kernelsu.html یاد بگیرید چگونه از کرنل اس یو و ماژول ها استفاده کنید از ما حمایت کنید KernelSU رایگان است و همیشه خواهد بود و منبع باز است. با این حال، می توانید با اهدای کمک مالی به ما نشان دهید که برایتان مهم است. Join our %2$s channel ]]> پروفایل برنامه پیش‌فرض قالب شخصی سازی شده اسم پروفایل Mount namespace اثر گرفته گلوبال تکی جداکردن ماژول ها ذخیره گزارش‌ها تغییر مسیر باینری su تغییر مسیر /system/bin/su به ksud برای برنامه‌هایی که مجوز Superuser در پروفایل برنامه دارند؛ فقط برای فرآیندهای جدید موثر است. جداسازی هسته رفتار جداسازی در سطح هسته که توسط KernelSU کنترل می‌شود ================================================ FILE: manager/app/src/main/res/values-fil/strings.xml ================================================ Katayuan ng SELinux Naka-disable Enforcing Permissive Hindi naka-install Panimula I-click para i-install Gumagana Bersyon: %d Hindi matukoy Hindi Suportado Sinusuportahan lamang ng KernelSU ang mga GKI na kernel. Nabigong paganahin ang module: %s Nabigong i-disable ang module: %s Walang naka-install na modyul Modyul I-install I-install I-reboot I-soft reboot I-reboot sa Download I-reboot sa EDL Tungkol Sigurado ka bang gusto mong i-uninstall ang module na %s? Na-uninstall ang %s Nabigong i-uninstall: %s May-akda Ipakita ang mga application ng system Ipadala ang mga log I-reboot para umepekto Hindi magagamit ang mga module dahil sa isang salungatan sa Magisk! Alamin ang KernelSU Matuto kung paano i-install ang KernelSU at gumamit ng mga module Suportahan Kami Ang KernelSU ay, at palaging magiging, libre, at open source. Gayunpaman, maaari mong ipakita sa amin na nagmamalasakit ka sa pamamagitan ng pagbibigay ng donasyon. Sumali sa aming %2$s channel]]> I-mount ang namespace Indibidwal Mga Grupo Mga Kakayanan Konteksto ng SELinux I-unmount ang mga module Nabigong i-update ang App Profile para sa %s Ang kasalukuyang bersyon ng KernelSU %1$d ay masyadong mababa para gumana nang maayos ang manager. Mangyaring mag-upgrade sa bersyon %2$d o mas mataas! Ang pag-enable sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga module para sa app na ito. Mga Tuntunin Nagda-download ng modyul: %s Simulan ang pag-download: %s Bagong bersyon: Available ang %s, i-click para mag-upgrade. Ilunsad Sapilitang itigil I-restart Nabigong i-update ang mga panuntunan ng SELinux para sa: %s Bersyon ng manager Mga setting I-reboot sa Recovery I-reboot sa Bootloader Bersyon I-uninstall Pangalan ng profile Minana Ang pangkalahatang default na halaga para sa \"Umount modules\" sa Mga Profile ng App. Kung pinagana, aalisin nito ang lahat ng mga pagbabago sa modyul sa system para sa mga aplikasyon na walang hanay ng Profile. I-save ang mga Log Bersyon ng kernel Fingerprint Superuser Ii-install ang mga sumusunod na module: %1$s Aksyon muna Pinagana muna Kumpirmahin Safe mode https://kernelsu.org/guide/what-is-kernelsu.html Default Template Pasadya Global I-unmount ang mga module bilang default Domain I-update Hindi mabigay ang Superuser access sa %s Mga pagbabago Template ng App Profile Ipamahala ang lokal at online na template ng App Profile Gumawa ng template I-edit ang template ID Hindi wastong template ID Pangalan Paksa I-save Burahin Tignan ang template Umiiral na ang Template ID! I-import/I-export Mag-import mula sa clipboard I-export sa clipboard Hindi makahanap ng lokal na template na ie-export! Matagumpay na na-import Nabigong i-save ang template Walang laman ang clipboard! Tumingin para sa mga update Awtomatikong tumingin para sa mga update kapag binubuksan ang app Nabigong ibigay ang root! Aksyon Buksan I-enable ang pag-debug ng WebView Maaaring gamitin para i-debug ang WebUI. Mangyaring paganahin kung kinakailangan lang. Direktang pag-install (Inirerekomenda) Pumili ng file I-install sa hindi aktibong slot (Pagkatapos ng OTA) Ang iyong device ay **PIPILITIN** na i-boot sa kasalukuyang hindi aktibong slot pagkatapos ng reboot!\nGamitin lamang ang opsyon na ito kung tapos na ang OTA.\nMagpatuloy? Susunod Inirerekomenda ang %1$s partition image Pumili ng KMI I-uninstall Pansamantalang i-uninstall Permanenteng i-uninstall Ibalik ang stock image Pansamantalang i-uninstall ang KernelSU, ibabalik sa orihinal na kalagayan pagkatapos ng susunod na reboot. Ina-uninstall ang KernelSU (root at lahat ng mga module) nang tuluyan at permanente. Ibalik ang stock factory image (kung may umiiral na backup), kadalasan na ginagamit bago ng OTA; kung kailangan mong i-uninstall ang KernelSU, mangyaring gamitin ang \"Permanenteng i-uninstall\". Nagfa-flash Matagumpay ang pag-flash Nabigo ang pag-flash Piniling LKM: %s Nai-save ang mga log Klasikong su command Payagan ang root access gamit ang /system/bin/su, sa mga bagong proseso. Mga Repo Pangalan (A → Z) Source code Sigurado ka ba gusto mong i-uninstall ang module na %s? Aapektuhan ng aksyon na ito ang lahat ng mga module, at hindi na gagana ang mga feature na ibinigay ng metamodule (tulad ng pag-mount). Naka-disable sa safe mode ang pag-install ng mga module ================================================ FILE: manager/app/src/main/res/values-fr/strings.xml ================================================ Non installé Fonctionnel Version : %d KernelSU prend désormais en charge seulement les noyaux GKI, mais vous pouvez patcher l\'image pour les appareils GKI. Version du noyau Empreinte Statut SELinux Désactivé Permissif (permissive) Inconnu Super-utilisateur Aucun module installé Accueil Appuyez pour installer Non pris en charge Échec de la désinstallation : %s Version Version du gestionnaire Imposition (enforcing) Échec de l\'activation du module : %s Module Désinstaller Installer Échec de la désactivation du module : %s Redémarrer Installer Paramètres Redémarrer vers le bootloader Redémarrage logiciel Redémarrer en mode Recovery Redémarrer en mode EDL À propos %s désinstallé Redémarrer en mode Download Auteur Voulez-vous vraiment désinstaller le module %s ? Apprenez à utiliser KernelSU Afficher les applications système Mode sans échec Envoyer les journaux Redémarrez pour appliquer les modifications Les modules sont indisponibles en raison d\'un conflit avec Magisk ! https://kernelsu.org/guide/what-is-kernelsu.html Soutenez-nous Découvrez comment installer KernelSU et utiliser les modules. KernelSU est, et restera toujours, gratuit et open-source. Néanmoins, vous pouvez nous témoigner de votre soutien en nous faisant un don. Rejoignez notre canal %2$s]]> Modèle Par défaut Personnalisé Nom du profil Espace de noms de montage Hérité Individuel Contexte SELinux Global Groupes Capacités Démonter les modules Échec de la modification du profil d\'application pour %s Activer cette option autorisera KernelSU à restaurer tous les fichiers de cette application qui ont été modifiés par les modules. Démonter par défaut les modules Valeur globale par défaut pour l\'option \"Démonter les modules\" dans les profils d\'application. Lorsque l\'option est activée, les modifications apportées au système par les modules sont supprimées pour les applications qui n\'ont pas de profil défini. Domaine Règles Mettre à jour Téléchargement du module : %s Lancer La nouvelle version %s est disponible, appuyez pour mettre à jour ! Début du téléchargement de : %s Forcer l\'arrêt Relancer l\'application Échec de la mise à jour des règles SELinux pour %s La version actuelle de KernelSU (%1$d) est trop ancienne pour que le gestionnaire puisse fonctionner correctement. Veuillez passer à la version %2$d ou ultérieure. Importation réussie Exporter vers le presse-papiers Impossible de trouver un modèle local à exporter ! L\'ID du modèle existe déjà ! Historique Importer à partir du presse-papiers Nom ID de modèle invalide Créez un modèle Importer/Exporter Échec de l\'enregistrement du modèle Modifier le modèle ID Modèles de profils d\'application Description Enregistrer Gérez les modèles de profils d\'application locaux et en ligne. Supprimer Le presse-papiers est vide ! Voir le modèle Rechercher automatiquement des mises à jour à l\'ouverture de l\'application. Rechercher des mises à jour Débogage WebView Peut être utilisé pour déboguer WebUI. Activez uniquement cette option si nécessaire. Impossible d\'accorder les privilèges root ! Ouvrir Installation directe (recommandé) Sélectionner un fichier Installer dans l\'emplacement inactif (après OTA) Votre appareil sera **FORCÉ** à démarrer sur l\'emplacement inactif actuel après un redémarrage ! \nN\'utilisez cette option qu\'une fois la mise à jour OTA terminée. \nContinuer ? Suivant L\'image de la partition %1$s est recommandée Sélectionner une KMI Désinstaller Désinstaller temporairement Désinstaller définitivement Restaurer l\'image d\'origine Restaurer l\'image d\'origine d\'usine (s\'il en existe une sauvegarde). Utilisé généralement avant une mise à jour OTA ; si vous devez désinstaller KernelSU, utilisez plutôt l\'option \"Désinstaller définitivement\". Flash en cours Flash réussi Échec du flash LKM sélectionné : %s Désinstallation complète et permanente de KernelSU (root et tous les modules). Désinstaller temporairement KernelSU, et rétablir l\'état original au redémarrage suivant. Enregistrer les journaux Par action Par activation Action Journaux enregistrés Les modules suivants seront installés : %1$s Impossible d\'accorder les autorisations superutilisateur à %s Confirmer Utiliser un fichier LKM local Seuls les fichiers .ko sont pris en charge Traitement… Tirez pour actualiser Relâchez pour actualiser Actualisation… Actualisé avec succès Rechercher des mises à jour des modules Commande SU classique Permettre l\'accès root via /system/bin/su, dans les nouveaux processus. Démonter les modules (niveau noyau) Démontez les modules noyau dans le profil d\'application. Sélectionner une partition Voulez-vous vraiment désinstaller le module %s ? Cette action affectera tous les modules, et certaines fonctionnalités fournies par le métamodule (telles que le montage) ne fonctionneront plus. Affecte les applis suivantes Contient %d applis Annuler Désinstallation de %s annulée avec succès Échec de l\'annulation de la désinstallation : %s Thème Choisissez le thème de l\'application. Suivre le système Clair Sombre Couleur clé Par défaut Bleu Rouge Vert Violet Orange Turquoise Rose Marron Le noyau ne prend pas en charge cette fonctionnalité Cette fonctionnalité est gérée par un module Dépôts Nom (A → Z) Code source À partir de la version 3.0.0, le mode de fonctionnement GKI sera utilisé uniquement dans les environnements de test. Nous vous le déconseillons pour une utilisation quotidienne, et les fichiers images ne seront plus fournis. Non connecté au réseau Réessayer README Versions Infos L\'installation de modules est désactivée en mode sans échec Action de module exécutée avec succès. Activer (par défaut) Désactiver jusqu\'au redémarrage Toujours désactiver Créer un raccourci Nom du raccourci Choisir une icône personnalisée Le lanceur d\'application ne prend pas en charge les raccourcis. Raccourci créé sur le bureau. Raccourci mis à jour. Supprimer le raccourci Activez pour cette application l\'autorisation \"Créer des raccourcis de bureau\" dans les paramètres Xiaomi. Activez pour cette application l\'autorisation \"Raccourci de bureau\" dans les paramètres OPPO. Si la création du raccourci échoue, veuillez activer pour cette application l\'autorisation relative aux raccourcis de bureau dans les paramètres du système. Le module %s n\'existe pas Le module %s est désactivé, en cours de mise à jour, ou en attente de suppression Sélectionnez le fichier image d\'appareil GKI que vous souhaitez patcher Version KMI de cet appareil : %s KMI de cet appareil ================================================ FILE: manager/app/src/main/res/values-gl/strings.xml ================================================ Inicio Redirixir o binario su Permite que as aplicacións con permiso de Superusuario no perfil da aplicación obteñan un shell de superusuario executando /system/bin/su; efectivo só para novos procesos. Desmontar núcleo Comportamento de desmontaxe a nivel de núcleo controlado por KernelSU ================================================ FILE: manager/app/src/main/res/values-hi/strings.xml ================================================ प्रभाव में होने के लिए रीबूट करें जानें कि KernelSU कैसे स्थापित करें और मॉड्यूल का उपयोग कैसे करें अज्ञात सिस्टम एप्प दिखाए %s अनइंस्टॉल सफल हुआ मॉड्यूल्स अनमाउंट करें लॉग भेजे डिसेबल्ड (बंद) हमें प्रोत्साहन दें Inherited मॉड्यूल बंद कर दिए गए हैं क्योंकि यह मैजिक के साथ टकरा रहे है! क्या बदलाव हुए है पर्मिसिव डाउनलोड में रिबूट करें डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। Individual %s मॉड्यूल चालू करने में विफल जबर्दस्ती बंद करें EDL मोड में रिबूट करें फिर से चालू करें क्षमताएं %s की डाउनलोडिंग स्टार्ट करें Global ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। एनफोर्सिंग SELinux context फिंगरप्रिंट डिफॉल्ट लॉन्च करें सेफ मोड मैनेजर के ठीक से काम करने के लिए वर्तमान KernelSU वर्जन %1$d बहुत कम है। कृपया वर्जन %2$d या उच्चतर में अपग्रेड करें! रिकवरी में रिबूट करें सॉफ्ट रिबूट प्रोफाइल का नाम KernelSU मुफ़्त और ओपन सोर्स और हमेशा रहेगा। हालाँकि आप दान देकर हमें दिखा सकते हैं कि आप संरक्षण करते हैं। अनइंस्टॉल करें Namspace माउंट करें इंस्टाल करें इंस्टाल करने के लिए क्लिक करें नियम समूह मॉड्यूल निर्माता हमारे बारे में वर्जन: %d रीबूट करें KernelSU अभी केवल GKI कर्नल्स को सपोर्ट करता है SELinux स्थिति वर्जन सपोर्ट नहीं करता है डोमेन होम कस्टम टेम्पलेट %s मॉड्यूल डाउनलोड हो रहा है अपडेट KernelSU सीखें क्या आप सच में मॉड्यूल %s को अनइंस्टॉल करना चाहते हैं\? %s अनइंस्टल करने में असफल सुपरयूजर सेटिंग काम कर रहा है %s मॉड्यूल बंद करने में विफल कोई मॉड्यूल इंस्टाल नहीं हुआ इंस्टाल करें कर्नल इंस्टाल नहीं हुआ %s के लिए ऐप प्रोफ़ाइल अपडेट करने में विफल https://kernelsu.org/guide/what-is-kernelsu.html %s के लिए SELinux नियमों को अपटेड करने में विफल बुटलोडर में रिबूट करें %1$s पर स्रोत कोड देखें
हमारे %2$s चैनल से जुड़ें
मैनेजर वर्जन नया वर्जन: %s उपलब्ध है,अपग्रेड के लिए क्लिक करें लॉग सहेजें su बाइनरी को फिर से रूट करें ऐप प्रोफ़ाइल में सुपरयूज़र अनुमति दिए गए ऐप्स को /system/bin/su को निष्पादित करके सुपरयूज़र शेल प्राप्त करने की अनुमति देता है; केवल नई प्रक्रियाओं के लिए प्रभावी। कर्नेल अनमाउंट KernelSU द्वारा नियंत्रित कर्नेल-स्तरीय अनमाउंट व्यवहार
================================================ FILE: manager/app/src/main/res/values-hr/strings.xml ================================================ Prikažite sistemske aplikacije Pošaljite izvještaje Sigurnosni mod Ponovno pokrenite da bi proradilo Nije uspjelo ažuriranje SELinux pravila za %s Početna Nije instalirano Verzija: %d Kliknite da instalirate Radi Nepodržano KernelSU sada samo podržava GKI kernele. Verzija kernela Verzija voditelja Otisak prsta Isključeno U Provođenju Permisivno SELinux stanje Nepoznato Superkorisnik Neuspješno uključivanje module: %s Neuspješno isključivanje module: %s Nema instaliranih modula Br. modula Deinstalirajte Instalirajte Instalirajte Ponovno pokrenite Postavke Lagano ponovno pokretanje Ponovno pokrenite u Oporavu Ponovno pokrenite u Pogonski Učitavalac Ponovno pokrenite u Preuzimanje Ponovo pokrenite u EDL O Jeste li sigurni da želite deinstalirati modulu %s\? %s deinstalirana Neuspješna deinstalacija: %s Verzija Autor Module su isključene jer je u sukobu sa Magisk-om! Naučite KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Naučite kako da instalirate KernelSU i da koristite module. Podržite Nas KernelSU je i uvijek će biti besplatan i otvorenog koda. Međutim, možete nam pokazati da vam je stalo donacijom. Pridružite se našem %2$s kanalu]]> Zadano Šablon Prilagođeno Naziv profila Naslijeđen Imenski prostor nosača Ažuriranje Profila Aplikacije za %s nije uspjelo Globalan Pojedinačan Umount module Grupe Sposobnosti SELinux kontekst Trenutna verzija KernelSU-a %1$d je preniska da bi upravitelj ispravno radio. Molimo vas da nadogradite na verziju %2$d ili noviju! Umontiraj module prema zadanim postavkama Globalna zadana vrijednost za \"Umontiraj module\" u profilu aplikacije. Ako je omogućeno, uklonit će sve modifikacije modula sustava za aplikacije koje nemaju postavljen profil. Domena Omogućavanje ove opcije omogućit će KernelSU-u da vrati sve izmijenjene datoteke od strane modula za ovu aplikaciju. Pravila Ažuriranje Preuzimanje module: %s Započnite sa preuzimanjem: %s Nova verzija %s je dostupna, kliknite za nadogradnju! Pokrenite Prisilno zaustavi Resetujte Spremi zapise Sljedeći moduli će biti instalirani: %1$s Prvo radnja Prvo omogućeno Potvrdi Predložak profila aplikacije Upravljanje lokalnim i online predloškom profila aplikacije. Izradi predložak Uredi predložak ID Nevažeći ID predloška Naziv Opis Spremi Izbriši Prikaži predložak ID predloška već postoji! Uvoz/Izvoz Uvezi iz međuspremnika Izvezi u međuspremnik Nije moguće pronaći lokalni predložak za izvoz! Uspješno uvezeno Spremanje predloška nije uspjelo Međuspremnik je prazan! Provjeri za ažuriranja Automatski provjeri za ažuriranja prilikom otvaranja aplikacije. Dodjeljivanje root pristupa nije uspjelo! Radnja Otvori WebView otklanjanje pogrešaka Može se koristiti za otklanjanje pogrešaka u WebUI-ju. Omogućite samo kada je potrebno. Izravna instalacija (preporučeno) Odaberite datoteku Instaliraj u neaktivni utor (nakon OTA) Vaš će uređaj biti **PRISILNO** pokrenuti se u trenutno neaktivni utor nakon ponovnog pokretanja!\nKoristite ovu opciju tek nakon što je OTA završen.\nNastaviti? Dalje Koristi lokalnu LKM datoteku Podržane su samo .ko datoteke Preporučuje se particija %1$s Odaberi KMI Deinstaliraj Privremeno deinstaliraj Trajno deinstaliraj Vrati stock image Privremeno deinstaliraj KernelSU, vrati u izvorno stanje nakon sljedećeg ponovnog pokretanja. Potpuno i trajno deinstaliranje KernelSU-a (root i svi moduli). Vrati stock factory image (ako postoji sigurnosna kopija), obično se koristi prije OTA-e; ako trebate deinstalirati KernelSU, koristite \"Trajno deinstaliraj\". Flešanje Uspješno flešano Neuspješno flešanje Odabrani LKM: %s Zapisi spremljeni Obrada… Povuci dolje za osvježiti Otpusti za osvježiti Osvježavanje… Uspješno osvježeno Nije moguće odobriti Superuser pristup za %s Zapis promjena Jeste li sigurni da želite deinstalirati modul %s? Ova radnja će utjecati na sve module, a određene značajke koje pruža metamodul (poput montiranja) više neće raditi. Utječe na sljedeće aplikacije Provjerite ažuriranja modula Odaberite particiju Kernel ne podržava ovu značajku. Ovom značajkom upravlja modul. Preusmjeri su binarnu datoteku Omogućava aplikacijama kojima je dodijeljeno dopuštenje Superuser u Profilu aplikacije da dobiju superuser shell izvršavanjem /system/bin/su; učinkovito samo za nove procese. Kernel umount Ponašanje umount na razini kernela koje kontrolira KernelSU Poništi Deinstalacija %s uspješno otkazana Poništavanje deinstalacije nije uspjelo: %s Sadrži %d aplikacija Tema Odaberite temu aplikacije. Prati sustav Svijetla Tamna Boja ključa Zadano Plava Crvena Zelena Ljubičasta Narančasta Tirkizna Ružičasta Smeđa Repozitoriji Naziv (A → Z) Izvorni kod Instalacija modula je onemogućena u sigurnom načinu rada Počevši od verzije 3.0.0, GKI način rada koristit će se samo u testnim okruženjima. Ne preporučujemo ga za svakodnevnu upotrebu, a image datoteke više neće biti dostupne. Nije povezano s mrežom Pokušaj ponovno PROČITAJ ME Izdanja Info ================================================ FILE: manager/app/src/main/res/values-hu/strings.xml ================================================ Működik Verzió: %d A KernelSU jelenleg csak GKI kerneleket támogat Kernel Alkalmazás verziója Ujjlenyomat Letiltva Újraindítás letöltő módba Újraindítás EDL-be Névjegy Biztos benne hogy eltávolítja a következő modult: %s? Nem sikerült eltávolítani: %s Készítő Rendszeralkalmazások megjelenítése Biztonságos mód A modulok nem érhetők el a Magiskkel való ütközés miatt! Tudjon meg többet a KernelSU-ról Ismerje meg a KernelSU telepítését és a modulok használatát Támogasson minket Tekintse meg a forráskódot a %1$s-on
Csatlakozzon a %2$s csatornánkhoz
Alapértelmezett Sablon Egyedi Profil neve Névtér csatlakoztatása Örökölt https://kernelsu.org/guide/what-is-kernelsu.html Különálló Csoportok Jogosultságok SELinux kontextus Modulok leválasztása alapértelmezetten Ha engedélyezi ezt az opciót, a KernelSU visszaállíthatja az alkalmazás moduljai által módosított fájlokat. Tartomány Szabályok Frissítés Modul letöltése: %s Letöltés indítása: %s Indítás Kényszerített leállítás újraindítás Kezdőlap Nincs telepítve Kattintson a telepítéshez Nem támogatott SELinux állapot Kényszerített Engedélyezett Ismeretlen Superuser Nem sikerült engedélyezni a következő modult: %s Nem sikerült letiltani a következő modult: %s Nincs telepített modul Modulok Eltávolítás Telepítés Telepítés Újraindítás Beállítások Rendszerfelület újraindítása Újraindítás recovery-módba Újraindítás bootloader-módba %s eltávolítva Verzió Naplók küldése Indítsa újra a készüléket a változások érvényesítéséhez A KernelSU ingyenes, nyílt forráskódú és mindig is az lesz. Ön azonban adományozással megmutathatja, hogy törődik a projekttel. Globális Modulok leválasztása Nem sikerült frissíteni az App Profilt ehhez: %s A \"Modulok leválasztása\" globális alapértelmezett értéke az App Profile-ban. Ha engedélyezve van, eltávolít minden modulmódosítást a rendszerből azon alkalmazások esetében, amelyeknek nincs profilja beállítva. Elérhető az új, %s verzió, kattintson a frissítéshez. Nem sikerült frissíteni az SELinux szabályokat a következőhöz: %s A jelenlegi KernelSU verzió %1$d túlságosan elavult a megfelelő működéshez. Kérjük frissítsen a %2$d verzióra vagy újabbra! Sikeresen importálva Exportálás a vágólapról Nem található helyi sablon az exportáláshoz! A sablon ID már létezik! Változások Importálás a vágólapról Név Hibás sablon ID Sablon készítése Import/Export A sablon mentése sikertelen Sablon szerkesztése ID App Profile sablon Leírás Mentés Az App Profile helyi és online sablonjának kezelése Törlés A vágólap üres! Sablon megtekintése Naplók mentése A WebUI hibakeresésére használható, csak szükség esetén engedélyezze. WebView hibakeresés engedélyezése Megnyitás Végleges eltávolítás %1$s partíció képfájl ajánlott KMI kiválasztása Következő Ideiglenes eltávolítás A KernelSU ideiglenes eltávolítása, az eredeti állapot visszaállítása a következő újraindítás után. Eltávolítás Telepítés Sikeres telepítés Kiválasztott LKM: %s Sikertelen telepítés A root jog megadása sikertelen! Telepítés inaktív helyre (OTA után) Fájl kiválasztása A KernelSU eltávolítása (root és az összes modul) teljesen és véglegesen. Eredeti képfájl visszaállítása Művelet Közvetlen telepítés (Ajánlott) Az eszköze **KÉNYSZERÍTETTEN** a jelenleg inaktív helyről fog indulni újraindítás után!\nCsak az OTA befejezése után használja.\nFolytatja? Állítsa vissza a gyári képfájlt (ha létezik biztonsági mentés). Általában OTA előtt használják. Ha a KernelSU-t szeretné eltávolítani, használja a végleges eltávolítás opciót. Frissítés ellenőrzése Automatikusan keressen frissítéseket az alkalmazás megnyitásakor Mentett naplók su bináris átirányítása Lehetővé teszi az App Profile-ban Superuser engedéllyel rendelkező alkalmazások számára, hogy superuser shell-t kapjanak a /system/bin/su végrehajtásával; csak új folyamatoknál hatékony. Kernel leválasztása KernelSU által vezérelt kernel szintű leválasztási viselkedés
================================================ FILE: manager/app/src/main/res/values-in/strings.xml ================================================ Beranda Tidak terpasang Ketuk untuk memasang Berfungsi Versi: %d Tidak didukung KernelSU sekarang hanya mendukung kernel GKI, tetapi Anda dapat menambal image untuk perangkat GKI. Versi kernel Versi manajer Identitas Status SELinux Dinonaktifkan Enforcing Permisif Tidak diketahui Superuser Gagal mengaktifkan modul: %s Gagal menonaktifkan modul: %s Tidak ada modul yang terpasang Modul Hapus Pasang Pasang Reboot Setelan Reboot secara halus Reboot ke Recovery Reboot ke Bootloader Reboot ke Download Reboot ke EDL Tentang Yakin ingin menghapus modul %s? %s berhasil dihapus Gagal menghapus: %s Versi Oleh Tampilkan apl sistem Kirim Log Mode aman Reboot agar berfungsi Modul tidak tersedia karena konflik dengan Magisk! Pelajari KernelSU https://kernelsu.org/id_ID/guide/what-is-kernelsu.html Pelajari cara memasang KernelSU dan menggunakan modul. Dukung Kami KernelSU akan selalu menjadi aplikasi gratis dan terbuka. Anda dapat memberikan donasi sebagai bentuk dukungan. Gabung saluran %2$s kami]]> Profil Apl Default Templat Khusus Nama profil Mount namespace Diwariskan Universal Individual Kelompok Kemampuan Konteks SELinux Umount modul Gagal memperbarui Profil Apl untuk %s Umount modul secara default Nilai default universal untuk \"Umount modul\" pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil. Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini. Domain Aturan Perbarui Mengunduh modul: %s Mulai mengunduh: %s Versi baru %s tersedia, klik untuk memperbarui! Luncurkan Paksa berhenti Mulai ulang apl Gagal memperbarui aturan SELinux untuk %s Versi KernelSU saat ini %1$d terlalu rendah untuk manajer berfungsi normal. Harap memperbarui ke versi %2$d atau di atasnya! Catatan perubahan Berhasil diimpor Ekspor ke papan klip Tidak ditemukan templat lokal untuk diekspor! ID templat sudah ada! Impor dari papan klip Nama ID templat tidak valid Buat templat Impor/Ekspor Gagal menyimpan templat Edit templat ID Templat Profil Aplikasi Deskripsi Simpan Kelola templat lokal dan online dari Profil Aplikasi. Hapus Papan klip kosong! Lihat templat Debugging WebView Dapat digunakan untuk mendebug antarmuka web (WebUI). Harap aktifkan hanya saat diperlukan. %1$s partisi image disarankan Pilih KMI Selanjutnya Perangkatmu akan **DIPAKSA** untuk boot ke slot nonaktif saat ini setelah reboot! \nGunakan hanya setelah proses OTA selesai. \nLanjutkan? Langsung pasang (Disarankan) Pilih berkas Pasang ke slot nonaktif (setelah OTA) Gagal memberikan akses root! Buka Periksa pembaruan Memeriksa pembaruan secara otomatis saat membuka aplikasi. Menghapus KernelSU (root dan semua modul) secara menyeluruh dan permanen. Hapus sementara Pulihkan image bawaan Hapus Hapus KernelSU untuk sementara, memulihkan ke kondisi asal setelah reboot berikutnya. Hapus permanen Pulihkan image bawaan pabrik (jika dicadangkan), biasanya digunakan sebelum pembaruan OTA; jika Anda perlu menghapus KernelSU, silakan gunakan \"Hapus secara permanen\". Pemasangan berhasil LKM dipilih: %s Memasang Pemasangan gagal Simpan Log Aksi Log disimpan Diaktifkan dahulu Aksi lebih dulu Modul berikut akan dipasang: %1$s Oke Gagal memberikan akses SU untuk %s Periksa pembaruan modul Gunakan berkas LKM lokal Hanya berkas .ko yang didukung Perintah SU klasik Mengizinkan akses root melalui /system/bin/su, pada proses baru yang dibuat. Unmount modul (tingkat kernel) Unmount modul dari kernel pada Profil Aplikasi. Memproses… Tarik ke bawah untuk menyegarkan Lepas untuk menyegarkan Menyegarkan… Penyegaran berhasil Repositori Nama (A → Z) Sumber kode Yakin ingin menghapus modul %s? Tindakan ini akan memengaruhi semua modul, dan fitur tertentu yang disediakan oleh metamodul (seperti mounting) tidak akan berfungsi lagi. Mulai dari v3.0.0, fungsi mode GKI dibatasi untuk lingkungan pengujian. Kami tidak menyarankan untuk penggunaan harian, dan berkas image tidak akan lagi disediakan. Mempengaruhi apl berikut Pilih partisi Kernel tidak mendukung fitur ini Fitur ini dikelola oleh modul Batalkan Berhasil membatalkan penghapusan %s Gagal membatalkan penghapusan: %s Berisi %d aplikasi Tema Pilih mode tema aplikasi. Ikuti sistem Terang Gelap Warna utama Default Biru Merah Hijau Ungu Jingga Hijau kebiruan Merah muda Coklat Tidak terhubung ke jaringan Coba lagi Baca Aku Rilis Informasi Pemasangan modul dinonaktifkan pada mode aman Tindakan modul berhasil dieksekusi. Aktif (Default) Nonaktif hingga Reboot Selalu nonaktif Buat pintasan Nama pintasan Pilih ikon khusus Launcher tidak mendukung pintasan layar utama. Pintasan dibuat di layar utama. Pintasan diperbarui. Hapus pintasan Harap aktifkan izin \"Buat pintasan layar utama\" untuk aplikasi ini di pengaturan Xiaomi. Harap aktifkan izin \"Pintasan layar utama\" untuk aplikasi ini di pengaturan OPPO. Jika pembuatan pintasan gagal, harap aktifkan izin pintasan layar utama untuk aplikasi ini di pengaturan sistem. Modul %s tidak tersedia Modul %s dinonaktifkan, diperbarui, atau menunggu penghapusan Silakan pilih berkas image perangkat GKI yang ingin Anda tambal. Versi KMI perangkat ini: %s Ini perangkat KMI ================================================ FILE: manager/app/src/main/res/values-it/strings.xml ================================================ Home Non installato Clicca per installare In esecuzione Versione: %d Non supportato KernelSU ora supporta solo i kernel GKI Kernel Versione del manager Impronta della build di Android Stato di SELinux Disabilitato Enforcing Permissive Sconosciuto Accesso root Impossibile abilitare il modulo: %s Impossibile disabilitare il modulo: %s Nessun modulo installato Modulo Disinstalla Installa Installa Riavvia Impostazioni Riavvio rapido Riavvia in modalità Recovery Riavvia in modalità Bootloader Riavvia in modalità Download Riavvia in modalità EDL Informazioni Sei sicuro di voler disinstallare il modulo %s? %s disinstallato Impossibile disinstallare: %s Versione Autore Mostra app di sistema Invia log Modalità provvisoria Riavvia per applicare la modifica I moduli sono disabilitati perché in conflitto con Magisk! Scopri KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Scopri come installare KernelSU e utilizzare i moduli Supportaci KernelSU è, e sempre sarà, gratuito e open source. Puoi comunque mostrarci il tuo apprezzamento facendo una donazione. Unisciti al nostro canale %2$s]]> Nome profilo Spazio dei nomi del mount Globale Gruppi Ereditato Individuale Predefinito Personalizzato Modello Scollega moduli Contesto SELinux Aggiornamento App Profile per %s fallito Aggiorna Apri Capacità Scollega moduli da default Regole Sto scaricando il modulo: %s Inizia a scaricare:%s Nuova versione: %s disponibile, tocca per aggiornare Arresto forzato Riavvia Aggiornamento regole SELinux per %s fallito Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. Dominio Il valore predefinito per \"Scollega moduli\" in App Profile. Se attivato, rimuoverà tutte le modifiche al sistema da parte dei moduli per le applicazioni che non hanno un profilo impostato. La versione attualmente installata di KernelSU (%1$d) è troppo vecchia ed il gestore non può funzionare correttamente. Si prega di aggiornare alla versione %2$d o successiva! Registro aggiornamenti Crea modello Modifica modello identificatore Identificativo modello non valido Nome Visualizza modello L\'identificatore del modello è già in uso! Importa/Esporta Importa dagli appunti Esporta negli appunti Impossibile trovare un modello locale da esportare! Importato con successo Gli appunti sono vuoti! Impossibile ottenere l\'accesso root! Modelli App Profile Gestisci i modelli locali e remoti di App Profile Elimina Descrizione Salva Impossibile salvare il modello Apri Controlla aggiornamenti Controlla automaticamente la disponibilità di aggiornamenti all\'apertura dell\'applicazione Abilita il debug di WebView Può essere usato per svolgere il debug di WebUI, è consigliato attivarlo solo quando necessario. È consigliato usare immagine della partizione %1$s Scegli il KMI Avanti Installazione diretta (Raccomandata) Scegli un file Installa nello slot inattivo (dopo OTA) Il tuo dispositivo sarà **FORZATO** ad avviarsi nello slot inattivo dopo il riavvio! \nUsa questa opzione solo quando l\'applicazione dell\'aggiornamento OTA è terminata. \nProcedere? Disinstalla Disinstalla temporaneamente Disinstalla permanentemente Ripristina immagine originale del produttore Disinstalla temporaneamente KernelSU, ripristina lo stato originale dopo il prossimo riavvio. Disinstalla KernelSU (root e tutti i moduli) completamente e permanentemente. Installazione Installazione completata Installazione fallita LKM selezionato: %s Ripristina l\'immagine di fabbrica del produttore (se il backup è presente), solitamente usato prima di applicare l\'OTA; se devi disinstallare KernelSU, utilizza invece \"Disinstalla permanentemente\". Salva Registri Reindirizza binario su Consente alle app con autorizzazione Superuser nel profilo app di ottenere una shell superuser eseguendo /system/bin/su; efficace solo per i nuovi processi. Umount del kernel Comportamento di umount a livello di kernel controllato da KernelSU ================================================ FILE: manager/app/src/main/res/values-iw/strings.xml ================================================ הפעל מחדש כדי להכניס לתוקף למד כיצד להתקין את KernelSU ולהשתמש במודולים לא ידוע הצג אפליקציות מערכת %s הוסר הסרת טעינת מודולים שלח לוג מושבת תמכו בנו ירושה מודולים מושבתים מכיוון שהם מתנגשים עם זה של Magisk! יומן שינויים התרים הפעלה מחדש למצב הורדה טעינת מודולים כברירת מחדל הפעלת אפשרות זו תאפשר ל-KernelSU לשחזר קבצים שהשתנו על ידי המודולים עבור יישום זה. אישי הפעלת המודל נכשלה: %s עצירה בכח הפעלה מחדש למצב EDL איתחול יכולת מפעיל מודל: %s גלובלי ערך ברירת המחדל הגלובלי עבור \"טעינת מודולים\" בפרופילי אפליקציה. אם מופעל, זה יסיר את כל שינויי המודול למערכת עבור יישומים שאין להם ערכת פרופיל. אכיפה הקשר SELinux טביעת אצבע ברירת מחדל להשיק מצב בטוח גרסת KernelSU הנוכחית %1$d נמוכה מדי כדי שהמנהל יפעל כראוי. אנא שדרג לגרסה %2$d ומעלה! הפעלה מחדש לריקברי רך Reboot שם פרופיל KernelSU הוא, ותמיד יהיה, חינמי וקוד פתוח. עם זאת, תוכל להראות לנו שאכפת לך על ידי תרומה. הסרה טעינת מרחב שמות התקנה לחץ להתקנה כללים קבוצה מודולים יוצר אודות גרסה: %d הפעלה מחדש KernelSU תומך רק בליבת GKI כעת סטטוס SELinux גרסה אינו נתמך תחום בית מותאם אישית תבנית מוריד מודל: %s עדכון למד אודות KernelSU האם אתה בטוח שברצונך להסיר את התקנת המודל %s\? הסרת התקנת %s נכשלה: משתמש על הגדרות עובד השבתת מודל %s נכשלה: אין מודלים מותקנים להתקין Kernel לא מותקן נכשל עדכון פרופיל האפליקציה עבור %s https://kernelsu.org/guide/what-is-kernelsu.html נכשל עדכון כללי SELinux עבור: %s הפעלה מחדש לבוטלאודר ראה את קוד המקור ב%1$s
הצטרף אלינו %2$s בערוץ
גרסת מנהל גרסה חדשה עבור: %s זמינה, לחץ כדי לשדרג שמור יומנים ניתוב מחדש של su binary הפנה מחדש את /system/bin/su ל-ksud עבור אפליקציות שקיבלו הרשאת Superuser בפרופיל האפליקציה; יעיל לתהליכים חדשים בלבד. ביטול טעינת קרנל התנהגות ביטול טעינה ברמת הקרנל הנשלטת על ידי KernelSU
================================================ FILE: manager/app/src/main/res/values-ja/strings.xml ================================================ ホーム 未インストール タップでインストール 動作中 バージョン: %d 非対応 KernelSU は現在 GKI カーネルのみをサポートしています。ただし、GKI デバイス向けにイメージにパッチを適用することは可能です。 カーネル アプリのバージョン Fingerprint SELinux の状態 Disabled Enforcing Permissive 不明 Superuser モジュールの有効化に失敗しました: %s モジュールの無効化に失敗しました: %s モジュールがインストールされていません モジュール アンインストール インストール インストール 再起動 設定 通常の再起動 リカバリーへ再起動 ブートローダー へ再起動 ダウンロードモードへ再起動 EDL へ再起動 アプリについて モジュール %s をアンインストールしますか? %s はアンインストールされました アンインストールに失敗しました: %s バージョン 制作者 システムアプリを表示 ログを送信 セーフモード 再起動すると有効化されます モジュールが Magisk との競合により利用できません! KernelSU について https://kernelsu.org/ja_JP/guide/what-is-kernelsu.html KernelSU のインストール方法やモジュールの使い方はこちら 支援する KernelSU はこれからもずっと無料でオープンソースです。寄付をして頂くことで、開発を支援していただけます。 %2$s チャンネルに参加]]> アプリのプロファイル 既定 テンプレート カスタム プロファイル名 名前空間のマウント 継承 共通 分離 モジュールのアンマウント グループ SELinux コンテキスト %s のアプリのプロファイルの更新をできませでした ドメイン ルール 新しいバージョン %s が利用可能です。タップしてダウンロード! アップデート ダウンロードを開始: %s 起動 強制停止 再起動 SELinux ルールの更新に失敗しました %s ケーパビリティ モジュールをダウンロード中: %s このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 デフォルトでモジュールをアンマウントする アプリプロファイルの「モジュールのアンマウント」の共通のデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 現在の KernelSU バージョン %1$d はマネージャーが適切に機能するには低すぎます。 バージョン %2$d 以降にアップグレードしてください! 変更履歴 インポート成功 クリップボードからエクスポート エクスポートするローカル テンプレートが見つかりません! テンプレート ID はすでに存在します! クリップボードからインポート 名前 無効なテンプレート ID テンプレートの作成 インポート/エクスポート テンプレートの保存に失敗しました テンプレートの編集 ID アプリプロファイルのテンプレート 説明 保存 アプリプロファイルのローカルおよびオンラインテンプレートを管理する。 消去 クリップボードが空です! テンプレートを表示 アップデートを確認 アプリを開いたときにアップデートを自動的に確認する。 root の付与に失敗しました! 開く WebView デバッグ WebUI のデバッグに使用できます。必要な場合にのみ有効にしてください。 %1$s パーティション イメージが推奨されます KMI を選択してください 次へ 非アクティブなスロットにインストール (OTA 後) 再起動後、デバイスは**強制的に**、現在非アクティブなスロットから起動します。\nこのオプションは、OTA が完了した後にのみ使用してください。\n続行しますか? 直接インストール (推奨) ファイルを選択してください 完全にアンインストールする ストックイメージを復元 一時的にアンインストールする アンインストール KernelSU を一時的にアンインストールし、次回の再起動後に元の状態に戻します。 KernelSU (ルートおよびすべてのモジュール) を完全かつ永久にアンインストールします。 バックアップが存在する場合、工場出荷時のイメージを復元できます (OTA の前に使用してください)。KernelSU をアンインストールする必要がある場合は、「完全にアンインストールする」を使用してください。 フラッシュ フラッシュ成功 フラッシュ失敗 選択された LKM: %s ログを保存 アクション 保存されたログ 並べ替え(最初に有効) 並べ替え(アクション優先) %s に superuser アクセスを許可できませんでした 次のモジュールがインストールされます: %1$s 確認 モジュールの更新を確認する ローカル LKM ファイルを使用する .ko ファイルのみサポートされています 従来の SU コマンド 新規プロセスで /system/bin/su 経由での root アクセスを許可します。 モジュールのアンマウント (カーネルレベル) アプリプロファイルに基づいて、カーネルからモジュールをアンマウントします。 処理中… 下に引いて更新 リリースして更新 更新中… 更新に成功しました リポジトリ 名前(A→Z) ソースコード モジュール %s をアンインストールしてもよろしいですか? この操作はすべてのモジュールに影響し、メタモジュールによって提供される特定の機能 (マウントなど) は動作しなくなります。 バージョン 3.0.0 以降、GKI ワークモードはテスト環境でのみ使用されます。日常的な使用には推奨されません。また、イメージファイルの提供も停止されます。 以下のアプリに影響します パーティションを選択 カーネルはこの機能をサポートしていません セーフモードではモジュールのインストールが無効になっています この機能はモジュールによって管理されます 元に戻す %s のアンインストールを正常にキャンセルしました アンインストールを元に戻すことができませんでした: %s %d 個のアプリが含まれています テーマ アプリのテーマモードを選択します。 システムに従う ライト ダーク キーカラー デフォルト ブルー レッド グリーン パープル オレンジ ティール ピンク ブラウン ネットワークに接続されていません 再試行 README リリース 情報 モジュールのアクションが正常に実行されました。 有効 (デフォルト) 再起動するまで無効 常に無効 ショートカットを作成 ショートカット名 カスタムアイコンを選択 ランチャーはホーム画面ショートカットをサポートしていません。 ホーム画面にショートカットを作成しました。 ショートカットが更新されました。 ショートカットを削除 Xiaomi の設定で、このアプリに対して「ホーム画面ショートカット」権限を有効にしてください。 OPPO の設定で、このアプリに対して「ホーム画面ショートカット」権限を有効にしてください。 ショートカットの作成に失敗した場合は、システム設定でこのアプリに対してホーム画面ショートカットの権限を有効にしてください。 モジュール %s は存在しません モジュール %s は無効、更新中、または削除の待機状態にあります パッチを適用する GKI デバイスのイメージファイルを選択してください このデバイスの KMI バージョン: %s このデバイスの KMI ================================================ FILE: manager/app/src/main/res/values-km/strings.xml ================================================ ទំព័រដើម មិនទាន់បានដំឡើង ចុចដើម្បីដំឡើង កែផ្លូវ su binary បញ្ជូន /system/bin/su ទៅកាន់ ksud សម្រាប់កម្មវិធីដែលបានទទួលសិទ្ធិ Superuser នៅក្នុងទម្រង់កម្មវិធី; មានប្រសិទ្ធភាពសម្រាប់ដំណើរការថ្មីប៉ុណ្ណោះ។ Kernel umount ឥរិយាបថ umount កម្រិតខឺណែលគ្រប់គ្រងដោយ KernelSU ================================================ FILE: manager/app/src/main/res/values-kn/strings.xml ================================================ ಪರಿಣಾಮ ಬೀರಲು ರೀಬೂಟ್ ಮಾಡಿ KernelSU ಅನ್ನು ಹೇಗೆ ಸ್ಥಾಪಿಸಬೇಕು ಮತ್ತು ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ಬಳಸುವುದು ಹೇಗೆ ಎಂದು ತಿಳಿಯಿರಿ ತಿಳಿಯದ ಸಿಸ್ಟಮ್ ಅಪ್ಲಿಕೇಶನ್‌ಗಳನ್ನು ತೋರಿಸಿ %s ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲಾಗಿದೆ Umount ಮಾಡ್ಯೂಲ್‌ಗಳು ಲಾಗ್ ಕಳುಹಿಸಿ ನಮ್ಮನ್ನು ಬೆಂಬಲಿಸಿ ಪಿತ್ರಾರ್ಜಿತ ಮಾಡ್ಯೂಲ್‌ಗಳನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲಾಗಿದೆ ಏಕೆಂದರೆ ಇದು ಮ್ಯಾಜಿಸ್ಕ್‌ನೊಂದಿಗೆ ಸಂಘರ್ಷವಾಗಿದೆ! ಚೇಂಜ್ಲಾಗ್ Permissive ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. ವೈಯಕ್ತಿಕ ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s ಫೋರ್ಸ್ ಸ್ಟಾಪ್ EDL ಗೆ ರೀಬೂಟ್ ಸಾಮರ್ಥ್ಯಗಳು ಡೌನ್‌ಲೋಡ್ ಮಾಡುವುದನ್ನು ಪ್ರಾರಂಭಿಸಿ: %s ಜಾಗತಿಕ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. SELinux ಸಂದರ್ಭ ಡೀಫಾಲ್ಟ್ ಲಾಂಚ್ ಸುರಕ್ಷಿತ ಮೋಡ್ ಪ್ರಸ್ತುತ KernelSU ಆವೃತ್ತಿ %1$d ಮ್ಯಾನೇಜರ್ ಸರಿಯಾಗಿ ಕಾರ್ಯನಿರ್ವಹಿಸಲು ತುಂಬಾ ಕಡಿಮೆಯಾಗಿದೆ. ದಯವಿಟ್ಟು ಆವೃತ್ತಿ %2$d ಅಥವಾ ಹೆಚ್ಚಿನದಕ್ಕೆ ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಿ! ಸಾಫ್ಟ್ ರೀಬೂಟ್ ಪ್ರೊಫೈಲ್ ಹೆಸರು KernelSU ಉಚಿತ ಮತ್ತು ಮುಕ್ತ ಮೂಲವಾಗಿದೆ ಮತ್ತು ಯಾವಾಗಲೂ ಇರುತ್ತದೆ. ಆದಾಗ್ಯೂ ನೀವು ದೇಣಿಗೆ ನೀಡುವ ಮೂಲಕ ನೀವು ಕಾಳಜಿ ವಹಿಸುತ್ತೀರಿ ಎಂದು ನಮಗೆ ತೋರಿಸಬಹುದು. ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮೌಂಟ್ ನೇಮ್‌ಸ್ಪೇಸ್ ನಿಯಮಗಳು ಗುಂಪುಗಳು ಮಾಡ್ಯೂಲ್ ಲೇಖಕ ಬಗ್ಗೆ ವರ್ಷನ್: %d ರೀಬೂಟ್ KernelSU ಈಗ GKI ಕರ್ನಲ್‌ಗಳನ್ನು ಮಾತ್ರ ಬೆಂಬಲಿಸುತ್ತದೆ SELinux ಸ್ಥಿತಿ ವರ್ಷನ್ ಬೆಂಬಲಿತವಾಗಿಲ್ಲ ಡೊಮೇನ್ ಮನೆ ಕಸ್ಟಮ್ ಟೆಂಪ್ಲೇಟ್ ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಡೌನ್‌ಲೋಡ್ ಮಾಡಲಾಗುತ್ತಿದೆ: %s KernelSU ಕಲಿಯಿರಿ %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಅಸ್ಥಾಪಿಸಲು ನೀವು ಖಚಿತವಾಗಿ ಬಯಸುವಿರಾ\? ಅನ್‌ಇನ್‌ಸ್ಟಾಲ್ ಮಾಡಲು ವಿಫಲವಾಗಿದೆ: %s ಸೂಪರ್ಯೂಸರ್ ಕೆಲಸ ಮಾಡುತ್ತಿದೆ ಮಾಡ್ಯೂಲ್ ಅನ್ನು ನಿಷ್ಕ್ರಿಯಗೊಳಿಸಲು ವಿಫಲವಾಗಿದೆ: %s ಮಾಡ್ಯೂಲ್ ಅನ್ನು ಸ್ಥಾಪಿಸಲಾಗಿಲ್ಲ ಕರ್ನಲ್ %s ಗಾಗಿ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್ ಅನ್ನು ನವೀಕರಿಸಲು ವಿಫಲವಾಗಿದೆ https://kernelsu.org/guide/what-is-kernelsu.html %1$s ನಲ್ಲಿ ಮೂಲ ಕೋಡ್ ಅನ್ನು ವೀಕ್ಷಿಸಿ
ನಮ್ಮ %2$s ಚಾನಲ್‌ಗೆ ಸೇರಿ
ಮ್ಯಾನೇಜರ್ ವರ್ಷನ್ ಹೊಸ ಆವೃತ್ತಿ: %s ಲಭ್ಯವಿದೆ, ಅಪ್‌ಗ್ರೇಡ್ ಮಾಡಲು ಕ್ಲಿಕ್ ಮಾಡಿ ಲಾಗ್ಗಳನ್ನು ಉಳಿಸಿ su ಬೈನರಿ ಮರುಮಾರ್ಗಗೊಳಿಸಿ ಆಪ್ ಪ್ರೊಫೈಲ್‌ನಲ್ಲಿ ಸೂಪರ್‌ಯೂಸರ್ ಅನುಮತಿ ಪಡೆದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗೆ /system/bin/su ಅನ್ನು ksud ಗೆ ಮರು ನಿರ್ದೇಶಿಸಿ; ಹೊಸ ಪ್ರಕ್ರಿಯೆಗಳಿಗೆ ಮಾತ್ರ ಪರಿಣಾಮಕಾರಿ. ಕರ್ನಲ್ ಅನ್‌ಮೌಂಟ್ KernelSU ನಿಂದ ನಿಯಂತ್ರಿಸಲ್ಪಡುವ ಕರ್ನಲ್-ಮಟ್ಟದ ಅನ್‌ಮೌಂಟ್ ನಡವಳಿಕೆ
================================================ FILE: manager/app/src/main/res/values-ko/strings.xml ================================================ 설치되지 않음 이 곳을 눌러 설치하기 작동 중 버전: %d 지원되지 않음 KernelSU는 현재 GKI 커널만 지원합니다. 커널 버전 매니저 버전 핑거프린트 SELinux 상태 비활성화됨 강제 허용 알 수 없음 슈퍼유저 모듈 활성화 실패: %s 모듈 비활성화 실패: %s 설치된 모듈 없음 모듈 제거 설치 설치 다시 시작 설정 빠른 다시 시작 리커버리로 다시 시작 부트로더로 다시 시작 다운로드 모드로 다시 시작 EDL 모드로 다시 시작 정보 %s 모듈을 삭제할까요? %s 모듈 삭제됨 모듈 삭제 실패: %s 버전 제작자 시스템 앱 표시 로그 보내기 안전 모드 다시 시작하여 변경 사항 적용 Magisk와 충돌로 모듈을 사용할 수 없습니다! KernelSU 알아보기 KernelSU 설치 및 모듈 사용 방법을 확인합니다. 지원하기 KernelSU는 현재와 미래에도 항상 무료이며 오픈 소스입니다. 기부를 통해 여러분의 관심과 지원을 표현해 주세요. %2$s 채널 참가하기]]> https://kernelsu.org/guide/what-is-kernelsu.html 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 지정합니다. 활성화하면 개별 프로필이 설정되지 않은 앱에는 시스템에 대한 모듈의 모든 수정 사항이 제외됩니다. 다시 시작 규칙 새 %s 버전을 사용할 수 있습니다, 여기를 눌러 업그레이드하세요! 다운로드 시작: %s 강제 중지 기본값 사용자 지정 템플릿 프로필 이름 마운트 대상 네임스페이스 상속 전역 개별 사용자 그룹 모듈 마운트 해제를 기본값으로 SELinux 컨텍스트 권한 %s에 대한 앱 프로필 업데이트 실패 모듈 마운트 해제를 기본값으로 이 옵션이 활성화되면 KernelSU는 이 앱에 대한 모듈의 모든 수정 사항을 복구합니다. 업데이트 모듈 다운로드 중: %s 도메인 실행 %s 앱에 대한 SELinux 규칙 업데이트 실패 로그 저장 업데이트 내역 WebUI 디버깅에 사용 가능, 필요한 경우에만 활성화하세요. 플래싱 중 선택된 LKM: %s %1$s 파티션 이미지가 권장됩니다 KMI 선택 다음 완전하고 영구적으로 KernelSU(루트 권한 및 모든 모듈)를 제거합니다. WebView 디버깅 현재 KernelSU %1$d 버전은 매니저가 정상 작동하기에 너무 낮습니다. %2$d 버전 이상으로 업그레이드하세요! 액션 임시 제거 열기 재부팅 후 기기가 **강제로** 비활성 슬롯으로 부팅합니다!\nOTA가 완료된 후에만 이 옵션을 사용하세요.\n계속할까요? 플래싱 성공 플래싱 실패 제거 영구 제거 임시적으로 KernelSU를 제거하고, 다음 재부팅 이후 기존 상태로 복구합니다. 앱 프로필 템플릿 로컬 및 온라인의 앱 프로필 템플릿을 관리합니다. ID 잘못된 템플릿 ID 이름 설명 저장 삭제 템플릿 ID가 이미 존재합니다! 불러오기/내보내기 클립보드에서 불러오기 클립보드로 내보내기 불러오기 성공 템플릿 저장 실패 클립보드가 비어 있습니다! 루트 권한 부여 실패! 템플릿 생성 템플릿 편집 템플릿 보기 내보낼 로컬 템플릿을 찾을 수 없습니다! 파일 선택 직접 설치 (권장) 비활성 슬롯에 설치 (OTA 이후) 제조사 이미지로 복구 제조사 이미지로 복구 (백업 존재 시), OTA 이전에 사용합니다. KernelSU를 제거해야 하는 경우, \"영구 삭제\"를 사용하세요. 업데이트 확인 앱 실행 시 자동으로 업데이트를 확인합니다. 로그 저장됨 활성화 우선 액션 우선 다음 모듈이 설치됩니다: %1$s 확인 %s에 슈퍼유저 권한을 부여할 수 없습니다 모듈 업데이트 확인 로컬 LKM 파일 사용 .ko 파일만 지원됩니다 처리 중… 당겨서 새로 고침 놓아서 새로 고침 새로 고침 중… 새로 고침 성공 파티션 선택 리포지토리 이름 (A → Z) 소스 코드 정말 %s 모듈을 제거할까요? 이 동작은 모든 모듈에 영향을 미치며, 메타모듈에서 제공하는 일부 기능(마운트 등)은 더 이상 작동하지 않습니다. v3.0.0 부터, GKI 동작 모드는 테스트 환경에서만 사용됩니다. 일상적인 사용은 권장하지 않으며, 이미지 파일은 더 이상 제공되지 않습니다. 다음 앱에 영향 커널이 이 기능을 지원하지 않습니다. 이 기능은 모듈에 의해 관리됩니다. 되돌리기 %s 제거 취소 성공 제거 취소 실패: %s %d 앱을 포함 테마 앱 테마 모드를 선택합니다. 시스템 설정에 따름 라이트 다크 주 색상 기본값 파랑 빨강 초록 보라 주황 청록 분홍 갈색 네트워크에 연결되지 않음 재시도 README 릴리즈 정보 안전 모드에서 모듈 설치가 비활성화됨 기존 su 명령 /system/bin/su를 통한 루트 권한 획득 허용 (새 프로세스 전용) 모듈 언마운트 앱 프로필에 따라 커널에서 모듈을 언마운트합니다 ================================================ FILE: manager/app/src/main/res/values-lt/strings.xml ================================================ Pirštų atspaudas Išjungta Priverstinas Nežinomas Supernaudotojai Nepavyko įjungti modulio: %s Nepavyko išjungti modulio: %s Leistinas Nėra įdiegtų modulių Moduliai Perkrovimas neišjungus Perkrauti į atkūrimo rėžimą Perkrauti į įkrovos tvarkyklę Perkrauti į atsisiuntimo rėžimą Apie Nepavyko išdiegti: %s %s išdiegtas Versija Autorius Rodyti sistemos programas Siųsti žurnalą Paleisti iš naujo Saugus rėžimas Paleiskite iš naujo, kad įsigaliotų Moduliai yra išjungti, nes jie konfliktuoja su Magisk\'s! https://kernelsu.org/guide/what-is-kernelsu.html Sužinokite apie KernelSU Sužinokite, kaip įdiegti KernelSU ir naudoti modulius Peržiūrėkite šaltinio kodą %1$s
Prisijunkite prie mūsų %2$s kanalo
Numatytas Šablonas Pasirinktinis Profilio pavadinimas Prijungti vardų erdvę Paveldėtas Globalus Individualus Grupės Galimybės SELinux kontekstas Atjungti modulius Atjungti modulius pagal numatytuosius parametrus Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai. Domenas Taisyklės Atnaujinti Atsisiunčiamas modulis: %s Pradedamas atsisiuntimas: %s Nauja versija: %s pasiekiama, spustelėkite norėdami atsinaujinti Paleisti Priversti sustoti Perkrauti Nepavyko atnaujinti SELinux taisyklių: %s Namai Neįdiegta KernelSU dabar palaiko tik GKI branduolius Spustelėkite norėdami įdiegti Veikia Versija: %d Nepalaikoma Tvarkyklės versija Branduolys SELinux statusas Išdiegti Įdiegti Įdiegti Parametrai Perkrauti į EDL Ar tikrai norite išdiegti modulį %s\? Paremkite mus KernelSU yra ir visada bus nemokamas ir atvirojo kodo. Tačiau galite parodyti, kad jums rūpi, paaukodami mums. Nepavyko atnaujinti programos profilio %s Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. Keitimų žurnalas Ši KernelSU versija %1$d yra per žema, kad šis vadybininkas galėtų tinkamai funkcionuoti. Prašome atsinaujinti į versiją %2$d ar aukščiau! Saglabāt Žurnālus Nukreipti su dvejetainį failą Nukreipti /system/bin/su į ksud programoms, kurioms suteiktas Superuser leidimas programos profilyje; veiksminga tik naujiems procesams. Branduolio atjungimas Branduolio lygio atjungimo elgsena, valdoma KernelSU
================================================ FILE: manager/app/src/main/res/values-lv/strings.xml ================================================ Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. Neizdevās atjaunināt SELinux noteikumus: %s Pārvaldiet vietējo un tiešsaistes lietotņu profila veidni Nederīgs veidnes id veidnes id jau pastāv! Eksportēt starpliktuvē Importēt no starpliktuves Importēts veiksmīgi Sākums Nav uzstādīts Noklikšķiniet, lai uzstādītu Darbojas Versija: %d Neatbalstīts KernelSU pagaidām atbalsta tikai GKI kodolus Kodols Pārvaldnieka versija Pirkstu nospiedums SELinux statuss Piespiests Atspējots Nezināms Superlietotājs Neizdevās atspējot moduli: %s Nav uzstādīts neviens modulis Moduļi Noņemt Uzstādīšana Restartēt Iestatījumi Ātri restartēt Restartēt uz Sāknēšanas režīmu Restartēt uz Atkopšanas režīmu Restartēt uz Lejupielādes režīmu Restartēt uz EDL režīmu Par lietotni %s noņemts Neizdevās noņemt: %s Autors Rādīt sistēmas lietotnes Sūtīt žurnālus Restartējiet, lai stātos spēkā Uzzināt par KernelSU https://kernelsu.org/guide/what-is-kernelsu.html Uzzināt, kā instalēt KernelSU un izmantot moduļus Atbalsti mūs Skatiet avota kodu vietnē %1$s
Pievienojies mūsu %2$s kanālam
Noklusējums Veidne Pielāgots Profila vārds Mount nosaukumvieta Individuāls Iespējas SELinux konteksts Atvienot moduļus Neizdevās atjaunināt lietotnes profilu %s Pēc noklusējuma atvienot moduļus Globālā noklusējuma vērtība vienumam “Atvienot moduļus” lietotņu profilos. Ja tas ir iespējots, lietojumprogrammām, kurām nav iestatīts profils, tiks noņemtas visas sistēmas moduļu modifikācijas. Domēns Noteikumi Atjaunināt Lejupielādē moduli: %s Sākt lejupielādi: %s Jaunā versija: %s ir pieejama, noklikšķiniet, lai atjauninātu Palaist Piespiedu apstāšana Restartēt aplikāciju Izmaiņu žurnāls Lietotnes profila veidne Izveidot veidni Rediģēt veidni id Vārds Apraksts Saglabāt Dzēst Skatīt veidni Importēt/Eksportēt Nevar atrast vietējo eksportējamo veidni! Neizdevās saglabāt veidni Starpliktuve ir tukša! Visatļautība Neizdevās iespējot moduli: %s Uzstādīt Vai tiešām vēlaties noņemt %s moduli? Versija Drošais režīms Moduļi nav pieejami dēļ konflikta ar Magisk! KernelSU ir un vienmēr būs bezmaksas un atvērtā koda. Tomēr jūs varat parādīt mums, ka jums rūp, veicot ziedojumu. Grupas Globāli Pašreizējā KernelSU versija %1$d ir pārāk zema, lai pārvaldnieks darbotos pareizi. Lūdzu, atjauniniet uz versiju %2$d vai jaunāku! Iespējot WebView atkļūdošanu Ieteicams %1$s nodalījuma attēls Nākamais Mantots Izvēlieties failu Instalēt neaktīvajā slotā (pēc OTA) Pēc restartēšanas jūsu ierīce tiks **PIESPIESTI** palaista pašreizējā neaktīvajā slotā! \nIzmantojiet šo opciju tikai pēc OTA pabeigšanas \nTurpināt? Tiešā instalēšana (Ieteicams) Atinstalēt Pagaidu atinstalēšana Atjaunot oriģinālo attēlu Īslaicīgi atinstalēt KernelSU, pēc nākamās restartēšanas atjaunot sākotnējo stāvokli. KernelSU (saknes un visu moduļu) pilnīga atinstalēšana. Atjaunojot rūpnīcas attēlu (ja ir dublējums), ko parasti izmanto pirms OTA; ja nepieciešams atinstalēt KernelSU, lūdzu, izmantojiet \"Neatgriezeniski atinstalēt\". Izvēlētais lkm: %s Neizdevās piešķirt sakni! Atvērt Pārbaudīt atjauninājumus Automātiski pārbaudīt atjauninājumus atverot aplikāciju Var izmantot WebUI atkļūdošanai, lūdzu, izmantot tikai tad, kad tas ir nepieciešams. Izvēlieties KMI Neatgriezeniski atinstalēt Instalē Instalēts veiksmīgi Instalēšana neizdevās Išsaugoti Žurnalus Kārtot (Iespējotie augšgalā) Apstiprināt Tiks uzstādīti šādi moduļi : %1$s Pārvirzīt su bināro failu Pārvirzīt /system/bin/su uz ksud lietotnēm, kurām piešķirta Superuser atļauja lietotnes profilā; efektīva tikai jauniem procesiem. Kodola atvienošana Kodola līmeņa atvienošanas uzvedība, ko kontrolē KernelSU
================================================ FILE: manager/app/src/main/res/values-mr/strings.xml ================================================ इंस्टॉल केले नाही होम इंस्टॉल साठी क्लिक करा कार्यरत आवृत्ती: %d असमर्थित KernelSU आता फक्त GKI कर्नलचे समर्थन करते कर्नल फिंगरप्रिंट व्यवस्थापक आवृत्ती SELinux स्थिती अक्षम एनफोर्सिंग परमिसिव अज्ञात स्थापित करा कोणतेही मॉड्यूल स्थापित केलेले नाही रीबूट करा सुपरयुझर मॉड्यूल सक्षम करण्यात अयशस्वी: %s विस्थापित करा मॉड्यूल अक्षम करण्यात अयशस्वी: %s मॉड्यूल स्थापित करा सेटिंग्ज सॉफ्ट रीबूट बद्दल EDL वर रीबूट करा तुमची खात्री आहे की तुम्ही मॉड्यूल %s विस्थापित करू इच्छिता\? विस्थापित करण्यात अयशस्वी: %s सिस्टम अॅप्स दाखवा बूटलोडरवर रीबूट करा %s विस्थापित आवृत्ती लेखक रिकवरी मध्ये रिबुट करा डाउनलोड करण्यासाठी रीबूट करा लॉग पाठवा सुरक्षित मोड प्रभावी होण्यासाठी रीबूट करा KernelSU शिका https://kernelsu.org/guide/what-is-kernelsu.html मॉड्यूल अक्षम केले आहेत कारण ते Magisk च्या विरोधाभास आहे! KernelSU कसे स्थापित करायचे आणि मॉड्यूल कसे वापरायचे ते शिका KernelSU विनामूल्य आणि मुक्त स्रोत आहे, आणि नेहमीच असेल. तथापि, देणगी देऊन तुम्ही आम्हाला दाखवू शकता की तुमची काळजी आहे. आम्हाला पाठिंबा द्या कस्टम माउंट नेमस्पेस डीफॉल्ट साचा वैयक्तिक क्षमता %1$s वर स्रोत कोड पहा
आमच्या %2$s चॅनेलमध्ये सामील व्हा
प्रोफाइल नाव इनहेरीटेड जागतिक गट SELinux संदर्भ उमाउंट मॉड्यूल्स %s साठी अॅप प्रोफाइल अपडेट करण्यात अयशस्वी डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा अॅप प्रोफाइलमधील \"उमाउंट मॉड्यूल्स\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल. हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल. यासाठी SELinux नियम अपडेट करण्यात अयशस्वी: %s नियम अपडेट करा डोमेन मॉड्यूल डाउनलोड करत आहे: %s डाउनलोड करणे सुरू करा: %s नवीन आवृत्ती: %s उपलब्ध आहे, डाउनलोड करण्यासाठी क्लिक करा सक्तीने थांबा लाँच करा पुन्हा सुरू करा लॉग जतन करा su बाइनरी पुन्हा मार्गस्थ करा ॲप प्रोफाईलमध्ये सुपरयुझर परवानगी दिलेल्या ॲप्ससाठी /system/bin/su ला ksud वर पुनर्निर्देशित करा; फक्त नवीन प्रक्रियांसाठी प्रभावी. कर्नल अनमाउंट KernelSU द्वारे नियंत्रित कर्नल-स्तरीय अनमाउंट वर्तन
================================================ FILE: manager/app/src/main/res/values-ms/strings.xml ================================================ Tidak Diketahui Lumpuhkan Permisif Reboot ke Download Modul tidak berjaya diaktifkan: %s Reboot ke EDL Enforcing Cap Jari Reboot ke Recovery Soft Reboot Padam Pasang Tekan untuk memasang Modul Tentang Versi: %d Reboot KernelSU ketika ini hanya menyokong kernel GKI Status SELinux Tidak Disokong Layar Utama Apakah anda pasti ingin membuang modul %s\? Superuser Tetapan Berjalan Gagal mematikan modul: %s Tiada modul dipasang Pasang Kernel Tidak terpasang Reboot ke Bootloader Versi manager Simpan Log Ubaḥ hala binari su Ubaḥ hala /system/bin/su ke ksud untuk aplikasi yang diberikan kebenaran Superuser dalam Profil Aplikasi; berkesan untuk proses baharu sahaja. Umount kernel Tingkah laku umount peringkat kernel dikawal oleh KernelSU ================================================ FILE: manager/app/src/main/res/values-my/strings.xml ================================================ su binary လမ်းကြောင်းပြောင်းမည် အက်ပ်ပရိုဖိုင်တွင် Superuser ခွင့်ပြုချက် ပေးထားသော အက်ပ်များအတွက် /system/bin/su ကို ksud သို့ လမ်းကြောင်းပြောင်းမည်; လုပ်ငန်းစဉ်အသစ်များအတွက်သာ ထိရောက်မှုရှိသည်။ Kernel ဖြုတ်မည် KernelSU မှ ထိန်းချုပ်ထားသော Kernel အဆင့် ဖြုတ်မည့် အပြုအမူ ================================================ FILE: manager/app/src/main/res/values-night/themes.xml ================================================