From 938198bf119d70d8e2cf2adfc8e69a01b489c4cd Mon Sep 17 00:00:00 2001 From: fr4nz Date: Thu, 20 Nov 2025 21:24:53 +0100 Subject: [PATCH] Updated to 4.0.0 --- LICENSE | 674 ------- README.md | 2 - SECURITY.md | 7 - crowdin.yml | 6 - docs/guide/tracepoint-hook.md | 33 +- docs/zh/guide/tracepoint-hook.md | 35 +- icon/logo.png | Bin 342 -> 0 bytes justfile | 14 - kernel/.clang-format | 12 +- kernel/.gitignore | 22 + kernel/.vscode/c_cpp_properties.json | 11 + kernel/.vscode/generate_compdb.py | 92 + kernel/.vscode/tasks.json | 16 + kernel/Kconfig | 73 +- kernel/Makefile | 94 +- kernel/allowlist.c | 831 ++++---- kernel/allowlist.h | 26 +- kernel/apk_sign.c | 581 +++--- kernel/app_profile.c | 302 +++ kernel/app_profile.h | 70 + kernel/arch.h | 8 +- kernel/core_hook.c | 1092 ----------- kernel/core_hook.h | 10 - kernel/dynamic_manager.c | 21 +- kernel/feature.c | 173 ++ kernel/feature.h | 36 + kernel/file_wrapper.c | 341 ++++ kernel/file_wrapper.h | 14 + kernel/include/ksu_hook.h | 28 - kernel/kernel_compat.c | 94 - kernel/kernel_compat.h | 68 +- kernel/kernel_umount.c | 193 ++ kernel/kernel_umount.h | 14 + kernel/kpm/compact.c | 2 +- kernel/kpm/kpm.c | 273 ++- kernel/kpm/kpm.h | 50 +- kernel/ksu.c | 107 +- kernel/ksu.h | 96 +- kernel/ksu_trace.c | 69 - kernel/ksu_trace.h | 37 - kernel/ksu_trace_export.c | 8 - kernel/ksud.c | 921 +++++---- kernel/ksud.h | 12 +- kernel/manager.h | 23 +- kernel/manager_sign.h | 6 +- kernel/manual_su.c | 356 ++++ kernel/manual_su.h | 49 + kernel/pkg_observer.c | 133 ++ kernel/seccomp_cache.c | 69 + kernel/seccomp_cache.h | 12 + kernel/selinux/Makefile | 10 +- kernel/selinux/rules.c | 802 ++++---- kernel/selinux/selinux.c | 190 +- kernel/selinux/selinux.h | 11 +- kernel/selinux/sepolicy.c | 1169 ++++++----- kernel/selinux/sepolicy.h | 22 +- kernel/setuid_hook.c | 171 ++ kernel/setuid_hook.h | 14 + kernel/sucompat.c | 387 ++-- kernel/sucompat.h | 18 + kernel/sulog.c | 340 ++++ kernel/sulog.h | 93 + kernel/supercalls.c | 935 +++++++++ kernel/supercalls.h | 180 ++ kernel/syscall_hook_manager.c | 374 ++++ kernel/syscall_hook_manager.h | 47 + kernel/throne_comm.c | 223 ++- kernel/throne_comm.h | 10 + kernel/throne_tracker.c | 987 +++++----- kernel/throne_tracker.h | 2 +- kernel/umount_manager.c | 278 +++ kernel/umount_manager.h | 66 + manager/app/build.gradle.kts | 11 +- manager/app/proguard-rules.pro | 4 +- manager/app/src/main/AndroidManifest.xml | 59 +- .../aidl/com/sukisu/zako/IKsuInterface.aidl | 10 + manager/app/src/main/assets/kpimg | Bin 179792 -> 179808 bytes manager/app/src/main/assets/kptools | Bin 306432 -> 310112 bytes manager/app/src/main/assets/ksu_susfs_1.5.7 | Bin 20072 -> 0 bytes manager/app/src/main/assets/ksu_susfs_1.5.8 | Bin 22184 -> 0 bytes manager/app/src/main/assets/ksu_susfs_1.5.9 | Bin 22128 -> 0 bytes manager/app/src/main/assets/ksu_susfs_2.0.0 | Bin 0 -> 21496 bytes manager/app/src/main/cpp/CMakeLists.txt | 7 +- manager/app/src/main/cpp/jni.c | 602 +++--- manager/app/src/main/cpp/ksu.c | 476 +++-- manager/app/src/main/cpp/ksu.h | 296 ++- manager/app/src/main/cpp/legacy.c | 163 ++ manager/app/src/main/cpp/prelude.h | 40 +- .../com/sukisu/ultra/KernelSUApplication.kt | 149 +- .../src/main/java/com/sukisu/ultra/Natives.kt | 85 +- .../java/com/sukisu/ultra/ui/KsuService.kt | 150 +- .../java/com/sukisu/ultra/ui/MainActivity.kt | 132 +- .../ultra/ui}/activity/component/BottomBar.kt | 17 +- .../ultra/ui}/activity/util/ThemeUtils.kt | 5 +- .../ui/activity/util/UltraActivityUtils.kt | 236 +++ .../com/sukisu/ultra/ui/component/Dialog.kt | 56 +- .../ultra/ui/component/ImageEditorDialog.kt | 223 --- .../ui/component/InstallConfirmationDialog.kt | 441 +++++ .../ultra/ui/component/KsuIsValidCheck.kt | 2 +- .../ultra/ui/component/SuperDropdown.kt | 250 +++ .../ui/component/VerticalExpandableFab.kt | 30 +- .../ui/component/profile/TemplateConfig.kt | 2 +- .../com/sukisu/ultra/ui/screen/AppProfile.kt | 18 +- .../java/com/sukisu/ultra/ui/screen/Flash.kt | 89 +- .../java/com/sukisu/ultra/ui/screen/Home.kt | 258 ++- .../com/sukisu/ultra/ui/screen/Install.kt | 301 ++- .../sukisu/ultra/ui/screen/LogViewerScreen.kt | 941 +++++++++ .../java/com/sukisu/ultra/ui/screen/Module.kt | 33 +- .../com/sukisu/ultra/ui/screen/Settings.kt | 466 +++-- .../com/sukisu/ultra/ui/screen/SuperUser.kt | 1004 +++++----- .../com/sukisu/ultra/ui/screen/Template.kt | 28 +- .../ultra/ui/screen/UmountManagerScreen.kt | 442 +++++ .../ultra/ui/{screen => susfs}/SuSFSConfig.kt | 269 ++- .../component/SuSFSConfigDialogs.kt | 6 +- .../{ => susfs}/component/SuSFSConfigTabs.kt | 135 +- .../ultra/ui/{ => susfs}/util/SuSFSManager.kt | 211 +- .../ui/{ => susfs}/util/SuSFSModuleScripts.kt | 17 +- .../com/sukisu/ultra/ui/theme/CardManage.kt | 239 ++- .../java/com/sukisu/ultra/ui/theme/Theme.kt | 787 ++++---- .../ui/theme/component/ImageEditorDialog.kt | 411 ++++ .../ui/{ => theme}/util/BackgroundUtils.kt | 2 +- .../java/com/sukisu/ultra/ui/util/KsuCli.kt | 363 ++-- .../ultra/ui/util/RestartActivityUtils.kt | 44 - .../ui/util/{ => module}/ModuleModify.kt | 41 +- .../ultra/ui/util/{ => module}/ModuleUtils.kt | 2 +- .../{ => module}/ModuleVerificationManager.kt | 3 +- .../ultra/ui/viewmodel/HomeViewModel.kt | 788 ++++---- .../ultra/ui/viewmodel/ModuleViewModel.kt | 2 +- .../ultra/ui/viewmodel/SuperUserViewModel.kt | 380 ++-- .../sukisu/ultra/ui/webui/AppIconUtil.java | 47 + .../sukisu/ultra/ui/webui/KsuLibSuProvider.kt | 20 +- .../sukisu/ultra/ui/webui/WebUIActivity.kt | 25 +- .../sukisu/ultra/ui/webui/WebUIXActivity.kt | 8 + .../sukisu/ultra/ui/webui/WebViewInterface.kt | 55 +- .../io/sukisu/ultra/UltraShellHelper.java | 29 - .../io/sukisu/ultra/UltraToolInstall.java | 23 - .../zakoui/activity/util/AnimatedBottomBar.kt | 20 - .../zako/zako/zakoui/activity/util/AppData.kt | 90 - .../zakoui/activity/util/DataRefreshUtils.kt | 46 - .../zako/zakoui/activity/util/DisplayUtils.kt | 24 - .../zako/zakoui/activity/util/LocaleUtils.kt | 48 - .../zako/zako/zakoui/screen/MoreSettings.kt | 1711 ----------------- .../screen/{ => kernelFlash}/KernelFlash.kt | 40 +- .../component/SlotSelectionDialog.kt | 2 +- .../kernelFlash/state/KernelFlashState.kt} | 9 +- .../screen/moreSettings/MoreSettings.kt | 726 +++++++ .../moreSettings/MoreSettingsHandlers.kt | 436 +++++ .../component/MoreSettingsComponents.kt | 201 ++ .../component/MoreSettingsDialogs.kt | 476 +++++ .../moreSettings/state/MoreSettingsState.kt | 104 + .../screen/moreSettings/util/LocaleHelper.kt | 154 ++ .../moreSettings/util/RestartActivityUtils.kt | 27 + manager/app/src/main/jniLibs/.gitignore | 13 +- .../main/jniLibs/arm64-v8a/libksu_susfs.so | Bin 0 -> 21496 bytes .../{libzakoboot.so => libmagiskboot.so} | Bin .../{libzakoboot.so => libmagiskboot.so} | Bin .../{libzakoboot.so => libmagiskboot.so} | Bin .../app/src/main/res/values-ar/strings.xml | 7 +- .../app/src/main/res/values-az/strings.xml | 7 +- .../app/src/main/res/values-bs/strings.xml | 7 +- .../app/src/main/res/values-da/strings.xml | 7 +- .../app/src/main/res/values-de/strings.xml | 7 +- .../app/src/main/res/values-es/strings.xml | 7 +- .../app/src/main/res/values-et/strings.xml | 7 +- .../app/src/main/res/values-fa/strings.xml | 7 +- .../app/src/main/res/values-fil/strings.xml | 7 +- .../app/src/main/res/values-fr/strings.xml | 7 +- .../app/src/main/res/values-hi/strings.xml | 7 +- .../app/src/main/res/values-hr/strings.xml | 7 +- .../app/src/main/res/values-hu/strings.xml | 7 +- .../app/src/main/res/values-idn/strings.xml | 541 ++++++ .../app/src/main/res/values-in/strings.xml | 103 +- .../app/src/main/res/values-it/strings.xml | 7 +- .../app/src/main/res/values-ja/strings.xml | 11 +- .../app/src/main/res/values-kn/strings.xml | 7 +- .../app/src/main/res/values-ko/strings.xml | 7 +- .../app/src/main/res/values-lt/strings.xml | 7 +- .../app/src/main/res/values-lv/strings.xml | 7 +- .../app/src/main/res/values-mr/strings.xml | 7 +- .../app/src/main/res/values-ms/strings.xml | 7 +- .../app/src/main/res/values-nl/strings.xml | 7 +- .../app/src/main/res/values-pl/strings.xml | 7 +- .../app/src/main/res/values-pt/strings.xml | 7 +- .../app/src/main/res/values-ro/strings.xml | 7 +- .../app/src/main/res/values-ru/strings.xml | 146 +- .../app/src/main/res/values-sl/strings.xml | 7 +- .../app/src/main/res/values-th/strings.xml | 7 +- .../app/src/main/res/values-tr/strings.xml | 124 +- .../app/src/main/res/values-uk/strings.xml | 14 +- .../app/src/main/res/values-vi/strings.xml | 155 +- .../src/main/res/values-zh-rCN/strings.xml | 129 +- .../src/main/res/values-zh-rHK/strings.xml | 20 +- .../src/main/res/values-zh-rTW/strings.xml | 76 +- manager/app/src/main/res/values/strings.xml | 121 +- manager/build.gradle.kts | 9 +- manager/gradle/libs.versions.toml | 25 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- manager/gradlew | 4 - manager/gradlew.bat | 4 +- manager/randomizer | 31 + scripts/ksubot.py | 15 +- userspace/kpmmgr/.gitignore | 2 - userspace/kpmmgr/jni/Android.mk | 6 - userspace/kpmmgr/jni/Application.mk | 3 - userspace/kpmmgr/jni/kpmmgr.c | 118 -- userspace/ksud/Cargo.lock | 121 +- userspace/ksud/Cargo.toml | 6 +- userspace/ksud/bin/aarch64/ksuinit | Bin 466288 -> 421792 bytes userspace/ksud/bin/x86_64/ksuinit | Bin 518312 -> 483808 bytes userspace/ksud/build.rs | 12 +- userspace/ksud/src/boot_patch.rs | 604 ++---- userspace/ksud/src/cli.rs | 257 ++- userspace/ksud/src/debug.rs | 48 +- userspace/ksud/src/feature.rs | 404 ++++ userspace/ksud/src/init_event.rs | 77 +- userspace/ksud/src/installer.sh | 39 + userspace/ksud/src/kpm.rs | 501 ++--- userspace/ksud/src/ksucalls.rs | 277 ++- userspace/ksud/src/magic_mount.rs | 93 +- userspace/ksud/src/main.rs | 5 +- userspace/ksud/src/module.rs | 145 +- userspace/ksud/src/profile.rs | 7 +- userspace/ksud/src/restorecon.rs | 18 +- userspace/ksud/src/sepolicy.rs | 24 +- userspace/ksud/src/su.rs | 52 +- userspace/ksud/src/uid_scanner.rs | 36 +- userspace/ksud/src/uid_scanner.sh | 5 + userspace/ksud/src/umount_manager.rs | 344 ++++ userspace/ksud/src/utils.rs | 53 +- userspace/su/jni/su.c | 57 +- userspace/susfs/.gitignore | 2 - userspace/susfs/jni/Android.mk | 6 - userspace/susfs/jni/Application.mk | 3 - userspace/susfs/jni/susfs.c | 137 -- 234 files changed, 21069 insertions(+), 12710 deletions(-) delete mode 100644 LICENSE delete mode 100644 README.md delete mode 100644 SECURITY.md delete mode 100644 crowdin.yml delete mode 100644 icon/logo.png delete mode 100644 justfile create mode 100644 kernel/.gitignore create mode 100644 kernel/.vscode/c_cpp_properties.json create mode 100644 kernel/.vscode/generate_compdb.py create mode 100644 kernel/.vscode/tasks.json create mode 100644 kernel/app_profile.c create mode 100644 kernel/app_profile.h delete mode 100644 kernel/core_hook.c delete mode 100644 kernel/core_hook.h create mode 100644 kernel/feature.c create mode 100644 kernel/feature.h create mode 100644 kernel/file_wrapper.c create mode 100644 kernel/file_wrapper.h delete mode 100644 kernel/include/ksu_hook.h delete mode 100644 kernel/kernel_compat.c create mode 100644 kernel/kernel_umount.c create mode 100644 kernel/kernel_umount.h delete mode 100644 kernel/ksu_trace.c delete mode 100644 kernel/ksu_trace.h delete mode 100644 kernel/ksu_trace_export.c create mode 100644 kernel/manual_su.c create mode 100644 kernel/manual_su.h create mode 100644 kernel/pkg_observer.c create mode 100644 kernel/seccomp_cache.c create mode 100644 kernel/seccomp_cache.h create mode 100644 kernel/setuid_hook.c create mode 100644 kernel/setuid_hook.h create mode 100644 kernel/sucompat.h create mode 100644 kernel/sulog.c create mode 100644 kernel/sulog.h create mode 100644 kernel/supercalls.c create mode 100644 kernel/supercalls.h create mode 100644 kernel/syscall_hook_manager.c create mode 100644 kernel/syscall_hook_manager.h create mode 100644 kernel/umount_manager.c create mode 100644 kernel/umount_manager.h create mode 100644 manager/app/src/main/aidl/com/sukisu/zako/IKsuInterface.aidl delete mode 100644 manager/app/src/main/assets/ksu_susfs_1.5.7 delete mode 100644 manager/app/src/main/assets/ksu_susfs_1.5.8 delete mode 100644 manager/app/src/main/assets/ksu_susfs_1.5.9 create mode 100644 manager/app/src/main/assets/ksu_susfs_2.0.0 create mode 100644 manager/app/src/main/cpp/legacy.c rename manager/app/src/main/java/{zako/zako/zako/zakoui => com/sukisu/ultra/ui}/activity/component/BottomBar.kt (94%) rename manager/app/src/main/java/{zako/zako/zako/zakoui => com/sukisu/ultra/ui}/activity/util/ThemeUtils.kt (95%) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt rename manager/app/src/main/java/com/sukisu/ultra/ui/{screen => susfs}/SuSFSConfig.kt (90%) rename manager/app/src/main/java/com/sukisu/ultra/ui/{ => susfs}/component/SuSFSConfigDialogs.kt (99%) rename manager/app/src/main/java/com/sukisu/ultra/ui/{ => susfs}/component/SuSFSConfigTabs.kt (87%) rename manager/app/src/main/java/com/sukisu/ultra/ui/{ => susfs}/util/SuSFSManager.kt (89%) rename manager/app/src/main/java/com/sukisu/ultra/ui/{ => susfs}/util/SuSFSModuleScripts.kt (97%) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt rename manager/app/src/main/java/com/sukisu/ultra/ui/{ => theme}/util/BackgroundUtils.kt (98%) delete mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt rename manager/app/src/main/java/com/sukisu/ultra/ui/util/{ => module}/ModuleModify.kt (92%) rename manager/app/src/main/java/com/sukisu/ultra/ui/util/{ => module}/ModuleUtils.kt (99%) rename manager/app/src/main/java/com/sukisu/ultra/ui/util/{ => module}/ModuleVerificationManager.kt (98%) create mode 100644 manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java delete mode 100644 manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java delete mode 100644 manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt delete mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt rename manager/app/src/main/java/zako/zako/zako/zakoui/screen/{ => kernelFlash}/KernelFlash.kt (91%) rename manager/app/src/main/java/{com/sukisu/ultra/ui => zako/zako/zako/zakoui/screen/kernelFlash}/component/SlotSelectionDialog.kt (99%) rename manager/app/src/main/java/zako/zako/zako/zakoui/{flash/KernelFlash.kt => screen/kernelFlash/state/KernelFlashState.kt} (98%) create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt create mode 100644 manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt create mode 100644 manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so rename manager/app/src/main/jniLibs/arm64-v8a/{libzakoboot.so => libmagiskboot.so} (100%) rename manager/app/src/main/jniLibs/armeabi-v7a/{libzakoboot.so => libmagiskboot.so} (100%) rename manager/app/src/main/jniLibs/x86_64/{libzakoboot.so => libmagiskboot.so} (100%) create mode 100644 manager/app/src/main/res/values-idn/strings.xml create mode 100644 manager/randomizer delete mode 100644 userspace/kpmmgr/.gitignore delete mode 100644 userspace/kpmmgr/jni/Android.mk delete mode 100644 userspace/kpmmgr/jni/Application.mk delete mode 100644 userspace/kpmmgr/jni/kpmmgr.c create mode 100644 userspace/ksud/src/feature.rs create mode 100644 userspace/ksud/src/uid_scanner.sh create mode 100644 userspace/ksud/src/umount_manager.rs delete mode 100644 userspace/susfs/.gitignore delete mode 100644 userspace/susfs/jni/Android.mk delete mode 100644 userspace/susfs/jni/Application.mk delete mode 100644 userspace/susfs/jni/susfs.c diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - 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 -. diff --git a/README.md b/README.md deleted file mode 100644 index e5a86f0..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# sukisu - diff --git a/SECURITY.md b/SECURITY.md deleted file mode 100644 index 83040d9..0000000 --- a/SECURITY.md +++ /dev/null @@ -1,7 +0,0 @@ -# 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. diff --git a/crowdin.yml b/crowdin.yml deleted file mode 100644 index ce0c41a..0000000 --- a/crowdin.yml +++ /dev/null @@ -1,6 +0,0 @@ -project_id_env: CROWDIN_PROJECT_ID -api_token_env: CROWDIN_API_TOKEN -preserve_hierarchy: 1 -files: - - source: /manager/app/src/main/res/values/strings.xml - translation: /manager/app/src/main/res/values-%two_letters_code%/strings.xml diff --git a/docs/guide/tracepoint-hook.md b/docs/guide/tracepoint-hook.md index 8333561..af5fa72 100644 --- a/docs/guide/tracepoint-hook.md +++ b/docs/guide/tracepoint-hook.md @@ -44,7 +44,7 @@ Generally need to modify the `do_execve` and `compat_do_execve` methods in `fs/e .ptr.compat = __envp, }; +#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */ ++ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0); // 32-bit su and 32-on-64 support +#endif return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); } @@ -237,34 +237,3 @@ Need to modify the `input_event` method in `drivers/input/input.c`, not `input_h spin_lock_irqsave(&dev->event_lock, flags); ``` - -### devpts Hook (`pty.c`) - -Need to modify the `pts_unix98_lookup` method in `drivers/tty/pty.c` - -```patch ---- a/drivers/tty/pty.c -+++ b/drivers/tty/pty.c -@@ -31,6 +31,10 @@ - #include - #include "tty.h" - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+#include <../../drivers/kernelsu/ksu_trace.h> -+#endif -+ - #undef TTY_DEBUG_HANGUP - #ifdef TTY_DEBUG_HANGUP - # define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args) -@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver, - { - struct tty_struct *tty; - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode); -+#endif -+ - mutex_lock(&devpts_mutex); - tty = devpts_get_priv(file->f_path.dentry); - mutex_unlock(&devpts_mutex); -``` diff --git a/docs/zh/guide/tracepoint-hook.md b/docs/zh/guide/tracepoint-hook.md index 3b98fbd..7dde784 100644 --- a/docs/zh/guide/tracepoint-hook.md +++ b/docs/zh/guide/tracepoint-hook.md @@ -44,7 +44,7 @@ .ptr.compat = __envp, }; +#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_execveat_sucompat_hook((int *)AT_FDCWD, &filename, NULL, NULL, NULL); /* 32-bit su */ ++ trace_ksu_trace_execveat_hook((int *)AT_FDCWD, &filename, &argv, &envp, 0)); // 32-bit su and 32-on-64 support +#endif return do_execveat_common(AT_FDCWD, filename, argv, envp, 0); } @@ -236,35 +236,4 @@ if (is_event_supported(type, dev->evbit, EV_MAX)) { spin_lock_irqsave(&dev->event_lock, flags); -``` - -### devpts 钩子 (`pty.c`) - -需要修改 `drivers/tty/pty.c` 的 `pts_unix98_lookup` 方法 - -```patch ---- a/drivers/tty/pty.c -+++ b/drivers/tty/pty.c -@@ -31,6 +31,10 @@ - #include - #include "tty.h" - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+#include <../../drivers/kernelsu/ksu_trace.h> -+#endif -+ - #undef TTY_DEBUG_HANGUP - #ifdef TTY_DEBUG_HANGUP - # define tty_debug_hangup(tty, f, args...) tty_debug(tty, f, ##args) -@@ -707,6 +711,10 @@ static struct tty_struct *pts_unix98_lookup(struct tty_driver *driver, - { - struct tty_struct *tty; - -+#if defined(CONFIG_KSU) && defined(CONFIG_KSU_TRACEPOINT_HOOK) -+ trace_ksu_trace_devpts_hook((struct inode *)file->f_path.dentry->d_inode); -+#endif -+ - mutex_lock(&devpts_mutex); - tty = devpts_get_priv(file->f_path.dentry); - mutex_unlock(&devpts_mutex); -``` +``` \ No newline at end of file diff --git a/icon/logo.png b/icon/logo.png deleted file mode 100644 index cddff920f64f874302f3ce534007042d7ca9908b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 342 zcmeAS@N?(olHy`uVBq!ia0vp^2SAvG8AvYpRA>UESkfJR9T^xl_H+M9WCijSl0AZa z85pY67#JE_7#My5g&JNkFq9fFFuY1&V6d9Oz#v{QXIG#NP(m@lC&X1*LR3{s+}X<9 z#o9tcR!U!0>Cf-q|NsBjS5f-$YcCo~c*Fes4PZ!6Kid%2* zEaW|uAkg+u$%BLIFpCOLfQLi_Ym{S9?8RlXmTZ*>b1>H84v9H8@qeXw#hKb~tGt3b zbN!}ATvyp3J3Yc5Xb^TVh2@Q%^BOzOH~0G^a)l;bvu4v@|68rGxWM|dHs_tD*Yl;Z p>jR2TVY$h2%Q~loCICgdW7z-z diff --git a/justfile b/justfile deleted file mode 100644 index 51bef76..0000000 --- a/justfile +++ /dev/null @@ -1,14 +0,0 @@ -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 diff --git a/kernel/.clang-format b/kernel/.clang-format index 10dc5a9..6453cf9 100644 --- a/kernel/.clang-format +++ b/kernel/.clang-format @@ -56,8 +56,8 @@ ColumnLimit: 80 CommentPragmas: '^ IWYU pragma:' #CompactNamespaces: false # Unknown to clang-format-4.0 ConstructorInitializerAllOnOneLineOrOnePerLine: false -ConstructorInitializerIndentWidth: 8 -ContinuationIndentWidth: 8 +ConstructorInitializerIndentWidth: 4 +ContinuationIndentWidth: 4 Cpp11BracedListStyle: false DerivePointerAlignment: false DisableFormat: false @@ -501,7 +501,7 @@ IncludeCategories: IncludeIsMainRegex: '(Test)?$' IndentCaseLabels: false #IndentPPDirectives: None # Unknown to clang-format-5.0 -IndentWidth: 8 +IndentWidth: 4 IndentWrappedFunctionNames: false JavaScriptQuotes: Leave JavaScriptWrapImports: true @@ -511,7 +511,7 @@ MacroBlockEnd: '' MaxEmptyLinesToKeep: 1 NamespaceIndentation: None #ObjCBinPackProtocolList: Auto # Unknown to clang-format-5.0 -ObjCBlockIndentWidth: 8 +ObjCBlockIndentWidth: 4 ObjCSpaceAfterProperty: true ObjCSpaceBeforeProtocolList: true @@ -543,6 +543,6 @@ SpacesInCStyleCastParentheses: false SpacesInParentheses: false SpacesInSquareBrackets: false Standard: Cpp03 -TabWidth: 8 -UseTab: Always +TabWidth: 4 +UseTab: Never ... diff --git a/kernel/.gitignore b/kernel/.gitignore new file mode 100644 index 0000000..20d68ae --- /dev/null +++ b/kernel/.gitignore @@ -0,0 +1,22 @@ +.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 \ No newline at end of file diff --git a/kernel/.vscode/c_cpp_properties.json b/kernel/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..f661370 --- /dev/null +++ b/kernel/.vscode/c_cpp_properties.json @@ -0,0 +1,11 @@ +{ + "configurations": [ + { + "name": "Linux", + "cStandard": "c11", + "intelliSenseMode": "gcc-arm64", + "compileCommands": "${workspaceFolder}/compile_commands.json" + } + ], + "version": 4 +} \ No newline at end of file diff --git a/kernel/.vscode/generate_compdb.py b/kernel/.vscode/generate_compdb.py new file mode 100644 index 0000000..8866913 --- /dev/null +++ b/kernel/.vscode/generate_compdb.py @@ -0,0 +1,92 @@ +#!/usr/bin/env python3 + +from __future__ import print_function, division + +import argparse +import fnmatch +import functools +import json +import math +import multiprocessing +import os +import re +import sys + + +CMD_VAR_RE = re.compile(r'^\s*(?:saved)?cmd_(\S+)\s*:=\s*(.+)\s*$', re.MULTILINE) +SOURCE_VAR_RE = re.compile(r'^\s*source_(\S+)\s*:=\s*(.+)\s*$', re.MULTILINE) + + +def print_progress_bar(progress): + progress_bar = '[' + '|' * int(50 * progress) + '-' * int(50 * (1.0 - progress)) + ']' + print('\r', progress_bar, "{0:.1%}".format(progress), end='\r', file=sys.stderr) + + +def parse_cmd_file(out_dir, cmdfile_path): + with open(cmdfile_path, 'r') as cmdfile: + cmdfile_content = cmdfile.read() + + commands = { match.group(1): match.group(2) for match in CMD_VAR_RE.finditer(cmdfile_content) } + sources = { match.group(1): match.group(2) for match in SOURCE_VAR_RE.finditer(cmdfile_content) } + + return [{ + 'directory': out_dir, + 'command': commands[o_file_name], + 'file': source, + 'output': o_file_name + } for o_file_name, source in sources.items()] + + +def gen_compile_commands(cmd_file_search_path, out_dir): + print("Building *.o.cmd file list...", file=sys.stderr) + + out_dir = os.path.abspath(out_dir) + + if not cmd_file_search_path: + cmd_file_search_path = [out_dir] + + cmd_files = [] + for search_path in cmd_file_search_path: + if (os.path.isdir(search_path)): + for cur_dir, subdir, files in os.walk(search_path): + cmd_files.extend(os.path.join(cur_dir, cmdfile_name) for cmdfile_name in fnmatch.filter(files, '*.o.cmd')) + else: + cmd_files.extend(search_path) + + if not cmd_files: + print("No *.o.cmd files found in", ", ".join(cmd_file_search_path), file=sys.stderr) + return + + print("Parsing *.o.cmd files...", file=sys.stderr) + + n_processed = 0 + print_progress_bar(0) + + compdb = [] + pool = multiprocessing.Pool() + try: + for compdb_chunk in pool.imap_unordered(functools.partial(parse_cmd_file, out_dir), cmd_files, chunksize=int(math.sqrt(len(cmd_files)))): + compdb.extend(compdb_chunk) + n_processed += 1 + print_progress_bar(n_processed / len(cmd_files)) + + finally: + pool.terminate() + pool.join() + + print(file=sys.stderr) + print("Writing compile_commands.json...", file=sys.stderr) + + with open('compile_commands.json', 'w') as compdb_file: + json.dump(compdb, compdb_file, indent=1) + + +def main(): + cmd_parser = argparse.ArgumentParser() + cmd_parser.add_argument('-O', '--out-dir', type=str, default=os.getcwd(), help="Build output directory") + cmd_parser.add_argument('cmd_file_search_path', nargs='*', help="*.cmd file search path") + gen_compile_commands(**vars(cmd_parser.parse_args())) + + +if __name__ == '__main__': + main() diff --git a/kernel/.vscode/tasks.json b/kernel/.vscode/tasks.json new file mode 100644 index 0000000..4ed9adb --- /dev/null +++ b/kernel/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "Generate compile_commands.json", + "type": "process", + "command": "python", + "args": [ + "${workspaceRoot}/.vscode/generate_compdb.py" + ], + "problemMatcher": [] + } + ] +} \ No newline at end of file diff --git a/kernel/Kconfig b/kernel/Kconfig index a24a5b9..fd5e0c1 100644 --- a/kernel/Kconfig +++ b/kernel/Kconfig @@ -1,57 +1,42 @@ menu "KernelSU" config KSU - tristate "KernelSU function support" - depends on OVERLAY_FS - default y - help - Enable kernel-level root privileges on Android System. - To compile as a module, choose M here: the - module will be called kernelsu. + tristate "KernelSU function support" + default y + help + Enable kernel-level root privileges on Android System. + 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. + bool "KernelSU debug mode" + depends on KSU + default n + help + Enable KernelSU debug mode. + +config KSU_MANUAL_SU + bool "Use manual su" + depends on KSU + default y + help + Use manual su and authorize the corresponding command line and application via prctl config KPM - bool "Enable SukiSU KPM" - depends on KSU && 64BIT - default n - help - Enabling this option will activate the KPM feature of SukiSU. - This option is suitable for scenarios where you need to force KPM to be enabled. - but it may affect system stability. - select KALLSYMS - select KALLSYMS_ALL - -choice - prompt "KernelSU hook type" - depends on KSU - default KSU_KPROBES_HOOK + bool "Enable SukiSU KPM" + depends on KSU && 64BIT + default n help - Hook type for KernelSU - -config KSU_KPROBES_HOOK - bool "Hook KernelSU with Kprobes" - depends on KPROBES - help - If enabled, Hook required KernelSU syscalls with Kernel-probe. - -config KSU_TRACEPOINT_HOOK - bool "Hook KernelSU with Tracepoint" - depends on TRACEPOINTS - help - If enabled, Hook required KernelSU syscalls with Tracepoint. + Enabling this option will activate the KPM feature of SukiSU. + This option is suitable for scenarios where you need to force KPM to be enabled. + but it may affect system stability. + select KALLSYMS + select KALLSYMS_ALL config KSU_MANUAL_HOOK bool "Hook KernelSU manually" - depends on KSU != m - help - If enabled, Hook required KernelSU syscalls with manually-patched function. - -endchoice + depends on KSU != m + help + If enabled, Hook required KernelSU syscalls with manually-patched function. endmenu diff --git a/kernel/Makefile b/kernel/Makefile index 0c2ba39..8b246bd 100644 --- a/kernel/Makefile +++ b/kernel/Makefile @@ -1,17 +1,28 @@ kernelsu-objs := ksu.o kernelsu-objs += allowlist.o +kernelsu-objs += app_profile.o kernelsu-objs += dynamic_manager.o kernelsu-objs += apk_sign.o kernelsu-objs += sucompat.o +kernelsu-objs += syscall_hook_manager.o kernelsu-objs += throne_tracker.o -kernelsu-objs += core_hook.o +kernelsu-objs += pkg_observer.o +kernelsu-objs += throne_tracker.o +kernelsu-objs += umount_manager.o +kernelsu-objs += setuid_hook.o +kernelsu-objs += kernel_umount.o +kernelsu-objs += supercalls.o +kernelsu-objs += feature.o kernelsu-objs += ksud.o kernelsu-objs += embed_ksud.o -kernelsu-objs += kernel_compat.o +kernelsu-objs += seccomp_cache.o +kernelsu-objs += file_wrapper.o kernelsu-objs += throne_comm.o +kernelsu-objs += sulog.o -ifeq ($(CONFIG_KSU_TRACEPOINT_HOOK), y) -kernelsu-objs += ksu_trace.o +ifeq ($(CONFIG_KSU_MANUAL_SU), y) +ccflags-y += -DCONFIG_KSU_MANUAL_SU +kernelsu-objs += manual_su.o endif kernelsu-objs += selinux/selinux.o @@ -21,46 +32,62 @@ 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 -obj-$(CONFIG_KSU_TRACEPOINT_HOOK) += ksu_trace_export.o obj-$(CONFIG_KPM) += kpm/ - REPO_OWNER := SukiSU-Ultra REPO_NAME := SukiSU-Ultra REPO_BRANCH := main -KSU_VERSION_API := 3.2.0 +KSU_VERSION_API := 4.0.0 GIT_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin git CURL_BIN := /usr/bin/env PATH="$$PATH":/usr/bin:/usr/local/bin curl +KDIR := $(KDIR) +MDIR := $(realpath $(dir $(abspath $(lastword $(MAKEFILE_LIST))))) + +ifneq ($(KDIR),) +$(info -- KDIR: $(KDIR)) +$(info -- MDIR: $(MDIR)) +endif + KSU_GITHUB_VERSION := $(shell $(CURL_BIN) -s "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/releases/latest" | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/') KSU_GITHUB_VERSION_COMMIT := $(shell $(CURL_BIN) -sI "https://api.github.com/repos/$(REPO_OWNER)/$(REPO_NAME)/commits?sha=$(REPO_BRANCH)&per_page=1" | grep -i "link:" | sed -n 's/.*page=\([0-9]*\)>; rel="last".*/\1/p') -LOCAL_GIT_EXISTS := $(shell test -e $(srctree)/$(src)/../.git && echo 1 || echo 0) +ifeq ($(findstring $(srctree),$(src)),$(srctree)) + KSU_SRC := $(src) +else + KSU_SRC := $(srctree)/$(src) +endif + +ifneq ($(shell test -e $(KSU_SRC)/../.git && echo "in-tree"),in-tree) + KSU_SRC := $(MDIR) +endif + +LOCAL_GIT_EXISTS := $(shell test -e $(KSU_SRC)/../.git && echo 1 || echo 0) define get_ksu_version_full -v$1-$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(srctree)/$(src); $(GIT_BIN) rev-parse --abbrev-ref HEAD) +v$1-$(shell cd $(KSU_SRC); $(GIT_BIN) rev-parse --short=8 HEAD)@$(shell cd $(KSU_SRC); $(GIT_BIN) rev-parse --abbrev-ref HEAD) endef ifeq ($(KSU_GITHUB_VERSION_COMMIT),) ifeq ($(LOCAL_GIT_EXISTS),1) - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) - KSU_LOCAL_VERSION := $(shell cd $(srctree)/$(src); $(GIT_BIN) rev-list --count $(REPO_BRANCH)) - KSU_VERSION := $(shell expr 10000 + $(KSU_LOCAL_VERSION) + 700) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + KSU_LOCAL_VERSION := $(shell cd $(KSU_SRC); $(GIT_BIN) rev-list --count $(REPO_BRANCH)) + KSU_VERSION := $(shell expr 40000 + $(KSU_LOCAL_VERSION) - 2815) $(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION)) else KSU_VERSION := 13000 $(warning -- Could not fetch version online or via local .git! Using fallback version: $(KSU_VERSION)) endif else - KSU_VERSION := $(shell expr 10000 + $(KSU_GITHUB_VERSION_COMMIT) + 700) + KSU_VERSION := $(shell expr 40000 + $(KSU_GITHUB_VERSION_COMMIT) - 2815) $(info -- $(REPO_NAME) version (GitHub): $(KSU_VERSION)) endif ifeq ($(KSU_GITHUB_VERSION),) ifeq ($(LOCAL_GIT_EXISTS),1) - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_VERSION_API)) $(info -- $(REPO_NAME) version (local .git): $(KSU_VERSION_FULL)) $(info -- $(REPO_NAME) Formatted version (local .git): $(KSU_VERSION)) @@ -69,7 +96,7 @@ ifeq ($(KSU_GITHUB_VERSION),) $(warning -- $(REPO_NAME) version: $(KSU_VERSION_FULL)) endif else - $(shell cd $(srctree)/$(src); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) + $(shell cd $(KSU_SRC); [ -f ../.git/shallow ] && $(GIT_BIN) fetch --unshallow) KSU_VERSION_FULL := $(call get_ksu_version_full,$(KSU_GITHUB_VERSION)) $(info -- $(REPO_NAME) version (Github): $(KSU_VERSION_FULL)) endif @@ -93,14 +120,13 @@ ccflags-y += -DKSU_MANAGER_PACKAGE=\"$(KSU_MANAGER_PACKAGE)\" $(info -- SukiSU Manager package name: $(KSU_MANAGER_PACKAGE)) endif -$(info -- Supported Unofficial Manager: 5ec1cff (GKI) ShirkNeko udochina (GKI and KPM)) - -ifeq ($(CONFIG_KSU_KPROBES_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_KPROBES_HOOK) -else ifeq ($(CONFIG_KSU_TRACEPOINT_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_TRACEPOINT_HOOK) -else ifeq ($(CONFIG_KSU_MANUAL_HOOK), y) -$(info -- SukiSU: CONFIG_KSU_MANUAL_HOOK) +ifeq ($(CONFIG_KSU_MANUAL_HOOK), y) +ccflags-y += -DKSU_MANUAL_HOOK +$(info -- SukiSU: KSU_MANUAL_HOOK Temporarily discontinued)) +else +ccflags-y += -DKSU_KPROBES_HOOK +ccflags-y += -DKSU_TP_HOOK +$(info -- SukiSU: KSU_TRACEPOINT_HOOK) endif KERNEL_VERSION := $(VERSION).$(PATCHLEVEL) @@ -117,14 +143,30 @@ endif $(info -- KERNEL_VERSION: $(KERNEL_VERSION)) $(info -- KERNEL_TYPE: $(KERNEL_TYPE)) -$(info -- KERNEL_VERSION: $(KERNEL_VERSION)) ifeq ($(CONFIG_KPM), y) $(info -- KPM is enabled) else $(info -- KPM is disabled) endif -ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -ccflags-y += -Wno-declaration-after-statement -Wno-unused-function +# Check new vfs_getattr() +ifeq ($(shell grep -A1 "^int vfs_getattr" $(srctree)/fs/stat.c | grep -q "query_flags" ; echo $$?),0) +ccflags-y += -DKSU_HAS_NEW_VFS_GETATTR +endif + +# Function proc_ops check +ifeq ($(shell grep -q "struct proc_ops " $(srctree)/include/linux/proc_fs.h; echo $$?),0) +ccflags-y += -DKSU_COMPAT_HAS_PROC_OPS +endif + +ccflags-y += -Wno-strict-prototypes -Wno-int-conversion -Wno-gcc-compat -Wno-missing-prototypes +ccflags-y += -Wno-declaration-after-statement -Wno-unused-function -Wno-unused-variable + +all: + make -C $(KDIR) M=$(MDIR) modules +compdb: + python3 $(MDIR)/.vscode/generate_compdb.py -O $(KDIR) $(MDIR) +clean: + make -C $(KDIR) M=$(MDIR) clean # Keep a new line here!! Because someone may append config diff --git a/kernel/allowlist.c b/kernel/allowlist.c index 9745567..914fefe 100644 --- a/kernel/allowlist.c +++ b/kernel/allowlist.c @@ -1,3 +1,5 @@ +#include +#include #include #include #include @@ -8,14 +10,16 @@ #include #include #include +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0) #include +#endif -#include "ksu.h" #include "klog.h" // IWYU pragma: keep +#include "ksud.h" #include "selinux/selinux.h" -#include "kernel_compat.h" #include "allowlist.h" #include "manager.h" +#include "syscall_hook_manager.h" #define FILE_MAGIC 0x7f4b5355 // ' KSU', u32 #define FILE_FORMAT_VERSION 3 // u32 @@ -29,58 +33,61 @@ static DEFINE_MUTEX(allowlist_mutex); 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); +void persistent_allow_list(void); + +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 *temp_arr; - int i, j; + int *temp_arr; + int i, j; - if (allow_list_pointer == 0) - return; + if (allow_list_pointer == 0) + return; - temp_arr = kmalloc(sizeof(allow_list_arr), GFP_KERNEL); - if (temp_arr == NULL) { - pr_err("%s: unable to allocate memory\n", __func__); - return; - } + temp_arr = kmalloc(sizeof(allow_list_arr), GFP_KERNEL); + if (temp_arr == NULL) { + pr_err("%s: unable to allocate memory\n", __func__); + return; + } - for (i = j = 0; i < allow_list_pointer; i++) { - if (allow_list_arr[i] == uid) - continue; - temp_arr[j++] = allow_list_arr[i]; - } + for (i = j = 0; i < allow_list_pointer; i++) { + if (allow_list_arr[i] == uid) + continue; + temp_arr[j++] = allow_list_arr[i]; + } - allow_list_pointer = j; + allow_list_pointer = j; - for (; j < ARRAY_SIZE(allow_list_arr); j++) - temp_arr[j] = -1; + for (; j < ARRAY_SIZE(allow_list_arr); j++) + temp_arr[j] = -1; - memcpy(&allow_list_arr, temp_arr, PAGE_SIZE); - kfree(temp_arr); + memcpy(&allow_list_arr, temp_arr, PAGE_SIZE); + kfree(temp_arr); } -static void init_default_profiles() +static void init_default_profiles(void) { - kernel_cap_t full_cap = CAP_FULL_SET; + 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 = 0; - strcpy(default_root_profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); + 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 = 0; + 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; + // This means that we will umount modules by default! + default_non_root_profile.umount_modules = true; } struct perm_data { - struct list_head list; - struct app_profile profile; + struct list_head list; + struct app_profile profile; }; static struct list_head allow_list; @@ -90,437 +97,535 @@ static uint8_t allow_list_bitmap[PAGE_SIZE] __read_mostly __aligned(PAGE_SIZE); #define KERNEL_SU_ALLOWLIST "/data/adb/ksu/.allowlist" -static struct work_struct ksu_save_work; -static struct work_struct ksu_load_work; - -bool persistent_allow_list(void); - void ksu_show_allow_list(void) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - pr_info("ksu_show_allow_list\n"); - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - pr_info("uid :%d, allow: %d\n", p->profile.current_uid, - p->profile.allow_su); - } + struct perm_data *p = NULL; + struct list_head *pos = NULL; + pr_info("ksu_show_allow_list\n"); + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + pr_info("uid :%d, allow: %d\n", p->profile.current_uid, + p->profile.allow_su); + } } #ifdef CONFIG_KSU_DEBUG -static void ksu_grant_root_to_shell() +static void ksu_grant_root_to_shell(void) { - struct app_profile profile = { - .version = KSU_APP_PROFILE_VER, - .allow_su = true, - .current_uid = 2000, - }; - strcpy(profile.key, "com.android.shell"); - strcpy(profile.rp_config.profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); - ksu_set_app_profile(&profile, false); + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = true, + .current_uid = 2000, + }; + strcpy(profile.key, "com.android.shell"); + strcpy(profile.rp_config.profile.selinux_domain, + KSU_DEFAULT_SELINUX_DOMAIN); + ksu_set_app_profile(&profile, false); } #endif bool ksu_get_app_profile(struct app_profile *profile) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - bool found = false; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, 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; - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, 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: - return found; + 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 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) { + return false; + } - if (profile->version < KSU_APP_PROFILE_VER) { - pr_info("Unsupported profile version: %d\n", profile->version); - 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 (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; - } - } + if (strlen(profile->rp_config.profile.selinux_domain) == 0) { + return false; + } + } - return true; + return true; } bool ksu_set_app_profile(struct app_profile *profile, bool persist) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - bool result = false; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool result = false; - if (!profile_valid(profile)) { - pr_err("Failed to set app profile: invalid profile!\n"); - return false; - } + if (!profile_valid(profile)) { + pr_err("Failed to set app profile: invalid profile!\n"); + return false; + } - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - // 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! - memcpy(&p->profile, profile, sizeof(*profile)); - result = true; - goto out; - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + // 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! + memcpy(&p->profile, profile, sizeof(*profile)); + result = true; + goto out; + } + } - // not found, alloc a new node! - p = (struct perm_data *)kmalloc(sizeof(struct perm_data), GFP_KERNEL); - if (!p) { - pr_err("ksu_set_app_profile alloc failed\n"); - return false; - } + // not found, alloc a new node! + p = (struct perm_data *)kmalloc(sizeof(struct perm_data), GFP_KERNEL); + if (!p) { + pr_err("ksu_set_app_profile alloc failed\n"); + return false; + } - 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(&p->list, &allow_list); + 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(&p->list, &allow_list); out: - 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); - return false; - } - allow_list_arr[allow_list_pointer++] = profile->current_uid; - } else { - remove_uid_from_arr(profile->current_uid); - } - } - result = true; + 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); + return false; + } + allow_list_arr[allow_list_pointer++] = profile->current_uid; + } else { + remove_uid_from_arr(profile->current_uid); + } + } + result = true; - // 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)); - } + // 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)); + } - if (unlikely(!strcmp(profile->key, "#"))) { - // set default root profile - memcpy(&default_root_profile, &profile->rp_config.profile, - sizeof(default_root_profile)); - } + if (unlikely(!strcmp(profile->key, "#"))) { + // set default root profile + memcpy(&default_root_profile, &profile->rp_config.profile, + sizeof(default_root_profile)); + } - if (persist) - persistent_allow_list(); + if (persist) { + persistent_allow_list(); + // FIXME: use a new flag + ksu_mark_running_process(); + } - return result; + return result; } bool __ksu_is_allow_uid(uid_t uid) { - int i; + int i; - if (unlikely(uid == 0)) { - // already root, but only allow our domain. - return is_ksu_domain(); - } + if (forbid_system_uid(uid)) { + // do not bother going through the list if it's system + return false; + } - if (forbid_system_uid(uid)) { - // do not bother going through the list if it's system - return false; - } + if (likely(ksu_is_manager_uid_valid()) && + unlikely(ksu_get_manager_uid() == uid)) { + // manager is always allowed! + return true; + } - if (likely(ksu_is_manager_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) { - // manager is always allowed! - 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; + } + } - 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; +} - 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_uid_valid()) && unlikely(ksu_get_manager_uid() == uid)) { - // 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; - } - } + struct app_profile profile = { .current_uid = uid }; + if (likely(ksu_is_manager_uid_valid()) && + unlikely(ksu_get_manager_uid() == uid)) { + // 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; + } + } } struct root_profile *ksu_get_root_profile(uid_t uid) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; + struct perm_data *p = NULL; + struct list_head *pos = NULL; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - if (uid == p->profile.current_uid && p->profile.allow_su) { - if (!p->profile.rp_config.use_default) { - return &p->profile.rp_config.profile; - } - } - } + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (uid == p->profile.current_uid && p->profile.allow_su) { + if (!p->profile.rp_config.use_default) { + return &p->profile.rp_config.profile; + } + } + } - // use default profile - return &default_root_profile; + // use default profile + return &default_root_profile; } bool ksu_get_allow_list(int *array, int *length, bool allow) { - struct perm_data *p = NULL; - struct list_head *pos = NULL; - int i = 0; - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - // pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow); - if (p->profile.allow_su == allow) { - array[i++] = p->profile.current_uid; - } - } - *length = i; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + int i = 0; + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + // pr_info("get_allow_list uid: %d allow: %d\n", p->uid, p->allow); + if (p->profile.allow_su == allow) { + array[i++] = p->profile.current_uid; + } + } + *length = i; - return true; + return true; } -void do_save_allow_list(struct work_struct *work) +static void do_persistent_allow_list(struct callback_head *_cb) { - u32 magic = FILE_MAGIC; - u32 version = FILE_FORMAT_VERSION; - struct perm_data *p = NULL; - struct list_head *pos = NULL; - loff_t off = 0; + u32 magic = FILE_MAGIC; + u32 version = FILE_FORMAT_VERSION; + struct perm_data *p = NULL; + struct list_head *pos = NULL; + loff_t off = 0; - struct file *fp = - ksu_filp_open_compat(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)); - return; - } + mutex_lock(&allowlist_mutex); + 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 unlock; + } - // store magic and version - if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != - sizeof(magic)) { - pr_err("save_allow_list write magic failed.\n"); - goto exit; - } + // 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 (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != - sizeof(version)) { - pr_err("save_allow_list write version failed.\n"); - goto exit; - } + if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) { + pr_err("save_allow_list write version failed.\n"); + goto close_file; + } - list_for_each (pos, &allow_list) { - p = list_entry(pos, struct perm_data, list); - pr_info("save allow list, name: %s uid :%d, allow: %d\n", - p->profile.key, p->profile.current_uid, - p->profile.allow_su); + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + pr_info("save allow list, name: %s uid :%d, allow: %d\n", + p->profile.key, p->profile.current_uid, p->profile.allow_su); - ksu_kernel_write_compat(fp, &p->profile, sizeof(p->profile), - &off); - } + kernel_write(fp, &p->profile, sizeof(p->profile), &off); + } -exit: - filp_close(fp, 0); +close_file: + filp_close(fp, 0); +unlock: + mutex_unlock(&allowlist_mutex); + kfree(_cb); } -void do_load_allow_list(struct work_struct *work) +void persistent_allow_list() { - loff_t off = 0; - ssize_t ret = 0; - struct file *fp = NULL; - u32 magic; - u32 version; + 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; + task_work_add(tsk, cb, TWA_RESUME); + +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; #ifdef CONFIG_KSU_DEBUG - // always allow adb shell by default - ksu_grant_root_to_shell(); + // always allow adb shell by default + ksu_grant_root_to_shell(); #endif - // load allowlist now! - fp = ksu_filp_open_compat(KERNEL_SU_ALLOWLIST, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_err("load_allow_list open file failed: %ld\n", PTR_ERR(fp)); - return; - } + // 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 (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != - sizeof(magic) || - magic != FILE_MAGIC) { - pr_err("allowlist file invalid: %d!\n", magic); - goto exit; - } + // 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 (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != - sizeof(version)) { - pr_err("allowlist read version: %d failed\n", version); - 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); + pr_info("allowlist version: %d\n", version); - while (true) { - struct app_profile profile; + while (true) { + struct app_profile profile; - ret = ksu_kernel_read_compat(fp, &profile, sizeof(profile), - &off); + ret = kernel_read(fp, &profile, sizeof(profile), &off); - if (ret <= 0) { - pr_info("load_allow_list read err: %zd\n", ret); - break; - } + 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, false); - } + 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, false); + } exit: - ksu_show_allow_list(); - filp_close(fp, 0); + ksu_show_allow_list(); + filp_close(fp, 0); } -void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), void *data) +void ksu_prune_allowlist(bool (*is_uid_valid)(uid_t, char *, void *), + void *data) { - struct perm_data *np = NULL; - struct perm_data *n = NULL; + struct perm_data *np = NULL; + struct perm_data *n = NULL; - bool modified = false; - // TODO: use RCU! - 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(&np->list); - if (likely(uid <= BITMAP_UID_MAX)) { - allow_list_bitmap[uid / BITS_PER_BYTE] &= ~(1 << (uid % BITS_PER_BYTE)); - } - remove_uid_from_arr(uid); - smp_mb(); - kfree(np); - } - } - mutex_unlock(&allowlist_mutex); + if (!ksu_boot_completed) { + pr_info("boot not completed, skip prune\n"); + return; + } - if (modified) { - persistent_allow_list(); - } -} + bool modified = false; + // TODO: use RCU! + 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(&np->list); + if (likely(uid <= BITMAP_UID_MAX)) { + allow_list_bitmap[uid / BITS_PER_BYTE] &= + ~(1 << (uid % BITS_PER_BYTE)); + } + remove_uid_from_arr(uid); + smp_mb(); + kfree(np); + } + } + mutex_unlock(&allowlist_mutex); -// make sure allow list works cross boot -bool persistent_allow_list(void) -{ - return ksu_queue_work(&ksu_save_work); -} - -bool ksu_load_allow_list(void) -{ - return ksu_queue_work(&ksu_load_work); + if (modified) { + persistent_allow_list(); + } } void ksu_allowlist_init(void) { - int i; + int i; - BUILD_BUG_ON(sizeof(allow_list_bitmap) != PAGE_SIZE); - BUILD_BUG_ON(sizeof(allow_list_arr) != PAGE_SIZE); + 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; + for (i = 0; i < ARRAY_SIZE(allow_list_arr); i++) + allow_list_arr[i] = -1; - INIT_LIST_HEAD(&allow_list); + INIT_LIST_HEAD(&allow_list); - INIT_WORK(&ksu_save_work, do_save_allow_list); - INIT_WORK(&ksu_load_work, do_load_allow_list); - - init_default_profiles(); + init_default_profiles(); } void ksu_allowlist_exit(void) { - struct perm_data *np = NULL; - struct perm_data *n = NULL; + struct perm_data *np = NULL; + struct perm_data *n = NULL; - do_save_allow_list(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); + // 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); } + +#ifdef CONFIG_KSU_MANUAL_SU +bool ksu_temp_grant_root_once(uid_t uid) +{ + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = true, + .current_uid = uid, + }; + + const char *default_key = "com.temp.once"; + + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; + + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (p->profile.current_uid == uid) { + strcpy(profile.key, p->profile.key); + found = true; + break; + } + } + + if (!found) { + strcpy(profile.key, default_key); + } + + profile.rp_config.profile.uid = default_root_profile.uid; + profile.rp_config.profile.gid = default_root_profile.gid; + profile.rp_config.profile.groups_count = default_root_profile.groups_count; + memcpy(profile.rp_config.profile.groups, default_root_profile.groups, sizeof(default_root_profile.groups)); + memcpy(&profile.rp_config.profile.capabilities, &default_root_profile.capabilities, sizeof(default_root_profile.capabilities)); + profile.rp_config.profile.namespaces = default_root_profile.namespaces; + strcpy(profile.rp_config.profile.selinux_domain, default_root_profile.selinux_domain); + + bool ok = ksu_set_app_profile(&profile, false); + if (ok) + pr_info("pending_root: UID=%d granted and persisted\n", uid); + return ok; +} + +void ksu_temp_revoke_root_once(uid_t uid) +{ + struct app_profile profile = { + .version = KSU_APP_PROFILE_VER, + .allow_su = false, + .current_uid = uid, + }; + + const char *default_key = "com.temp.once"; + + struct perm_data *p = NULL; + struct list_head *pos = NULL; + bool found = false; + + list_for_each (pos, &allow_list) { + p = list_entry(pos, struct perm_data, list); + if (p->profile.current_uid == uid) { + strcpy(profile.key, p->profile.key); + found = true; + break; + } + } + + if (!found) { + strcpy(profile.key, default_key); + } + + profile.nrp_config.profile.umount_modules = default_non_root_profile.umount_modules; + strcpy(profile.rp_config.profile.selinux_domain, KSU_DEFAULT_SELINUX_DOMAIN); + + ksu_set_app_profile(&profile, false); + persistent_allow_list(); + pr_info("pending_root: UID=%d removed and persist updated\n", uid); +} +#endif \ No newline at end of file diff --git a/kernel/allowlist.h b/kernel/allowlist.h index e89bf71..4bac8c3 100644 --- a/kernel/allowlist.h +++ b/kernel/allowlist.h @@ -2,19 +2,29 @@ #define __KSU_H_ALLOWLIST #include -#include "ksu.h" +#include +#include "app_profile.h" + +#define PER_USER_RANGE 100000 +#define FIRST_APPLICATION_UID 10000 +#define LAST_APPLICATION_UID 19999 void ksu_allowlist_init(void); void ksu_allowlist_exit(void); -bool ksu_load_allow_list(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, int *length, bool allow); void ksu_prune_allowlist(bool (*is_uid_exist)(uid_t, char *, void *), void *data); @@ -24,4 +34,16 @@ bool ksu_set_app_profile(struct app_profile *, bool persist); bool ksu_uid_should_umount(uid_t uid); struct root_profile *ksu_get_root_profile(uid_t uid); + +static inline bool is_appuid(uid_t uid) +{ + uid_t appid = uid % PER_USER_RANGE; + return appid >= FIRST_APPLICATION_UID && appid <= LAST_APPLICATION_UID; +} + +#ifdef CONFIG_KSU_MANUAL_SU +bool ksu_temp_grant_root_once(uid_t uid); +void ksu_temp_revoke_root_once(uid_t uid); +#endif + #endif diff --git a/kernel/apk_sign.c b/kernel/apk_sign.c index 3c22ca0..1d49985 100644 --- a/kernel/apk_sign.c +++ b/kernel/apk_sign.c @@ -17,69 +17,65 @@ #include "apk_sign.h" #include "dynamic_manager.h" #include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" #include "manager_sign.h" struct sdesc { - struct shash_desc shash; - char ctx[]; + struct shash_desc shash; + char ctx[]; }; -static struct apk_sign_key { - unsigned size; - const char *sha256; -} apk_sign_keys[] = { - {EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU +static apk_sign_key_t apk_sign_keys[] = { + {EXPECTED_SIZE_SHIRKNEKO, EXPECTED_HASH_SHIRKNEKO}, // ShirkNeko/SukiSU #ifdef EXPECTED_SIZE - {EXPECTED_SIZE, EXPECTED_HASH}, // Custom + {EXPECTED_SIZE, EXPECTED_HASH}, // Custom #endif }; static struct sdesc *init_sdesc(struct crypto_shash *alg) { - struct sdesc *sdesc; - int size; + struct sdesc *sdesc; + int size; - size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); - sdesc = kmalloc(size, GFP_KERNEL); - if (!sdesc) - return ERR_PTR(-ENOMEM); - sdesc->shash.tfm = alg; - return sdesc; + size = sizeof(struct shash_desc) + crypto_shash_descsize(alg); + sdesc = kmalloc(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) + unsigned int datalen, unsigned char *digest) { - struct sdesc *sdesc; - int ret; + struct sdesc *sdesc; + int ret; - sdesc = init_sdesc(alg); - if (IS_ERR(sdesc)) { - pr_info("can't alloc sdesc\n"); - return PTR_ERR(sdesc); - } + 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; + 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) + unsigned char *digest) { - struct crypto_shash *alg; - char *hash_alg_name = "sha256"; - int ret; + 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; + 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; } @@ -87,304 +83,307 @@ static struct dynamic_sign_key dynamic_sign = DYNAMIC_SIGN_DEFAULT_CONFIG; static bool check_dynamic_sign(struct file *fp, u32 size4, loff_t *pos, int *matched_index) { - struct dynamic_sign_key current_dynamic_key = dynamic_sign; - - if (ksu_get_dynamic_manager_config(¤t_dynamic_key.size, ¤t_dynamic_key.hash)) { - pr_debug("Using dynamic manager config: size=0x%x, hash=%.16s...\n", - current_dynamic_key.size, current_dynamic_key.hash); - } - - if (size4 != current_dynamic_key.size) { - return false; - } + struct dynamic_sign_key current_dynamic_key = dynamic_sign; + + if (ksu_get_dynamic_manager_config(¤t_dynamic_key.size, ¤t_dynamic_key.hash)) { + pr_debug("Using dynamic manager config: size=0x%x, hash=%.16s...\n", + current_dynamic_key.size, current_dynamic_key.hash); + } + + if (size4 != current_dynamic_key.size) { + return false; + } #define CERT_MAX_LENGTH 1024 - char cert[CERT_MAX_LENGTH]; - if (size4 > CERT_MAX_LENGTH) { - pr_info("cert length overlimit\n"); - return false; - } - - ksu_kernel_read_compat(fp, cert, size4, pos); - - unsigned char digest[SHA256_DIGEST_SIZE]; - if (ksu_sha256(cert, size4, digest) < 0) { - pr_info("sha256 error\n"); - return false; - } + 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 (ksu_sha256(cert, size4, digest) < 0) { + 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, index: dynamic\n", hash_str, current_dynamic_key.hash); - - if (strcmp(current_dynamic_key.hash, hash_str) == 0) { - if (matched_index) { - *matched_index = DYNAMIC_SIGN_INDEX; - } - return true; - } - - 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, index: dynamic\n", hash_str, current_dynamic_key.hash); + + if (strcmp(current_dynamic_key.hash, hash_str) == 0) { + if (matched_index) { + *matched_index = DYNAMIC_SIGN_INDEX; + } + return true; + } + + return false; } static bool check_block(struct file *fp, u32 *size4, loff_t *pos, u32 *offset, int *matched_index) { - int i; - struct apk_sign_key sign_key; - bool signature_valid = false; + int i; + apk_sign_key_t sign_key; + bool signature_valid = false; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer-sequence length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signer length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // signed data length + 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; + *offset += 0x4 * 3; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // digests-sequence length + kernel_read(fp, size4, 0x4, pos); // digests-sequence length - *pos += *size4; - *offset += 0x4 + *size4; + *pos += *size4; + *offset += 0x4 + *size4; - ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificates length - ksu_kernel_read_compat(fp, size4, 0x4, pos); // certificate length - *offset += 0x4 * 2; + kernel_read(fp, size4, 0x4, pos); // certificates length + kernel_read(fp, size4, 0x4, pos); // certificate length + *offset += 0x4 * 2; - if (ksu_is_dynamic_manager_enabled()) { - loff_t temp_pos = *pos; - if (check_dynamic_sign(fp, *size4, &temp_pos, matched_index)) { - *pos = temp_pos; - *offset += *size4; - return true; - } - } + if (ksu_is_dynamic_manager_enabled()) { + loff_t temp_pos = *pos; + if (check_dynamic_sign(fp, *size4, &temp_pos, matched_index)) { + *pos = temp_pos; + *offset += *size4; + return true; + } + } - for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) { - sign_key = apk_sign_keys[i]; + for (i = 0; i < ARRAY_SIZE(apk_sign_keys); i++) { + sign_key = apk_sign_keys[i]; - if (*size4 != sign_key.size) - continue; - *offset += *size4; + if (*size4 != sign_key.size) + continue; + *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; - } - ksu_kernel_read_compat(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 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 (ksu_sha256(cert, *size4, digest) < 0 ) { + pr_info("sha256 error\n"); + return false; + } - char hash_str[SHA256_DIGEST_SIZE * 2 + 1]; - hash_str[SHA256_DIGEST_SIZE * 2] = '\0'; + 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, index: %d\n", hash_str, sign_key.sha256, i); - - if (strcmp(sign_key.sha256, hash_str) == 0) { - signature_valid = true; - if (matched_index) { - *matched_index = i; - } - break; - } - } - return signature_valid; + bin2hex(hash_str, digest, SHA256_DIGEST_SIZE); + pr_info("sha256: %s, expected: %s, index: %d\n", hash_str, sign_key.sha256, i); + + if (strcmp(sign_key.sha256, hash_str) == 0) { + signature_valid = true; + if (matched_index) { + *matched_index = i; + } + break; + } + } + return signature_valid; } 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; + 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"; + struct zip_entry_header header; + const char MANIFEST[] = "META-INF/MANIFEST.MF"; - loff_t pos = 0; + loff_t pos = 0; - while (ksu_kernel_read_compat(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)]; - ksu_kernel_read_compat(fp, fileName, - header.file_name_length, &pos); - fileName[header.file_name_length] = '\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; - } + // 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; - } + // Skip to the next entry + pos += header.extra_field_length + header.compressed_size; + } - return false; + return false; } static __always_inline bool check_v2_signature(char *path, bool check_multi_manager, int *signature_index) { - 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 matched_index = -1; - int i; - struct file *fp = ksu_filp_open_compat(path, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_err("open %s error.\n", path); - return false; - } + unsigned char buffer[0x11] = { 0 }; + u32 size4; + u64 size8, size_of_block; - // If you want to check for multi-manager APK signing, but dynamic managering is not enabled, skip - if (check_multi_manager && !ksu_is_dynamic_manager_enabled()) { - filp_close(fp, 0); - return 0; - } + loff_t pos; - // disable inotify for this file - fp->f_mode |= FMODE_NONOTIFY; + bool v2_signing_valid = false; + int v2_signing_blocks = 0; + bool v3_signing_exist = false; + bool v3_1_signing_exist = false; + int matched_index = -1; + int i; + struct file *fp = filp_open(path, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_err("open %s error.\n", path); + return false; + } - // 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); - ksu_kernel_read_compat(fp, &n, 2, &pos); - if (n == i) { - pos -= 22; - ksu_kernel_read_compat(fp, &size4, 4, &pos); - if ((size4 ^ 0xcafebabeu) == 0xccfbf1eeu) { - break; - } - } - if (i == 0xffff) { - pr_info("error: cannot find eocd\n"); - goto clean; - } - } + // If you want to check for multi-manager APK signing, but dynamic managering is not enabled, skip + if (check_multi_manager && !ksu_is_dynamic_manager_enabled()) { + filp_close(fp, 0); + return 0; + } - pos += 12; - // offset - ksu_kernel_read_compat(fp, &size4, 0x4, &pos); - pos = size4 - 0x18; + // disable inotify for this file + fp->f_mode |= FMODE_NONOTIFY; - ksu_kernel_read_compat(fp, &size8, 0x8, &pos); - ksu_kernel_read_compat(fp, buffer, 0x10, &pos); - if (strcmp((char *)buffer, "APK Sig Block 42")) { - goto clean; - } + // 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 = size4 - (size8 + 0x8); - ksu_kernel_read_compat(fp, &size_of_block, 0x8, &pos); - if (size_of_block != size8) { - goto clean; - } + pos += 12; + // offset + kernel_read(fp, &size4, 0x4, &pos); + pos = size4 - 0x18; - int loop_count = 0; - while (loop_count++ < 10) { - uint32_t id; - uint32_t offset; - ksu_kernel_read_compat(fp, &size8, 0x8, - &pos); // sequence length - if (size8 == size_of_block) { - break; - } - ksu_kernel_read_compat(fp, &id, 0x4, &pos); // id - offset = 4; - if (id == 0x7109871au) { - v2_signing_blocks++; - bool result = check_block(fp, &size4, &pos, &offset, &matched_index); - if (result) { - v2_signing_valid = true; - } - } 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 { + 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++; + bool result = check_block(fp, &size4, &pos, &offset, &matched_index); + if (result) { + v2_signing_valid = true; + } + } 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); + pr_info("Unknown id: 0x%08x\n", id); #endif - } - pos += (size8 - offset); - } + } + pos += (size8 - offset); + } - if (v2_signing_blocks != 1) { + if (v2_signing_blocks != 1) { #ifdef CONFIG_KSU_DEBUG - pr_err("Unexpected v2 signature count: %d\n", - v2_signing_blocks); + pr_err("Unexpected v2 signature count: %d\n", + v2_signing_blocks); #endif - v2_signing_valid = false; - } + 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; - } - } + 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); + filp_close(fp, 0); - if (v3_signing_exist || v3_1_signing_exist) { + if (v3_signing_exist || v3_1_signing_exist) { #ifdef CONFIG_KSU_DEBUG - pr_err("Unexpected v3 signature scheme found!\n"); + pr_err("Unexpected v3 signature scheme found!\n"); #endif - return false; - } + return false; + } - if (v2_signing_valid) { - if (signature_index) { - *signature_index = matched_index; - } - - if (check_multi_manager) { - // 0: ShirkNeko/SukiSU, DYNAMIC_SIGN_INDEX : Dynamic Sign - if (matched_index == 0 || matched_index == DYNAMIC_SIGN_INDEX) { - pr_info("Multi-manager APK detected (dynamic_manager enabled): signature_index=%d\n", matched_index); - return true; - } - return false; - } else { - // Common manager check: any valid signature will do - return true; - } - } - return false; + if (v2_signing_valid) { + if (signature_index) { + *signature_index = matched_index; + } + + if (check_multi_manager) { + // 0: ShirkNeko/SukiSU, DYNAMIC_SIGN_INDEX : Dynamic Sign + if (matched_index == 0 || matched_index == DYNAMIC_SIGN_INDEX) { + pr_info("Multi-manager APK detected (dynamic_manager enabled): signature_index=%d\n", matched_index); + return true; + } + return false; + } else { + // Common manager check: any valid signature will do + return true; + } + } + return false; } #ifdef CONFIG_KSU_DEBUG @@ -395,19 +394,19 @@ int ksu_debug_manager_uid = -1; static int set_expected_size(const char *val, const struct kernel_param *kp) { - int rv = param_set_uint(val, kp); - ksu_set_manager_uid(ksu_debug_manager_uid); - pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid); - return rv; + int rv = param_set_uint(val, kp); + ksu_set_manager_uid(ksu_debug_manager_uid); + pr_info("ksu_manager_uid set to %d\n", ksu_debug_manager_uid); + return rv; } static struct kernel_param_ops expected_size_ops = { - .set = set_expected_size, - .get = param_get_uint, + .set = set_expected_size, + .get = param_get_uint, }; module_param_cb(ksu_debug_manager_uid, &expected_size_ops, - &ksu_debug_manager_uid, S_IRUSR | S_IWUSR); + &ksu_debug_manager_uid, S_IRUSR | S_IWUSR); #endif diff --git a/kernel/app_profile.c b/kernel/app_profile.c new file mode 100644 index 0000000..9567e3d --- /dev/null +++ b/kernel/app_profile.c @@ -0,0 +1,302 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "objsec.h" + +#include "allowlist.h" +#include "app_profile.h" +#include "klog.h" // IWYU pragma: keep +#include "selinux/selinux.h" +#include "syscall_hook_manager.h" +#include "sucompat.h" +#include "sulog.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 + +static 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 disable_seccomp(void) +{ + assert_spin_locked(¤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 + +#ifdef CONFIG_SECCOMP + current->seccomp.mode = 0; + current->seccomp.filter = NULL; + atomic_set(¤t->seccomp.filter_count, 0); +#else +#endif +} + +void escape_with_root_profile(void) +{ + struct cred *cred; + struct task_struct *p = current; + struct task_struct *t; + + 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"); +#if __SULOG_GATE + ksu_sulog_report_su_grant(current_euid().val, NULL, "escape_to_root_failed"); +#endif + abort_creds(cred); + return; + } + + struct root_profile *profile = ksu_get_root_profile(cred->uid.val); + + 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)); + + // 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); + + commit_creds(cred); + + // 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(); + spin_unlock_irq(¤t->sighand->siglock); + + setup_selinux(profile->selinux_domain); +#if __SULOG_GATE + ksu_sulog_report_su_grant(current_euid().val, NULL, "escape_to_root"); +#endif + + for_each_thread (p, t) { + ksu_set_task_tracepoint_flag(t); + } +} + +#ifdef CONFIG_KSU_MANUAL_SU + +#include "ksud.h" + +#ifndef DEVPTS_SUPER_MAGIC +#define DEVPTS_SUPER_MAGIC 0x1cd1 +#endif + +static int __manual_su_handle_devpts(struct inode *inode) +{ + if (!current->mm) { + return 0; + } + + uid_t uid = current_uid().val; + if (uid % 100000 < 10000) { + // not untrusted_app, ignore it + return 0; + } + + if (likely(!ksu_is_allow_uid_for_current(uid))) + return 0; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 1, 0) || defined(KSU_OPTIONAL_SELINUX_INODE) + struct inode_security_struct *sec = selinux_inode(inode); +#else + struct inode_security_struct *sec = + (struct inode_security_struct *)inode->i_security; +#endif + if (ksu_file_sid && sec) + sec->sid = ksu_file_sid; + + return 0; +} + +static void disable_seccomp_for_task(struct task_struct *tsk) +{ + assert_spin_locked(&tsk->sighand->siglock); +#ifdef CONFIG_SECCOMP + if (tsk->seccomp.mode == SECCOMP_MODE_DISABLED && !tsk->seccomp.filter) + return; +#endif + clear_tsk_thread_flag(tsk, TIF_SECCOMP); +#ifdef CONFIG_SECCOMP + tsk->seccomp.mode = SECCOMP_MODE_DISABLED; + if (tsk->seccomp.filter) { +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) + seccomp_filter_release(tsk); +#else + put_seccomp_filter(tsk); + tsk->seccomp.filter = NULL; +#endif + } +#endif +} + +void escape_to_root_for_cmd_su(uid_t target_uid, pid_t target_pid) +{ + struct cred *newcreds; + struct task_struct *target_task; + unsigned long flags; + struct task_struct *p = current; + struct task_struct *t; + + pr_info("cmd_su: escape_to_root_for_cmd_su called for UID: %d, PID: %d\n", target_uid, target_pid); + + // Find target task by PID + rcu_read_lock(); + target_task = pid_task(find_vpid(target_pid), PIDTYPE_PID); + if (!target_task) { + rcu_read_unlock(); + pr_err("cmd_su: target task not found for PID: %d\n", target_pid); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "target_not_found"); +#endif + return; + } + get_task_struct(target_task); + rcu_read_unlock(); + + if (task_uid(target_task).val == 0) { + pr_warn("cmd_su: target task is already root, PID: %d\n", target_pid); + put_task_struct(target_task); + return; + } + + newcreds = prepare_kernel_cred(target_task); + if (newcreds == NULL) { + pr_err("cmd_su: failed to allocate new cred for PID: %d\n", target_pid); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "cred_alloc_failed"); +#endif + put_task_struct(target_task); + return; + } + + struct root_profile *profile = ksu_get_root_profile(target_uid); + + newcreds->uid.val = profile->uid; + newcreds->suid.val = profile->uid; + newcreds->euid.val = profile->uid; + newcreds->fsuid.val = profile->uid; + + newcreds->gid.val = profile->gid; + newcreds->fsgid.val = profile->gid; + newcreds->sgid.val = profile->gid; + newcreds->egid.val = profile->gid; + newcreds->securebits = 0; + + u64 cap_for_cmd_su = profile->capabilities.effective | CAP_DAC_READ_SEARCH | CAP_SETUID | CAP_SETGID; + memcpy(&newcreds->cap_effective, &cap_for_cmd_su, sizeof(newcreds->cap_effective)); + memcpy(&newcreds->cap_permitted, &profile->capabilities.effective, sizeof(newcreds->cap_permitted)); + memcpy(&newcreds->cap_bset, &profile->capabilities.effective, sizeof(newcreds->cap_bset)); + + setup_groups(profile, newcreds); + task_lock(target_task); + + const struct cred *old_creds = get_task_cred(target_task); + + rcu_assign_pointer(target_task->real_cred, newcreds); + rcu_assign_pointer(target_task->cred, get_cred(newcreds)); + task_unlock(target_task); + + if (target_task->sighand) { + spin_lock_irqsave(&target_task->sighand->siglock, flags); + disable_seccomp_for_task(target_task); + spin_unlock_irqrestore(&target_task->sighand->siglock, flags); + } + + setup_selinux(profile->selinux_domain); + put_cred(old_creds); + wake_up_process(target_task); + + if (target_task->signal->tty) { + struct inode *inode = target_task->signal->tty->driver_data; + if (inode && inode->i_sb->s_magic == DEVPTS_SUPER_MAGIC) { + __manual_su_handle_devpts(inode); + } + } + + put_task_struct(target_task); +#if __SULOG_GATE + ksu_sulog_report_su_grant(target_uid, "cmd_su", "manual_escalation"); +#endif + for_each_thread (p, t) { + ksu_set_task_tracepoint_flag(t); + } + pr_info("cmd_su: privilege escalation completed for UID: %d, PID: %d\n", target_uid, target_pid); +} +#endif diff --git a/kernel/app_profile.h b/kernel/app_profile.h new file mode 100644 index 0000000..871abb6 --- /dev/null +++ b/kernel/app_profile.h @@ -0,0 +1,70 @@ +#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_cmd_su(uid_t target_uid, pid_t target_pid); + +void disable_seccomp(void); + +#endif diff --git a/kernel/arch.h b/kernel/arch.h index eec38c2..ee2b16c 100644 --- a/kernel/arch.h +++ b/kernel/arch.h @@ -18,10 +18,8 @@ #define __PT_SP_REG sp #define __PT_IP_REG pc -#define PRCTL_SYMBOL "__arm64_sys_prctl" +#define REBOOT_SYMBOL "__arm64_sys_reboot" #define SYS_READ_SYMBOL "__arm64_sys_read" -#define SYS_NEWFSTATAT_SYMBOL "__arm64_sys_newfstatat" -#define SYS_FACCESSAT_SYMBOL "__arm64_sys_faccessat" #define SYS_EXECVE_SYMBOL "__arm64_sys_execve" #elif defined(__x86_64__) @@ -39,10 +37,8 @@ #define __PT_RC_REG ax #define __PT_SP_REG sp #define __PT_IP_REG ip -#define PRCTL_SYMBOL "__x64_sys_prctl" +#define REBOOT_SYMBOL "__x64_sys_reboot" #define SYS_READ_SYMBOL "__x64_sys_read" -#define SYS_NEWFSTATAT_SYMBOL "__x64_sys_newfstatat" -#define SYS_FACCESSAT_SYMBOL "__x64_sys_faccessat" #define SYS_EXECVE_SYMBOL "__x64_sys_execve" #else diff --git a/kernel/core_hook.c b/kernel/core_hook.c deleted file mode 100644 index 63c066f..0000000 --- a/kernel/core_hook.c +++ /dev/null @@ -1,1092 +0,0 @@ -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - -#ifdef MODULE -#include -#include -#include -#include -#include -#endif - -#include "allowlist.h" -#include "arch.h" -#include "core_hook.h" -#include "klog.h" // IWYU pragma: keep -#include "ksu.h" -#include "ksud.h" -#include "manager.h" -#include "selinux/selinux.h" -#include "throne_tracker.h" -#include "throne_comm.h" -#include "kernel_compat.h" - -#include "kpm/kpm.h" -#include "dynamic_manager.h" - -static bool ksu_module_mounted = false; - -extern int handle_sepolicy(unsigned long arg3, void __user *arg4); - -static bool ksu_su_compat_enabled = true; -extern void ksu_sucompat_init(); -extern void ksu_sucompat_exit(); - -static inline bool is_allow_su() -{ - if (is_manager()) { - // we are manager, allow! - return true; - } - return ksu_is_allow_uid(current_uid().val); -} - -static inline bool is_unsupported_app_uid(uid_t uid) -{ -#define LAST_APPLICATION_UID 19999 - uid_t appid = uid % 100000; - return appid > LAST_APPLICATION_UID; -} - -static struct group_info root_groups = { .usage = ATOMIC_INIT(2) }; - -static 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); -} - -static void disable_seccomp() -{ - assert_spin_locked(¤t->sighand->siglock); - // disable seccomp -#if defined(CONFIG_GENERIC_ENTRY) && \ - LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0) - current_thread_info()->syscall_work &= ~SYSCALL_WORK_SECCOMP; -#else - current_thread_info()->flags &= ~(TIF_SECCOMP | _TIF_SECCOMP); -#endif - -#ifdef CONFIG_SECCOMP - current->seccomp.mode = 0; - current->seccomp.filter = NULL; -#else -#endif -} - -void escape_to_root(void) -{ - struct cred *cred; - - 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"); - abort_creds(cred); - return; - } - - struct root_profile *profile = ksu_get_root_profile(cred->uid.val); - - 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)); - - // 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); - - commit_creds(cred); - - // 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(); - spin_unlock_irq(¤t->sighand->siglock); - - setup_selinux(profile->selinux_domain); -} - -int ksu_handle_rename(struct dentry *old_dentry, struct dentry *new_dentry) -{ - if (!current->mm) { - // skip kernel threads - return 0; - } - - if (current_uid().val != 1000) { - // skip non system uid - return 0; - } - - if (!old_dentry || !new_dentry) { - return 0; - } - - // /data/system/packages.list.tmp -> /data/system/packages.list - if (strcmp(new_dentry->d_iname, "packages.list")) { - return 0; - } - - char path[128]; - char *buf = dentry_path_raw(new_dentry, path, sizeof(path)); - if (IS_ERR(buf)) { - pr_err("dentry_path_raw failed.\n"); - return 0; - } - - if (!strstr(buf, "/system/packages.list")) { - return 0; - } - pr_info("renameat: %s -> %s, new path: %s\n", old_dentry->d_iname, - new_dentry->d_iname, buf); - - track_throne(); - - // Also request userspace scan for next time - ksu_request_userspace_scan(); - - return 0; -} - -#ifdef CONFIG_EXT4_FS -static void nuke_ext4_sysfs() { - struct path path; - int err = kern_path("/data/adb/modules", 0, &path); - if (err) { - pr_err("nuke path err: %d\n", err); - return; - } - - 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"); - return; - } - - ext4_unregister_sysfs(sb); - path_put(&path); -} -#else -static inline void nuke_ext4_sysfs() { } -#endif - -static bool is_system_bin_su() -{ - // YES in_execve becomes 0 when it succeeds. - if (!current->mm || current->in_execve) - return false; - - // quick af check - return (current->mm->exe_file && !strcmp(current->mm->exe_file->f_path.dentry->d_name.name, "su")); -} - -int ksu_handle_prctl(int option, unsigned long arg2, unsigned long arg3, - unsigned long arg4, unsigned long arg5) -{ - // if success, we modify the arg5 as result! - u32 *result = (u32 *)arg5; - u32 reply_ok = KERNEL_SU_OPTION; - - if (KERNEL_SU_OPTION != option) { - return 0; - } - - // TODO: find it in throne tracker! - uid_t current_uid_val = current_uid().val; - uid_t manager_uid = ksu_get_manager_uid(); - if (current_uid_val != manager_uid && - current_uid_val % 100000 == manager_uid) { - ksu_set_manager_uid(current_uid_val); - } - - bool from_root = 0 == current_uid().val; - bool from_manager = is_manager(); - - if (!from_root && !from_manager - && !(is_allow_su() && is_system_bin_su())) { - // only root or manager can access this interface - return 0; - } - -#ifdef CONFIG_KSU_DEBUG - pr_info("option: 0x%x, cmd: %ld\n", option, arg2); -#endif - - if (arg2 == CMD_BECOME_MANAGER) { - if (from_manager) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("become_manager: prctl reply error\n"); - } - return 0; - } - return 0; - } - - if (arg2 == CMD_GRANT_ROOT) { - if (is_allow_su()) { - pr_info("allow root for: %d\n", current_uid().val); - escape_to_root(); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("grant_root: prctl reply error\n"); - } - } - return 0; - } - - // Both root manager and root processes should be allowed to get version - if (arg2 == CMD_GET_VERSION) { - u32 version = KERNEL_SU_VERSION; - if (copy_to_user(arg3, &version, sizeof(version))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - u32 version_flags = 2; -#ifdef MODULE - version_flags |= 0x1; -#endif - if (arg4 && - copy_to_user(arg4, &version_flags, sizeof(version_flags))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - // Allow root manager to get full version strings - if (arg2 == CMD_GET_FULL_VERSION) { - char ksu_version_full[KSU_FULL_VERSION_STRING] = {0}; -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) - strscpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING); -#else - strlcpy(ksu_version_full, KSU_VERSION_FULL, KSU_FULL_VERSION_STRING); -#endif - if (copy_to_user((void __user *)arg3, ksu_version_full, KSU_FULL_VERSION_STRING)) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - return -EFAULT; - } - return 0; - } - - // Allow the root manager to configure dynamic manageratures - if (arg2 == CMD_DYNAMIC_MANAGER) { - if (!from_root && !from_manager) { - return 0; - } - - struct dynamic_manager_user_config config; - - if (copy_from_user(&config, (void __user *)arg3, sizeof(config))) { - pr_err("copy dynamic manager config failed\n"); - return 0; - } - - int ret = ksu_handle_dynamic_manager(&config); - - if (ret == 0 && config.operation == DYNAMIC_MANAGER_OP_GET) { - if (copy_to_user((void __user *)arg3, &config, sizeof(config))) { - pr_err("copy dynamic manager config back failed\n"); - return 0; - } - } - - if (ret == 0) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("dynamic_manager: prctl reply error\n"); - } - } - return 0; - } - - // Allow root manager to get active managers - if (arg2 == CMD_GET_MANAGERS) { - if (!from_root && !from_manager) { - return 0; - } - - struct manager_list_info manager_info; - int ret = ksu_get_active_managers(&manager_info); - - if (ret == 0) { - if (copy_to_user((void __user *)arg3, &manager_info, sizeof(manager_info))) { - pr_err("copy manager list failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("get_managers: prctl reply error\n"); - } - } - return 0; - } - - if (arg2 == CMD_REPORT_EVENT) { - if (!from_root) { - return 0; - } - switch (arg3) { - case EVENT_POST_FS_DATA: { - static bool post_fs_data_lock = false; - if (!post_fs_data_lock) { - post_fs_data_lock = true; - pr_info("post-fs-data triggered\n"); - on_post_fs_data(); - // Initialize throne communication - ksu_throne_comm_init(); - // Initializing Dynamic Signatures - ksu_dynamic_manager_init(); - pr_info("Dynamic sign config loaded during post-fs-data\n"); - } - break; - } - case EVENT_BOOT_COMPLETED: { - static bool boot_complete_lock = false; - if (!boot_complete_lock) { - boot_complete_lock = true; - pr_info("boot_complete triggered\n"); - } - break; - } - case EVENT_MODULE_MOUNTED: { - ksu_module_mounted = true; - pr_info("module mounted!\n"); - nuke_ext4_sysfs(); - break; - } - default: - break; - } - return 0; - } - - if (arg2 == CMD_SET_SEPOLICY) { - if (!from_root) { - return 0; - } - if (!handle_sepolicy(arg3, arg4)) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("sepolicy: prctl reply error\n"); - } - } - - return 0; - } - - if (arg2 == CMD_CHECK_SAFEMODE) { - if (ksu_is_safe_mode()) { - pr_warn("safemode enabled!\n"); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("safemode: prctl reply error\n"); - } - } - return 0; - } - - if (arg2 == CMD_GET_ALLOW_LIST || arg2 == CMD_GET_DENY_LIST) { - u32 array[128]; - u32 array_length; - bool success = ksu_get_allow_list(array, &array_length, - arg2 == CMD_GET_ALLOW_LIST); - if (success) { - if (!copy_to_user(arg4, &array_length, - sizeof(array_length)) && - !copy_to_user(arg3, array, - sizeof(u32) * array_length)) { - if (copy_to_user(result, &reply_ok, - sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", - arg2); - } - } else { - pr_err("prctl copy allowlist error\n"); - } - } - return 0; - } - - if (arg2 == CMD_UID_GRANTED_ROOT || arg2 == CMD_UID_SHOULD_UMOUNT) { - uid_t target_uid = (uid_t)arg3; - bool allow = false; - if (arg2 == CMD_UID_GRANTED_ROOT) { - allow = ksu_is_allow_uid(target_uid); - } else if (arg2 == CMD_UID_SHOULD_UMOUNT) { - allow = ksu_uid_should_umount(target_uid); - } else { - pr_err("unknown cmd: %lu\n", arg2); - } - if (!copy_to_user(arg4, &allow, sizeof(allow))) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } else { - pr_err("prctl copy err, cmd: %lu\n", arg2); - } - return 0; - } - - if (arg2 == CMD_ENABLE_SU) { - bool enabled = (arg3 != 0); - if (enabled == ksu_su_compat_enabled) { - pr_info("cmd enable su but no need to change.\n"); - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) {// return the reply_ok directly - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - if (enabled) { - ksu_sucompat_init(); - } else { - ksu_sucompat_exit(); - } - ksu_su_compat_enabled = enabled; - - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - - return 0; - } - - #ifdef CONFIG_KPM - // ADD: 添加KPM模块控制 - if(sukisu_is_kpm_control_code(arg2)) { - int res; - - pr_info("KPM: calling before arg2=%d\n", (int) arg2); - - res = sukisu_handle_kpm(arg2, arg3, arg4, arg5); - - return 0; - } - #endif - - if (arg2 == CMD_ENABLE_KPM) { - bool KPM_Enabled = IS_ENABLED(CONFIG_KPM); - if (copy_to_user((void __user *)arg3, &KPM_Enabled, sizeof(KPM_Enabled))) - pr_info("KPM: copy_to_user() failed\n"); - return 0; - } - - // Checking hook usage - if (arg2 == CMD_HOOK_TYPE) { - const char *hook_type = "Kprobes"; -#if defined(CONFIG_KSU_TRACEPOINT_HOOK) - hook_type = "Tracepoint"; -#elif defined(CONFIG_KSU_MANUAL_HOOK) - hook_type = "Manual"; -#endif - - size_t len = strlen(hook_type) + 1; - if (copy_to_user((void __user *)arg3, hook_type, len)) { - pr_err("hook_type: copy_to_user failed\n"); - return 0; - } - - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("hook_type: prctl reply error\n"); - } - return 0; - } - - - // all other cmds are for 'root manager' - if (!from_manager) { - return 0; - } - - // we are already manager - if (arg2 == CMD_GET_APP_PROFILE) { - struct app_profile profile; - if (copy_from_user(&profile, arg3, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - - bool success = ksu_get_app_profile(&profile); - if (success) { - if (copy_to_user(arg3, &profile, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } - return 0; - } - - if (arg2 == CMD_SET_APP_PROFILE) { - struct app_profile profile; - if (copy_from_user(&profile, arg3, sizeof(profile))) { - pr_err("copy profile failed\n"); - return 0; - } - - // todo: validate the params - if (ksu_set_app_profile(&profile, true)) { - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - } - return 0; - } - - if (arg2 == CMD_IS_SU_ENABLED) { - if (copy_to_user(arg3, &ksu_su_compat_enabled, - sizeof(ksu_su_compat_enabled))) { - pr_err("copy su compat failed\n"); - return 0; - } - if (copy_to_user(result, &reply_ok, sizeof(reply_ok))) { - pr_err("prctl reply error, cmd: %lu\n", arg2); - } - return 0; - } - - return 0; -} - -static bool is_non_appuid(kuid_t uid) -{ -#define PER_USER_RANGE 100000 -#define FIRST_APPLICATION_UID 10000 - - uid_t appid = uid.val % PER_USER_RANGE; - return appid < FIRST_APPLICATION_UID; -} - -static bool should_umount(struct path *path) -{ - if (!path) { - return false; - } - - if (current->nsproxy->mnt_ns == init_nsproxy.mnt_ns) { - pr_info("ignore global mnt namespace process: %d\n", - current_uid().val); - return false; - } - - if (path->mnt && path->mnt->mnt_sb && path->mnt->mnt_sb->s_type) { - const char *fstype = path->mnt->mnt_sb->s_type->name; - return strcmp(fstype, "overlay") == 0; - } - return false; -} - -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, bool check_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; - } - - // we are only interest in some specific mounts - if (check_mnt && !should_umount(&path)) { - path_put(&path); - return; - } - - ksu_umount_mnt(&path, flags); -} - -int ksu_handle_setuid(struct cred *new, const struct cred *old) -{ - // this hook is used for umounting overlayfs for some uid, if there isn't any module mounted, just ignore it! - if (!ksu_module_mounted) { - return 0; - } - - if (!new || !old) { - return 0; - } - - kuid_t new_uid = new->uid; - kuid_t old_uid = old->uid; - - if (0 != old_uid.val) { - // old process is not root, ignore it. - return 0; - } - - if (is_non_appuid(new_uid)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle setuid ignore non application uid: %d\n", new_uid.val); -#endif - return 0; - } - - // isolated process may be directly forked from zygote, always unmount - if (is_unsupported_app_uid(new_uid.val)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle umount for unsupported application uid: %d\n", new_uid.val); -#endif - goto do_umount; - } - - if (ksu_is_allow_uid(new_uid.val)) { -#ifdef CONFIG_KSU_DEBUG - pr_info("handle setuid ignore allowed application: %d\n", new_uid.val); -#endif - return 0; - } - - if (!ksu_uid_should_umount(new_uid.val)) { - return 0; - } else { -#ifdef CONFIG_KSU_DEBUG - pr_info("uid: %d should not umount!\n", current_uid().val); -#endif - } - -do_umount: - // 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! - if (!is_zygote(old->security)) { - pr_info("handle umount ignore non zygote child: %d\n", - current->pid); - return 0; - } -#ifdef CONFIG_KSU_DEBUG - // umount the target mnt - pr_info("handle umount for uid: %d, pid: %d\n", new_uid.val, - current->pid); -#endif - - // fixme: use `collect_mounts` and `iterate_mount` to iterate all mountpoint and - // filter the mountpoint whose target is `/data/adb` - try_umount("/system", true, 0); - try_umount("/vendor", true, 0); - try_umount("/product", true, 0); - try_umount("/system_ext", true, 0); - try_umount("/data/adb/modules", false, MNT_DETACH); - - // try umount ksu temp path - try_umount("/debug_ramdisk", false, MNT_DETACH); - - return 0; -} - -// Init functons - -static int handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int option = (int)PT_REGS_PARM1(real_regs); - unsigned long arg2 = (unsigned long)PT_REGS_PARM2(real_regs); - unsigned long arg3 = (unsigned long)PT_REGS_PARM3(real_regs); - // PRCTL_SYMBOL is the arch-specificed one, which receive raw pt_regs from syscall - unsigned long arg4 = (unsigned long)PT_REGS_SYSCALL_PARM4(real_regs); - unsigned long arg5 = (unsigned long)PT_REGS_PARM5(real_regs); - - return ksu_handle_prctl(option, arg2, arg3, arg4, arg5); -} - -static struct kprobe prctl_kp = { - .symbol_name = PRCTL_SYMBOL, - .pre_handler = handler_pre, -}; - -static int renameat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ -#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 12, 0) - // https://elixir.bootlin.com/linux/v5.12-rc1/source/include/linux/fs.h - struct renamedata *rd = PT_REGS_PARM1(regs); - struct dentry *old_entry = rd->old_dentry; - struct dentry *new_entry = rd->new_dentry; -#else - struct dentry *old_entry = (struct dentry *)PT_REGS_PARM2(regs); - struct dentry *new_entry = (struct dentry *)PT_REGS_CCALL_PARM4(regs); -#endif - - return ksu_handle_rename(old_entry, new_entry); -} - -static struct kprobe renameat_kp = { - .symbol_name = "vfs_rename", - .pre_handler = renameat_handler_pre, -}; - -__maybe_unused int ksu_kprobe_init(void) -{ - int rc = 0; - rc = register_kprobe(&prctl_kp); - - if (rc) { - pr_info("prctl kprobe failed: %d.\n", rc); - return rc; - } - - rc = register_kprobe(&renameat_kp); - pr_info("renameat kp: %d\n", rc); - - return rc; -} - -__maybe_unused int ksu_kprobe_exit(void) -{ - unregister_kprobe(&prctl_kp); - unregister_kprobe(&renameat_kp); - return 0; -} - -#ifndef DEVPTS_SUPER_MAGIC -#define DEVPTS_SUPER_MAGIC 0x1cd1 -#endif - -extern int __ksu_handle_devpts(struct inode *inode); // sucompat.c - -int ksu_inode_permission(struct inode *inode, int mask) -{ - if (inode && inode->i_sb - && unlikely(inode->i_sb->s_magic == DEVPTS_SUPER_MAGIC)) { - //pr_info("%s: handling devpts for: %s \n", __func__, current->comm); - __ksu_handle_devpts(inode); - } - return 0; -} - -#ifdef CONFIG_COMPAT -bool ksu_is_compat __read_mostly = false; -#endif - -int ksu_bprm_check(struct linux_binprm *bprm) -{ - char *filename = (char *)bprm->filename; - - if (likely(!ksu_execveat_hook)) - return 0; - -#ifdef CONFIG_COMPAT - static bool compat_check_done __read_mostly = false; - if ( unlikely(!compat_check_done) && unlikely(!strcmp(filename, "/data/adb/ksud")) - && !memcmp(bprm->buf, "\x7f\x45\x4c\x46", 4) ) { - if (bprm->buf[4] == 0x01 ) - ksu_is_compat = true; - - pr_info("%s: %s ELF magic found! ksu_is_compat: %d \n", __func__, filename, ksu_is_compat); - compat_check_done = true; - } -#endif - - ksu_handle_pre_ksud(filename); - - return 0; - -} - -static int ksu_task_prctl(int option, unsigned long arg2, unsigned long arg3, - unsigned long arg4, unsigned long arg5) -{ - ksu_handle_prctl(option, arg2, arg3, arg4, arg5); - return -ENOSYS; -} - -static int ksu_inode_rename(struct inode *old_inode, struct dentry *old_dentry, - struct inode *new_inode, struct dentry *new_dentry) -{ - return ksu_handle_rename(old_dentry, new_dentry); -} - -static int ksu_task_fix_setuid(struct cred *new, const struct cred *old, - int flags) -{ - return ksu_handle_setuid(new, old); -} - -#ifndef MODULE -static struct security_hook_list ksu_hooks[] = { - LSM_HOOK_INIT(task_prctl, ksu_task_prctl), - LSM_HOOK_INIT(inode_rename, ksu_inode_rename), - LSM_HOOK_INIT(task_fix_setuid, ksu_task_fix_setuid), - LSM_HOOK_INIT(inode_permission, ksu_inode_permission), -#ifndef CONFIG_KSU_KPROBES_HOOK - LSM_HOOK_INIT(bprm_check_security, ksu_bprm_check), -#endif -}; - -void __init ksu_lsm_hook_init(void) -{ - security_add_hooks(ksu_hooks, ARRAY_SIZE(ksu_hooks), "ksu"); -} - -#else -static int override_security_head(void *head, const void *new_head, size_t len) -{ - unsigned long base = (unsigned long)head & PAGE_MASK; - unsigned long offset = offset_in_page(head); - - // this is impossible for our case because the page alignment - // but be careful for other cases! - BUG_ON(offset + len > PAGE_SIZE); - struct page *page = phys_to_page(__pa(base)); - if (!page) { - return -EFAULT; - } - - void *addr = vmap(&page, 1, VM_MAP, PAGE_KERNEL); - if (!addr) { - return -ENOMEM; - } - local_irq_disable(); - memcpy(addr + offset, new_head, len); - local_irq_enable(); - vunmap(addr); - return 0; -} - -static void free_security_hook_list(struct hlist_head *head) -{ - struct hlist_node *temp; - struct security_hook_list *entry; - - if (!head) - return; - - hlist_for_each_entry_safe (entry, temp, head, list) { - hlist_del(&entry->list); - kfree(entry); - } - - kfree(head); -} - -struct hlist_head *copy_security_hlist(struct hlist_head *orig) -{ - struct hlist_head *new_head = kmalloc(sizeof(*new_head), GFP_KERNEL); - if (!new_head) - return NULL; - - INIT_HLIST_HEAD(new_head); - - struct security_hook_list *entry; - struct security_hook_list *new_entry; - - hlist_for_each_entry (entry, orig, list) { - new_entry = kmalloc(sizeof(*new_entry), GFP_KERNEL); - if (!new_entry) { - free_security_hook_list(new_head); - return NULL; - } - - *new_entry = *entry; - - hlist_add_tail_rcu(&new_entry->list, new_head); - } - - return new_head; -} - -#define LSM_SEARCH_MAX 180 // This should be enough to iterate -static void *find_head_addr(void *security_ptr, int *index) -{ - if (!security_ptr) { - return NULL; - } - struct hlist_head *head_start = - (struct hlist_head *)&security_hook_heads; - - for (int i = 0; i < LSM_SEARCH_MAX; i++) { - struct hlist_head *head = head_start + i; - struct security_hook_list *pos; - hlist_for_each_entry (pos, head, list) { - if (pos->hook.capget == security_ptr) { - if (index) { - *index = i; - } - return head; - } - } - } - - return NULL; -} - -#define GET_SYMBOL_ADDR(sym) \ - ({ \ - void *addr = kallsyms_lookup_name(#sym ".cfi_jt"); \ - if (!addr) { \ - addr = kallsyms_lookup_name(#sym); \ - } \ - addr; \ - }) - -#define KSU_LSM_HOOK_HACK_INIT(head_ptr, name, func) \ - do { \ - static struct security_hook_list hook = { \ - .hook = { .name = func } \ - }; \ - hook.head = head_ptr; \ - hook.lsm = "ksu"; \ - struct hlist_head *new_head = copy_security_hlist(hook.head); \ - if (!new_head) { \ - pr_err("Failed to copy security list: %s\n", #name); \ - break; \ - } \ - hlist_add_tail_rcu(&hook.list, new_head); \ - if (override_security_head(hook.head, new_head, \ - sizeof(*new_head))) { \ - free_security_hook_list(new_head); \ - pr_err("Failed to hack lsm for: %s\n", #name); \ - } \ - } while (0) - -void __init ksu_lsm_hook_init(void) -{ - void *cap_prctl = GET_SYMBOL_ADDR(cap_task_prctl); - void *prctl_head = find_head_addr(cap_prctl, NULL); - if (prctl_head) { - if (prctl_head != &security_hook_heads.task_prctl) { - pr_warn("prctl's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(prctl_head, task_prctl, ksu_task_prctl); - } else { - pr_warn("Failed to find task_prctl!\n"); - } - - int inode_killpriv_index = -1; - void *cap_killpriv = GET_SYMBOL_ADDR(cap_inode_killpriv); - find_head_addr(cap_killpriv, &inode_killpriv_index); - if (inode_killpriv_index < 0) { - pr_warn("Failed to find inode_rename, use kprobe instead!\n"); - register_kprobe(&renameat_kp); - } else { - int inode_rename_index = inode_killpriv_index + - &security_hook_heads.inode_rename - - &security_hook_heads.inode_killpriv; - struct hlist_head *head_start = - (struct hlist_head *)&security_hook_heads; - void *inode_rename_head = head_start + inode_rename_index; - if (inode_rename_head != &security_hook_heads.inode_rename) { - pr_warn("inode_rename's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(inode_rename_head, inode_rename, - ksu_inode_rename); - } - void *cap_setuid = GET_SYMBOL_ADDR(cap_task_fix_setuid); - void *setuid_head = find_head_addr(cap_setuid, NULL); - if (setuid_head) { - if (setuid_head != &security_hook_heads.task_fix_setuid) { - pr_warn("setuid's address has shifted!\n"); - } - KSU_LSM_HOOK_HACK_INIT(setuid_head, task_fix_setuid, - ksu_task_fix_setuid); - } else { - pr_warn("Failed to find task_fix_setuid!\n"); - } - smp_mb(); -} -#endif - -void __init ksu_core_init(void) -{ - ksu_lsm_hook_init(); -} - -void ksu_core_exit(void) -{ - ksu_throne_comm_exit(); -#ifdef CONFIG_KPROBE - pr_info("ksu_core_kprobe_exit\n"); - // we dont use this now - // ksu_kprobe_exit(); -#endif -} diff --git a/kernel/core_hook.h b/kernel/core_hook.h deleted file mode 100644 index 6ed328a..0000000 --- a/kernel/core_hook.h +++ /dev/null @@ -1,10 +0,0 @@ -#ifndef __KSU_H_KSU_CORE -#define __KSU_H_KSU_CORE - -#include -#include "apk_sign.h" - -void __init ksu_core_init(void); -void ksu_core_exit(void); - -#endif diff --git a/kernel/dynamic_manager.c b/kernel/dynamic_manager.c index 6f34d19..96bc710 100644 --- a/kernel/dynamic_manager.c +++ b/kernel/dynamic_manager.c @@ -17,7 +17,6 @@ #include "dynamic_manager.h" #include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" #include "manager.h" #define MAX_MANAGERS 2 @@ -233,23 +232,23 @@ static void do_save_dynamic_manager(struct work_struct *work) return; } - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (IS_ERR(fp)) { pr_err("save_dynamic_manager create file failed: %ld\n", PTR_ERR(fp)); return; } - if (ksu_kernel_write_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { + if (kernel_write(fp, &magic, sizeof(magic), &off) != sizeof(magic)) { pr_err("save_dynamic_manager write magic failed.\n"); goto exit; } - if (ksu_kernel_write_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + if (kernel_write(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("save_dynamic_manager write version failed.\n"); goto exit; } - if (ksu_kernel_write_compat(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) { + if (kernel_write(fp, &config_to_save, sizeof(config_to_save), &off) != sizeof(config_to_save)) { pr_err("save_dynamic_manager write config failed.\n"); goto exit; } @@ -271,7 +270,7 @@ static void do_load_dynamic_manager(struct work_struct *work) unsigned long flags; int i; - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_RDONLY, 0); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_RDONLY, 0); if (IS_ERR(fp)) { if (PTR_ERR(fp) == -ENOENT) { pr_info("No saved dynamic manager config found\n"); @@ -281,20 +280,20 @@ static void do_load_dynamic_manager(struct work_struct *work) return; } - if (ksu_kernel_read_compat(fp, &magic, sizeof(magic), &off) != sizeof(magic) || + if (kernel_read(fp, &magic, sizeof(magic), &off) != sizeof(magic) || magic != DYNAMIC_MANAGER_FILE_MAGIC) { pr_err("dynamic manager file invalid magic: %x!\n", magic); goto exit; } - if (ksu_kernel_read_compat(fp, &version, sizeof(version), &off) != sizeof(version)) { + if (kernel_read(fp, &version, sizeof(version), &off) != sizeof(version)) { pr_err("dynamic manager read version failed\n"); goto exit; } pr_info("dynamic manager file version: %d\n", version); - ret = ksu_kernel_read_compat(fp, &loaded_config, sizeof(loaded_config), &off); + ret = kernel_read(fp, &loaded_config, sizeof(loaded_config), &off); if (ret <= 0) { pr_info("load_dynamic_manager read err: %zd\n", ret); goto exit; @@ -348,14 +347,14 @@ static void do_clear_dynamic_manager(struct work_struct *work) memset(zero_buffer, 0, sizeof(zero_buffer)); - fp = ksu_filp_open_compat(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); + fp = filp_open(KERNEL_SU_DYNAMIC_MANAGER, O_WRONLY | O_CREAT | O_TRUNC, 0644); if (IS_ERR(fp)) { pr_err("clear_dynamic_manager create file failed: %ld\n", PTR_ERR(fp)); return; } // Write null bytes to overwrite the file content - if (ksu_kernel_write_compat(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) { + if (kernel_write(fp, zero_buffer, sizeof(zero_buffer), &off) != sizeof(zero_buffer)) { pr_err("clear_dynamic_manager write null bytes failed.\n"); } else { pr_info("Dynamic sign config file cleared successfully\n"); diff --git a/kernel/feature.c b/kernel/feature.c new file mode 100644 index 0000000..99277b5 --- /dev/null +++ b/kernel/feature.c @@ -0,0 +1,173 @@ +#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"); +} diff --git a/kernel/feature.h b/kernel/feature.h new file mode 100644 index 0000000..d9cbc92 --- /dev/null +++ b/kernel/feature.h @@ -0,0 +1,36 @@ +#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_ENHANCED_SECURITY = 2, + + 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 diff --git a/kernel/file_wrapper.c b/kernel/file_wrapper.c new file mode 100644 index 0000000..d73cf5d --- /dev/null +++ b/kernel/file_wrapper.c @@ -0,0 +1,341 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" // IWYU pragma: keep +#include "selinux/selinux.h" + +#include "file_wrapper.h" + +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 unsigned long mmap_supported_flags {} + +static int ksu_wrapper_open(struct inode *ino, struct file *fp) { + struct ksu_file_wrapper* data = fp->private_data; + struct file* orig = data->orig; + struct inode *orig_ino = file_inode(orig); + return orig->f_op->open(orig_ino, orig); +} + +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); + } +} + +static ssize_t ksu_wrapper_copy_file_range(struct file *f1, loff_t off1, struct file *f2, + loff_t off2, size_t sz, unsigned int flags) { + // TODO: determine which file to use + struct ksu_file_wrapper* data = f1->private_data; + struct file* orig = data->orig; + if (orig->f_op->copy_file_range) { + return orig->f_op->copy_file_range(orig, off1, f2, off2, sz, flags); + } + return -EINVAL; +} + +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) { + // TODO: determine which file to use + struct ksu_file_wrapper* data = file_in->private_data; + struct file* orig = data->orig; + if (orig->f_op->remap_file_range) { + return orig->f_op->remap_file_range(orig, pos_in, file_out, pos_out, len, remap_flags); + } + return -EINVAL; +} + +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 int ksu_wrapper_release(struct inode *inode, struct file *filp) { + ksu_delete_file_wrapper(filp->private_data); + return 0; +} + +struct ksu_file_wrapper* ksu_create_file_wrapper(struct file* fp) { + struct ksu_file_wrapper* p = kcalloc(sizeof(struct ksu_file_wrapper), 1, GFP_KERNEL); + if (!p) { + return NULL; + } + + 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.open = fp->f_op->open ? ksu_wrapper_open : NULL; + 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; +} + +void ksu_delete_file_wrapper(struct ksu_file_wrapper* data) { + fput((struct file*) data->orig); + kfree(data); +} \ No newline at end of file diff --git a/kernel/file_wrapper.h b/kernel/file_wrapper.h new file mode 100644 index 0000000..421e20e --- /dev/null +++ b/kernel/file_wrapper.h @@ -0,0 +1,14 @@ +#ifndef KSU_FILE_WRAPPER_H +#define KSU_FILE_WRAPPER_H + +#include +#include + +struct ksu_file_wrapper { + struct file* orig; + struct file_operations ops; +}; + +struct ksu_file_wrapper* ksu_create_file_wrapper(struct file* fp); +void ksu_delete_file_wrapper(struct ksu_file_wrapper* data); +#endif // KSU_FILE_WRAPPER_H \ No newline at end of file diff --git a/kernel/include/ksu_hook.h b/kernel/include/ksu_hook.h deleted file mode 100644 index ea0b04d..0000000 --- a/kernel/include/ksu_hook.h +++ /dev/null @@ -1,28 +0,0 @@ -#ifndef __KSU_H_KSHOOK -#define __KSU_H_KSHOOK - -#include -#include - -// For sucompat - -int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, - int *flags); - -int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); - -// For ksud - -int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, - size_t *count_ptr, loff_t **pos); - -// For ksud and sucompat - -int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, - void *envp, int *flags); - -// For volume button -int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, - int *value); - -#endif diff --git a/kernel/kernel_compat.c b/kernel/kernel_compat.c deleted file mode 100644 index 3fb7ecd..0000000 --- a/kernel/kernel_compat.c +++ /dev/null @@ -1,94 +0,0 @@ -#include -#include -#include -#include -#include -#include "klog.h" // IWYU pragma: keep -#include "kernel_compat.h" - -extern struct task_struct init_task; - -// mnt_ns context switch for environment that android_init->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns, such as WSA -struct ksu_ns_fs_saved { - struct nsproxy *ns; - struct fs_struct *fs; -}; - -static void ksu_save_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved) -{ - ns_fs_saved->ns = current->nsproxy; - ns_fs_saved->fs = current->fs; -} - -static void ksu_load_ns_fs(struct ksu_ns_fs_saved *ns_fs_saved) -{ - current->nsproxy = ns_fs_saved->ns; - current->fs = ns_fs_saved->fs; -} - -static bool android_context_saved_checked = false; -static bool android_context_saved_enabled = false; -static struct ksu_ns_fs_saved android_context_saved; - -void ksu_android_ns_fs_check() -{ - if (android_context_saved_checked) - return; - android_context_saved_checked = true; - task_lock(current); - if (current->nsproxy && current->fs && - current->nsproxy->mnt_ns != init_task.nsproxy->mnt_ns) { - android_context_saved_enabled = true; -#ifdef CONFIG_KSU_DEBUG - pr_info("android context saved enabled due to init mnt_ns(%p) != android mnt_ns(%p)\n", - current->nsproxy->mnt_ns, init_task.nsproxy->mnt_ns); -#endif - ksu_save_ns_fs(&android_context_saved); - } else { - pr_info("android context saved disabled\n"); - } - task_unlock(current); -} - -struct file *ksu_filp_open_compat(const char *filename, int flags, umode_t mode) -{ - // switch mnt_ns even if current is not wq_worker, to ensure what we open is the correct file in android mnt_ns, rather than user created mnt_ns - struct ksu_ns_fs_saved saved; - if (android_context_saved_enabled) { -#ifdef CONFIG_KSU_DEBUG - pr_info("start switch current nsproxy and fs to android context\n"); -#endif - task_lock(current); - ksu_save_ns_fs(&saved); - ksu_load_ns_fs(&android_context_saved); - task_unlock(current); - } - struct file *fp = filp_open(filename, flags, mode); - if (android_context_saved_enabled) { - task_lock(current); - ksu_load_ns_fs(&saved); - task_unlock(current); -#ifdef CONFIG_KSU_DEBUG - pr_info("switch current nsproxy and fs back to saved successfully\n"); -#endif - } - return fp; -} - -ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count, - loff_t *pos) -{ - return kernel_read(p, buf, count, pos); -} - -ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, size_t count, - loff_t *pos) -{ - return kernel_write(p, buf, count, pos); -} - -long ksu_strncpy_from_user_nofault(char *dst, const void __user *unsafe_addr, - long count) -{ - return strncpy_from_user_nofault(dst, unsafe_addr, count); -} diff --git a/kernel/kernel_compat.h b/kernel/kernel_compat.h index ed02888..14e1cb2 100644 --- a/kernel/kernel_compat.h +++ b/kernel/kernel_compat.h @@ -3,63 +3,7 @@ #include #include -#include "ss/policydb.h" -#include "linux/key.h" -/** - * list_count_nodes - count the number of nodes in a list - * @head: the head of the list - * - * This function iterates over the list starting from @head and counts - * the number of nodes in the list. It does not modify the list. - * - * Context: Any context. The function is safe to call in any context, - * including interrupt context, as it does not sleep or allocate - * memory. - * - * Return: the number of nodes in the list (excluding the head) - */ -#if LINUX_VERSION_CODE < KERNEL_VERSION(6, 6, 0) -static inline __maybe_unused size_t list_count_nodes(const struct list_head *head) -{ - const struct list_head *pos; - size_t count = 0; - - if (!head) - return 0; - - list_for_each(pos, head) - count++; - - return count; -} -#endif - -/* - * Adapt to Huawei HISI kernel without affecting other kernels , - * Huawei Hisi Kernel EBITMAP Enable or Disable Flag , - * From ss/ebitmap.h - */ -#if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 9, 0)) && \ - (LINUX_VERSION_CODE < KERNEL_VERSION(4, 10, 0)) || \ - (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 14, 0)) && \ - (LINUX_VERSION_CODE < KERNEL_VERSION(4, 15, 0)) -#ifdef HISI_SELINUX_EBITMAP_RO -#define CONFIG_IS_HW_HISI -#endif -#endif - -extern long ksu_strncpy_from_user_nofault(char *dst, - const void __user *unsafe_addr, - long count); - -extern void ksu_android_ns_fs_check(); -extern struct file *ksu_filp_open_compat(const char *filename, int flags, - umode_t mode); -extern ssize_t ksu_kernel_read_compat(struct file *p, void *buf, size_t count, - loff_t *pos); -extern ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, - size_t count, loff_t *pos); /* * ksu_copy_from_user_retry * try nofault copy first, if it fails, try with plain @@ -67,14 +11,14 @@ extern ssize_t ksu_kernel_write_compat(struct file *p, const void *buf, * 0 = success */ static long ksu_copy_from_user_retry(void *to, - const void __user *from, unsigned long count) + const void __user *from, unsigned long count) { - long ret = copy_from_user_nofault(to, from, count); - if (likely(!ret)) - return ret; + long ret = copy_from_user_nofault(to, from, count); + if (likely(!ret)) + return ret; - // we faulted! fallback to slow path - return copy_from_user(to, from, count); + // we faulted! fallback to slow path + return copy_from_user(to, from, count); } #endif diff --git a/kernel/kernel_umount.c b/kernel/kernel_umount.c new file mode 100644 index 0000000..d714a22 --- /dev/null +++ b/kernel/kernel_umount.c @@ -0,0 +1,193 @@ +#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 "umount_manager.h" +#include "sulog.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, +}; + +static bool should_umount(struct path *path) +{ + if (!path) { + return false; + } + + if (current->nsproxy->mnt_ns == init_nsproxy.mnt_ns) { + pr_info("ignore global mnt namespace process: %d\n", current_uid().val); + return false; + } + + if (path->mnt && path->mnt->mnt_sb && path->mnt->mnt_sb->s_type) { + const char *fstype = path->mnt->mnt_sb->s_type->name; + return strcmp(fstype, "overlay") == 0; + } + return false; +} + +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); + } +} + +void try_umount(const char *mnt, bool check_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; + } + + // we are only interest in some specific mounts + if (check_mnt && !should_umount(&path)) { + path_put(&path); + return; + } + + ksu_umount_mnt(&path, flags); +} + +struct umount_tw { + struct callback_head cb; + const struct cred *old_cred; +}; + +static void umount_tw_func(struct callback_head *cb) +{ + struct umount_tw *tw = container_of(cb, struct umount_tw, cb); + const struct cred *saved = NULL; + if (tw->old_cred) { + saved = override_creds(tw->old_cred); + } + + // fixme: use `collect_mounts` and `iterate_mount` to iterate all mountpoint and + // filter the mountpoint whose target is `/data/adb` + ksu_umount_manager_execute_all(tw->old_cred); + + if (saved) + revert_creds(saved); + + if (tw->old_cred) + put_cred(tw->old_cred); + + kfree(tw); +} + +int ksu_handle_umount(uid_t old_uid, uid_t new_uid) +{ + struct umount_tw *tw; + + // this hook is used for umounting overlayfs for some uid, if there isn't any module mounted, just ignore it! + if (!ksu_module_mounted) { + return 0; + } + + if (!ksu_kernel_umount_enabled) { + return 0; + } + + // FIXME: isolated process which directly forks from zygote is not handled + if (!is_appuid(new_uid)) { + return 0; + } + + if (!ksu_uid_should_umount(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! + 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; + } +#if __SULOG_GATE + ksu_sulog_report_syscall(new_uid, NULL, "setuid", NULL); +#endif + // umount the target mnt + pr_info("handle umount for uid: %d, pid: %d\n", new_uid, current->pid); + + tw = kmalloc(sizeof(*tw), GFP_ATOMIC); + if (!tw) + return 0; + + tw->old_cred = get_current_cred(); + tw->cb.func = umount_tw_func; + + int err = task_work_add(current, &tw->cb, TWA_RESUME); + if (err) { + if (tw->old_cred) { + put_cred(tw->old_cred); + } + kfree(tw); + pr_warn("unmount add task_work failed\n"); + } + + return 0; +} + +void ksu_kernel_umount_init(void) +{ + int rc = 0; + rc = ksu_umount_manager_init(); + if (rc) { + pr_err("Failed to initialize umount manager: %d\n", rc); + } + 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); +} \ No newline at end of file diff --git a/kernel/kernel_umount.h b/kernel/kernel_umount.h new file mode 100644 index 0000000..68d2f75 --- /dev/null +++ b/kernel/kernel_umount.h @@ -0,0 +1,14 @@ +#ifndef __KSU_H_KERNEL_UMOUNT +#define __KSU_H_KERNEL_UMOUNT + +#include + +void ksu_kernel_umount_init(void); +void ksu_kernel_umount_exit(void); + +void try_umount(const char *mnt, bool check_mnt, int flags); + +// Handler function to be called from setresuid hook +int ksu_handle_umount(uid_t old_uid, uid_t new_uid); + +#endif \ No newline at end of file diff --git a/kernel/kpm/compact.c b/kernel/kpm/compact.c index 8af317c..5791db4 100644 --- a/kernel/kpm/compact.c +++ b/kernel/kpm/compact.c @@ -31,7 +31,7 @@ static int sukisu_is_su_allow_uid(uid_t uid) { - return ksu_is_allow_uid(uid) ? 1 : 0; + return ksu_is_allow_uid_for_current(uid) ? 1 : 0; } static int sukisu_get_ap_mod_exclude(uid_t uid) diff --git a/kernel/kpm/kpm.c b/kernel/kpm/kpm.c index 32a58ce..e31384b 100644 --- a/kernel/kpm/kpm.c +++ b/kernel/kpm/kpm.c @@ -9,13 +9,10 @@ * 并参照KernelPatch的标准KPM格式实现加载和控制 */ -#include -#include #include #include #include #include -#include #include #include #include @@ -25,26 +22,25 @@ #include #include #include -#include #include #include #include #include -#include #include -#include #include #include #include #include #include -#include #if LINUX_VERSION_CODE >= KERNEL_VERSION(5,0,0) && defined(CONFIG_MODULES) #include #endif #include "kpm.h" #include "compact.h" +#define KPM_NAME_LEN 32 +#define KPM_ARGS_LEN 1024 + #ifndef NO_OPTIMIZE #if defined(__GNUC__) && !defined(__clang__) #define NO_OPTIMIZE __attribute__((optimize("O0"))) @@ -56,156 +52,231 @@ #endif noinline NO_OPTIMIZE void sukisu_kpm_load_module_path(const char *path, - const char *args, void *ptr, void __user *result) + const char *args, void *ptr, int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_load_module_path). " + pr_info("kpm: Stub function called (sukisu_kpm_load_module_path). " "path=%s args=%s ptr=%p\n", path, args, ptr); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_load_module_path); noinline NO_OPTIMIZE void sukisu_kpm_unload_module(const char *name, - void *ptr, void __user *result) + void *ptr, int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_unload_module). " + pr_info("kpm: Stub function called (sukisu_kpm_unload_module). " "name=%s ptr=%p\n", name, ptr); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_unload_module); -noinline NO_OPTIMIZE void sukisu_kpm_num(void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_num(int *result) { - int res = 0; - - printk("KPM: Stub function called (sukisu_kpm_num).\n"); + pr_info("kpm: Stub function called (sukisu_kpm_num).\n"); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_num); -noinline NO_OPTIMIZE void sukisu_kpm_info(const char *name, void __user *out, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_info(const char *name, char *buf, int bufferSize, + int *size) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_info). " - "name=%s buffer=%p\n", name, out); + pr_info("kpm: Stub function called (sukisu_kpm_info). " + "name=%s buffer=%p\n", name, buf); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_info); -noinline NO_OPTIMIZE void sukisu_kpm_list(void __user *out, unsigned int bufferSize, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_list(void *out, int bufferSize, + int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_list). " + pr_info("kpm: Stub function called (sukisu_kpm_list). " "buffer=%p size=%d\n", out, bufferSize); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_list); -noinline NO_OPTIMIZE void sukisu_kpm_control(void __user *name, void __user *args, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_control(const char *name, const char *args, long arg_len, + int *result) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_control). " - "name=%p args=%p\n", name, args); + pr_info("kpm: Stub function called (sukisu_kpm_control). " + "name=%p args=%p arg_len=%ld\n", name, args, arg_len); __asm__ volatile("nop"); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); } EXPORT_SYMBOL(sukisu_kpm_control); -noinline NO_OPTIMIZE void sukisu_kpm_version(void __user *out, unsigned int bufferSize, - void __user *result) +noinline NO_OPTIMIZE void sukisu_kpm_version(char *buf, int bufferSize) { - int res = -1; - - printk("KPM: Stub function called (sukisu_kpm_version). " - "buffer=%p size=%d\n", out, bufferSize); - - if (copy_to_user(result, &res, sizeof(res)) < 1) - printk("KPM: Copy to user failed."); + pr_info("kpm: Stub function called (sukisu_kpm_version). " + "buffer=%p\n", buf); } EXPORT_SYMBOL(sukisu_kpm_version); -noinline int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, - unsigned long arg5) +noinline int sukisu_handle_kpm(unsigned long control_code, unsigned long arg1, unsigned long arg2, + unsigned long result_code) { - if (arg2 == SUKISU_KPM_LOAD) { - char kernel_load_path[256] = { 0 }; - char kernel_args_buffer[256] = { 0 }; + int res = -1; + if (control_code == SUKISU_KPM_LOAD) { + char kernel_load_path[256]; + char kernel_args_buffer[256]; - if (arg3 == 0) - return -1; + if (arg1 == 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg1, 255)) { + goto invalid_arg; + } - strncpy_from_user((char *)&kernel_load_path, (const char __user *)arg3, 255); + strncpy_from_user((char *)&kernel_load_path, (const char *)arg1, 255); - if (arg4 != 0) - strncpy_from_user((char *)&kernel_args_buffer, (const char __user *)arg4, 255); + if (arg2 != 0) { + if (!access_ok(arg2, 255)) { + goto invalid_arg; + } + + strncpy_from_user((char *)&kernel_args_buffer, (const char *)arg2, 255); + } sukisu_kpm_load_module_path((const char *)&kernel_load_path, - (const char *)&kernel_args_buffer, NULL, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_UNLOAD) { - char kernel_name_buffer[256] = { 0 }; + (const char *)&kernel_args_buffer, NULL, &res); + } else if (control_code == SUKISU_KPM_UNLOAD) { + char kernel_name_buffer[256]; - if (arg3 == 0) - return -1; - - strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg3, 255); - - sukisu_kpm_unload_module((const char *)&kernel_name_buffer, NULL, - (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_NUM) { - sukisu_kpm_num((void __user *)arg5); - } else if (arg2 == SUKISU_KPM_INFO) { - char kernel_name_buffer[256] = { 0 }; + if (arg1 == 0) { + res = -EINVAL; + goto exit; + } - if (arg3 == 0 || arg4 == 0) - return -1; + if (!access_ok(arg1, sizeof(kernel_name_buffer))) { + goto invalid_arg; + } - strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg3, 255); + strncpy_from_user((char *)&kernel_name_buffer, (const char *)arg1, sizeof(kernel_name_buffer)); - sukisu_kpm_info((const char *)&kernel_name_buffer, (char __user *)arg4, - (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_LIST) { - sukisu_kpm_list((char __user *)arg3, (unsigned int)arg4, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_CONTROL) { - sukisu_kpm_control((char __user *)arg3, (char __user *)arg4, (void __user *)arg5); - } else if (arg2 == SUKISU_KPM_VERSION) { - sukisu_kpm_version((char __user *)arg3, (unsigned int)arg4, (void __user *)arg5); + sukisu_kpm_unload_module((const char *)&kernel_name_buffer, NULL, &res); + } else if (control_code == SUKISU_KPM_NUM) { + sukisu_kpm_num(&res); + } else if (control_code == SUKISU_KPM_INFO) { + char kernel_name_buffer[256]; + char buf[256]; + int size; + + if (arg1 == 0 || arg2 == 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg1, sizeof(kernel_name_buffer))) { + goto invalid_arg; + } + + strncpy_from_user((char *)&kernel_name_buffer, (const char __user *)arg1, sizeof(kernel_name_buffer)); + + sukisu_kpm_info((const char *)&kernel_name_buffer, (char *)&buf, sizeof(buf), &size); + + if (!access_ok(arg2, size)) { + goto invalid_arg; + } + + res = copy_to_user(arg2, &buf, size); + + } else if (control_code == SUKISU_KPM_LIST) { + char buf[1024]; + int len = (int) arg2; + + if (len <= 0) { + res = -EINVAL; + goto exit; + } + + if (!access_ok(arg2, len)) { + goto invalid_arg; + } + + sukisu_kpm_list((char *)&buf, sizeof(buf), &res); + + if (res > len) { + res = -ENOBUFS; + goto exit; + } + + if (copy_to_user(arg1, &buf, len) != 0) + pr_info("kpm: Copy to user failed."); + + } else if (control_code == SUKISU_KPM_CONTROL) { + char kpm_name[KPM_NAME_LEN] = { 0 }; + char kpm_args[KPM_ARGS_LEN] = { 0 }; + + if (!access_ok(arg1, sizeof(kpm_name))) { + goto invalid_arg; + } + + if (!access_ok(arg2, sizeof(kpm_args))) { + goto invalid_arg; + } + + long name_len = strncpy_from_user((char *)&kpm_name, (const char __user *)arg1, sizeof(kpm_name)); + if (name_len <= 0) { + res = -EINVAL; + goto exit; + } + + long arg_len = strncpy_from_user((char *)&kpm_args, (const char __user *)arg2, sizeof(kpm_args)); + + sukisu_kpm_control((const char *)&kpm_name, (const char *)&kpm_args, arg_len, &res); + + } else if (control_code == SUKISU_KPM_VERSION) { + char buffer[256] = {0}; + + sukisu_kpm_version((char*) &buffer, sizeof(buffer)); + + unsigned int outlen = (unsigned int) arg2; + int len = strlen(buffer); + if (len >= outlen) len = outlen - 1; + + res = copy_to_user(arg1, &buffer, len + 1); } + +exit: + if (copy_to_user(result_code, &res, sizeof(res)) != 0) + pr_info("kpm: Copy to user failed."); return 0; +invalid_arg: + pr_err("kpm: invalid pointer detected! arg1: %px arg2: %px\n", (void *)arg1, (void *)arg2); + res = -EFAULT; + goto exit; } EXPORT_SYMBOL(sukisu_handle_kpm); -int sukisu_is_kpm_control_code(unsigned long arg2) { - return (arg2 >= CMD_KPM_CONTROL && - arg2 <= CMD_KPM_CONTROL_MAX) ? 1 : 0; +int sukisu_is_kpm_control_code(unsigned long control_code) { + return (control_code >= CMD_KPM_CONTROL && + control_code <= CMD_KPM_CONTROL_MAX) ? 1 : 0; } + +int do_kpm(void __user *arg) +{ + struct ksu_kpm_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("kpm: copy_from_user failed\n"); + return -EFAULT; + } + + if (!access_ok(cmd.control_code, sizeof(int))) { + pr_err("kpm: invalid control_code pointer %px\n", (void *)cmd.control_code); + return -EFAULT; + } + + if (!access_ok(cmd.result_code, sizeof(int))) { + pr_err("kpm: invalid result_code pointer %px\n", (void *)cmd.result_code); + return -EFAULT; + } + + return sukisu_handle_kpm(cmd.control_code, cmd.arg1, cmd.arg2, cmd.result_code); +} + diff --git a/kernel/kpm/kpm.h b/kernel/kpm/kpm.h index e8349d3..4fdcc20 100644 --- a/kernel/kpm/kpm.h +++ b/kernel/kpm/kpm.h @@ -1,58 +1,70 @@ #ifndef __SUKISU_KPM_H #define __SUKISU_KPM_H -extern int sukisu_handle_kpm(unsigned long arg2, unsigned long arg3, unsigned long arg4, - unsigned long arg5); -extern int sukisu_is_kpm_control_code(unsigned long arg2); +#include +#include + +struct ksu_kpm_cmd { + __aligned_u64 __user control_code; + __aligned_u64 __user arg1; + __aligned_u64 __user arg2; + __aligned_u64 __user result_code; +}; + +int sukisu_handle_kpm(unsigned long control_code, unsigned long arg3, unsigned long arg4, unsigned long result_code); +int sukisu_is_kpm_control_code(unsigned long control_code); +int do_kpm(void __user *arg); + +#define KSU_IOCTL_KPM _IOC(_IOC_READ|_IOC_WRITE, 'K', 200, 0) /* KPM Control Code */ -#define CMD_KPM_CONTROL 28 -#define CMD_KPM_CONTROL_MAX 35 +#define CMD_KPM_CONTROL 1 +#define CMD_KPM_CONTROL_MAX 10 /* Control Code */ /* - * prctl(xxx, 28, "PATH", "ARGS") + * prctl(xxx, 1, "PATH", "ARGS") * success return 0, error return -N */ -#define SUKISU_KPM_LOAD 28 +#define SUKISU_KPM_LOAD 1 /* - * prctl(xxx, 29, "NAME") + * prctl(xxx, 2, "NAME") * success return 0, error return -N */ -#define SUKISU_KPM_UNLOAD 29 +#define SUKISU_KPM_UNLOAD 2 /* - * num = prctl(xxx, 30) + * num = prctl(xxx, 3) * error return -N * success return +num or 0 */ -#define SUKISU_KPM_NUM 30 +#define SUKISU_KPM_NUM 3 /* - * prctl(xxx, 31, Buffer, BufferSize) + * prctl(xxx, 4, Buffer, BufferSize) * success return +out, error return -N */ -#define SUKISU_KPM_LIST 31 +#define SUKISU_KPM_LIST 4 /* - * prctl(xxx, 32, "NAME", Buffer[256]) + * prctl(xxx, 5, "NAME", Buffer[256]) * success return +out, error return -N */ -#define SUKISU_KPM_INFO 32 +#define SUKISU_KPM_INFO 5 /* - * prctl(xxx, 33, "NAME", "ARGS") + * prctl(xxx, 6, "NAME", "ARGS") * success return KPM's result value * error return -N */ -#define SUKISU_KPM_CONTROL 33 +#define SUKISU_KPM_CONTROL 6 /* - * prctl(xxx, 34, buffer, bufferSize) + * prctl(xxx, 7, buffer, bufferSize) * success return KPM's result value * error return -N */ -#define SUKISU_KPM_VERSION 34 +#define SUKISU_KPM_VERSION 7 #endif diff --git a/kernel/ksu.c b/kernel/ksu.c index 37a1754..850050c 100644 --- a/kernel/ksu.c +++ b/kernel/ksu.c @@ -3,89 +3,103 @@ #include #include #include +#include #include "allowlist.h" -#include "arch.h" -#include "core_hook.h" +#include "feature.h" #include "klog.h" // IWYU pragma: keep -#include "ksu.h" #include "throne_tracker.h" +#include "syscall_hook_manager.h" +#include "ksud.h" +#include "supercalls.h" + +#include "sulog.h" +#include "throne_comm.h" +#include "dynamic_manager.h" static struct workqueue_struct *ksu_workqueue; bool ksu_queue_work(struct work_struct *work) { - return queue_work(ksu_workqueue, work); + return queue_work(ksu_workqueue, work); } -extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, - void *argv, void *envp, int *flags); +void sukisu_custom_config_init(void) +{ +} -extern void ksu_sucompat_init(); -extern void ksu_sucompat_exit(); -extern void ksu_ksud_init(); -extern void ksu_ksud_exit(); -#ifdef CONFIG_KSU_TRACEPOINT_HOOK -extern void ksu_trace_register(); -extern void ksu_trace_unregister(); +void sukisu_custom_config_exit(void) +{ + ksu_uid_exit(); + ksu_throne_comm_exit(); + ksu_dynamic_manager_exit(); +#if __SULOG_GATE + ksu_sulog_exit(); #endif +} int __init kernelsu_init(void) { #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("*************************************************************"); + 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 - ksu_core_init(); + ksu_feature_init(); - ksu_workqueue = alloc_ordered_workqueue("kernelsu_work_queue", 0); + ksu_supercalls_init(); - ksu_allowlist_init(); + sukisu_custom_config_init(); - ksu_throne_tracker_init(); -#ifdef CONFIG_KSU_KPROBES_HOOK - ksu_sucompat_init(); - ksu_ksud_init(); + ksu_syscall_hook_manager_init(); + + ksu_workqueue = alloc_ordered_workqueue("kernelsu_work_queue", 0); + + ksu_allowlist_init(); + + ksu_throne_tracker_init(); + +#ifdef KSU_KPROBES_HOOK + ksu_ksud_init(); #else - pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html"); -#endif - -#ifdef CONFIG_KSU_TRACEPOINT_HOOK - ksu_trace_register(); + pr_alert("KPROBES is disabled, KernelSU may not work, please check https://kernelsu.org/guide/how-to-integrate-for-non-gki.html"); #endif #ifdef MODULE #ifndef CONFIG_KSU_DEBUG - kobject_del(&THIS_MODULE->mkobj.kobj); + kobject_del(&THIS_MODULE->mkobj.kobj); #endif #endif - return 0; + return 0; } +extern void ksu_observer_exit(void); void kernelsu_exit(void) { - ksu_allowlist_exit(); + ksu_allowlist_exit(); - ksu_throne_tracker_exit(); + ksu_observer_exit(); - destroy_workqueue(ksu_workqueue); + ksu_throne_tracker_exit(); -#ifdef CONFIG_KSU_KPROBES_HOOK - ksu_ksud_exit(); - ksu_sucompat_exit(); + destroy_workqueue(ksu_workqueue); + +#ifdef KSU_KPROBES_HOOK + ksu_ksud_exit(); #endif -#ifdef CONFIG_KSU_TRACEPOINT_HOOK - ksu_trace_unregister(); -#endif + ksu_syscall_hook_manager_exit(); - ksu_core_exit(); + sukisu_custom_config_exit(); + + ksu_supercalls_exit(); + + ksu_feature_exit(); } module_init(kernelsu_init); @@ -94,4 +108,9 @@ 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 diff --git a/kernel/ksu.h b/kernel/ksu.h index 5320180..93750af 100644 --- a/kernel/ksu.h +++ b/kernel/ksu.h @@ -7,40 +7,12 @@ #define KERNEL_SU_VERSION KSU_VERSION #define KERNEL_SU_OPTION 0xDEADBEEF -#define CMD_GRANT_ROOT 0 -#define CMD_BECOME_MANAGER 1 -#define CMD_GET_VERSION 2 -#define CMD_ALLOW_SU 3 -#define CMD_DENY_SU 4 -#define CMD_GET_ALLOW_LIST 5 -#define CMD_GET_DENY_LIST 6 -#define CMD_REPORT_EVENT 7 -#define CMD_SET_SEPOLICY 8 -#define CMD_CHECK_SAFEMODE 9 -#define CMD_GET_APP_PROFILE 10 -#define CMD_SET_APP_PROFILE 11 -#define CMD_UID_GRANTED_ROOT 12 -#define CMD_UID_SHOULD_UMOUNT 13 -#define CMD_IS_SU_ENABLED 14 -#define CMD_ENABLE_SU 15 - -#define CMD_GET_FULL_VERSION 0xC0FFEE1A - -#define CMD_ENABLE_KPM 100 -#define CMD_HOOK_TYPE 101 -#define CMD_DYNAMIC_MANAGER 103 -#define CMD_GET_MANAGERS 104 +extern bool ksu_uid_scanner_enabled; #define EVENT_POST_FS_DATA 1 #define EVENT_BOOT_COMPLETED 2 #define EVENT_MODULE_MOUNTED 3 -#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 - // SukiSU Ultra kernel su version full strings #ifndef KSU_VERSION_FULL #define KSU_VERSION_FULL "v3.x-00000000@unknown" @@ -51,6 +23,10 @@ #define DYNAMIC_MANAGER_OP_GET 1 #define DYNAMIC_MANAGER_OP_CLEAR 2 +#define UID_SCANNER_OP_GET_STATUS 0 +#define UID_SCANNER_OP_TOGGLE 1 +#define UID_SCANNER_OP_CLEAR_ENV 2 + struct dynamic_manager_user_config { unsigned int operation; unsigned int size; @@ -65,68 +41,22 @@ struct manager_list_info { } managers[2]; }; -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; - }; -}; - bool ksu_queue_work(struct work_struct *work); +#if 0 static inline int startswith(char *s, char *prefix) { - return strncmp(s, prefix, strlen(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); + size_t slen = strlen(s); + size_t tlen = strlen(t); + if (tlen > slen) + return 1; + return strcmp(s + slen - tlen, t); } +#endif #endif diff --git a/kernel/ksu_trace.c b/kernel/ksu_trace.c deleted file mode 100644 index 5acf092..0000000 --- a/kernel/ksu_trace.c +++ /dev/null @@ -1,69 +0,0 @@ -#include "ksu_trace.h" - - -// extern kernelsu functions -extern bool ksu_vfs_read_hook __read_mostly; -extern bool ksu_input_hook __read_mostly; -extern int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags); -extern int ksu_handle_faccessat(int *dfd, const char __user **filename_user, int *mode, int *flags); -extern int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr, size_t *count_ptr); -extern int ksu_handle_stat(int *dfd, const char __user **filename_user, int *flags); -extern int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, int *value); -// end kernelsu functions - - -// tracepoint callback functions -void ksu_trace_execveat_sucompat_hook_callback(void *data, int *fd, struct filename **filename_ptr, - void *argv, void *envp, int *flags) -{ - ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp, flags); -} - -void ksu_trace_faccessat_hook_callback(void *data, int *dfd, const char __user **filename_user, - int *mode, int *flags) -{ - ksu_handle_faccessat(dfd, filename_user, mode, flags); -} - -void ksu_trace_sys_read_hook_callback(void *data, unsigned int fd, char __user **buf_ptr, - size_t *count_ptr) -{ - if (unlikely(ksu_vfs_read_hook)) - ksu_handle_sys_read(fd, buf_ptr, count_ptr); -} - -void ksu_trace_stat_hook_callback(void *data, int *dfd, const char __user **filename_user, - int *flags) -{ - ksu_handle_stat(dfd, filename_user, flags); -} - -void ksu_trace_input_hook_callback(void *data, unsigned int *type, unsigned int *code, - int *value) -{ - if (unlikely(ksu_input_hook)) - ksu_handle_input_handle_event(type, code, value); -} - -// end tracepoint callback functions - - -// register tracepoint callback functions -void ksu_trace_register(void) -{ - register_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL); - register_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL); - register_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL); - register_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL); - register_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL); -} - -// unregister tracepoint callback functions -void ksu_trace_unregister(void) -{ - unregister_trace_ksu_trace_execveat_sucompat_hook(ksu_trace_execveat_sucompat_hook_callback, NULL); - unregister_trace_ksu_trace_faccessat_hook(ksu_trace_faccessat_hook_callback, NULL); - unregister_trace_ksu_trace_sys_read_hook(ksu_trace_sys_read_hook_callback, NULL); - unregister_trace_ksu_trace_stat_hook(ksu_trace_stat_hook_callback, NULL); - unregister_trace_ksu_trace_input_hook(ksu_trace_input_hook_callback, NULL); -} diff --git a/kernel/ksu_trace.h b/kernel/ksu_trace.h deleted file mode 100644 index dc5394b..0000000 --- a/kernel/ksu_trace.h +++ /dev/null @@ -1,37 +0,0 @@ -#undef TRACE_SYSTEM -#define TRACE_SYSTEM ksu_trace - -#if !defined(_KSU_TRACE_H) || defined(TRACE_HEADER_MULTI_READ) -#define _KSU_TRACE_H - -#include -#include - -DECLARE_TRACE(ksu_trace_execveat_sucompat_hook, - TP_PROTO(int *fd, struct filename **filename_ptr, void *argv, void *envp, int *flags), - TP_ARGS(fd, filename_ptr, argv, envp, flags)); - -DECLARE_TRACE(ksu_trace_faccessat_hook, - TP_PROTO(int *dfd, const char __user **filename_user, int *mode, int *flags), - TP_ARGS(dfd, filename_user, mode, flags)); - -DECLARE_TRACE(ksu_trace_sys_read_hook, - TP_PROTO(unsigned int fd, char __user **buf_ptr, size_t *count_ptr), - TP_ARGS(fd, buf_ptr, count_ptr)); - -DECLARE_TRACE(ksu_trace_stat_hook, - TP_PROTO(int *dfd, const char __user **filename_user, int *flags), - TP_ARGS(dfd, filename_user, flags)); - -DECLARE_TRACE(ksu_trace_input_hook, - TP_PROTO(unsigned int *type, unsigned int *code, int *value), - TP_ARGS(type, code, value)); - -#endif /* _KSU_TRACE_H */ - -#undef TRACE_INCLUDE_PATH -#define TRACE_INCLUDE_PATH . -#undef TRACE_INCLUDE_FILE -#define TRACE_INCLUDE_FILE ksu_trace - -#include diff --git a/kernel/ksu_trace_export.c b/kernel/ksu_trace_export.c deleted file mode 100644 index afa4472..0000000 --- a/kernel/ksu_trace_export.c +++ /dev/null @@ -1,8 +0,0 @@ -#define CREATE_TRACE_POINTS -#include "ksu_trace.h" - -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_execveat_sucompat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_faccessat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_sys_read_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_stat_hook); -EXPORT_TRACEPOINT_SYMBOL_GPL(ksu_trace_input_hook); diff --git a/kernel/ksud.c b/kernel/ksud.c index f121b62..4431231 100644 --- a/kernel/ksud.c +++ b/kernel/ksud.c @@ -1,3 +1,6 @@ +#include +#include +#include #include #include #include @@ -11,226 +14,318 @@ #include #include #include +#include #include +#include "manager.h" #include "allowlist.h" #include "arch.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" -#include "kernel_compat.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" + "\n" - "on post-fs-data\n" - " start logd\n" - // We should wait for the post-fs-data finish - " exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n" - "\n" + "on post-fs-data\n" + " start logd\n" + // We should wait for the post-fs-data finish + " exec u:r:su:s0 root -- " KSUD_PATH " post-fs-data\n" + "\n" - "on nonencrypted\n" - " exec u:r:su:s0 root -- " KSUD_PATH " services\n" - "\n" + "on nonencrypted\n" + " exec u:r:su:s0 root -- " KSUD_PATH " services\n" + "\n" - "on property:vold.decrypt=trigger_restart_framework\n" - " exec u:r:su:s0 root -- " KSUD_PATH " services\n" - "\n" + "on property:vold.decrypt=trigger_restart_framework\n" + " exec u:r:su:s0 root -- " KSUD_PATH " services\n" + "\n" - "on property:sys.boot_completed=1\n" - " exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n" - "\n" + "on property:sys.boot_completed=1\n" + " exec u:r:su:s0 root -- " KSUD_PATH " boot-completed\n" + "\n" - "\n"; + "\n"; static void stop_vfs_read_hook(); static void stop_execve_hook(); static void stop_input_hook(); -#ifdef CONFIG_KSU_KPROBES_HOOK +#ifdef KSU_KPROBES_HOOK static struct work_struct stop_vfs_read_work; static struct work_struct stop_execve_hook_work; static struct work_struct stop_input_hook_work; #else bool ksu_vfs_read_hook __read_mostly = true; +bool ksu_execveat_hook __read_mostly = true; bool ksu_input_hook __read_mostly = true; #endif -bool ksu_execveat_hook __read_mostly = true; -u32 ksu_devpts_sid; +u32 ksu_file_sid; // Detect whether it is on or not static bool is_boot_phase = true; 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(); - // sanity check, this may influence the performance - stop_input_hook(); + 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(); - ksu_devpts_sid = ksu_get_devpts_sid(); - pr_info("devpts sid: %d\n", ksu_devpts_sid); - - // End of boot state + // End of boot state is_boot_phase = false; + + ksu_file_sid = ksu_get_ksu_file_sid(); + pr_info("ksu_file sid: %d\n", ksu_file_sid); } -// since _ksud handler only uses argv and envp for comparisons -// this can probably work -// adapted from ksu_handle_execveat_ksud -static int ksu_handle_bprm_ksud(const char *filename, const char *argv1, const char *envp, size_t envp_len) +extern void ext4_unregister_sysfs(struct super_block *sb); +static void nuke_ext4_sysfs(void) { - static const char app_process[] = "/system/bin/app_process"; - static bool first_app_process = true; +#ifdef CONFIG_EXT4_FS + struct path path; + int err = kern_path("/data/adb/modules", 0, &path); + if (err) { + pr_err("nuke path err: %d\n", err); + return; + } - /* This applies to versions Android 10+ */ - static const char system_bin_init[] = "/system/bin/init"; - /* This applies to versions between Android 6 ~ 9 */ - static const char old_system_init[] = "/init"; - static bool init_second_stage_executed = false; + 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; + } - // return early when disabled - if (!ksu_execveat_hook) - return 0; + ext4_unregister_sysfs(sb); + path_put(&path); +#endif +} - if (!filename) - return 0; +void on_module_mounted(void){ + pr_info("on_module_mounted!\n"); + ksu_module_mounted = true; + nuke_ext4_sysfs(); +} - // debug! remove me! - pr_info("%s: filename: %s argv1: %s envp_len: %zu\n", __func__, filename, argv1, envp_len); +void on_boot_completed(void){ + ksu_boot_completed = true; + pr_info("on_boot_completed!\n"); + track_throne(true); +} -#ifdef CONFIG_KSU_DEBUG - const char *envp_n = envp; - unsigned int envc = 1; - do { - pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n); - envp_n += strlen(envp_n) + 1; - envc++; - } while (envp_n < envp + 256); +#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 (init_second_stage_executed) - goto first_app_process; + if (get_user(native, argv.ptr.native + nr)) + return ERR_PTR(-EFAULT); - // /system/bin/init with argv1 - if (!init_second_stage_executed - && (!memcmp(filename, system_bin_init, sizeof(system_bin_init) - 1))) { - if (argv1 && !strcmp(argv1, "second_stage")) { - pr_info("%s: /system/bin/init second_stage executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - - // /init with argv1 - if (!init_second_stage_executed - && (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) { - if (argv1 && !strcmp(argv1, "--second-stage")) { - pr_info("%s: /init --second-stage executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - - if (!envp || !envp_len) - goto first_app_process; - - // /init without argv1/useless-argv1 but usable envp - // untested! TODO: test and debug me! - if (!init_second_stage_executed && (!memcmp(filename, old_system_init, sizeof(old_system_init) - 1))) { - - // we hunt for "INIT_SECOND_STAGE" - const char *envp_n = envp; - unsigned int envc = 1; - do { - if (strstarts(envp_n, "INIT_SECOND_STAGE")) - break; - envp_n += strlen(envp_n) + 1; - envc++; - } while (envp_n < envp + envp_len); - pr_info("%s: envp[%d]: %s\n", __func__, envc, envp_n); - - if (!strcmp(envp_n, "INIT_SECOND_STAGE=1") - || !strcmp(envp_n, "INIT_SECOND_STAGE=true") ) { - pr_info("%s: /init +envp: INIT_SECOND_STAGE executed\n", __func__); - apply_kernelsu_rules(); - init_second_stage_executed = true; - ksu_android_ns_fs_check(); - } - } - -first_app_process: - if (first_app_process && !memcmp(filename, app_process, sizeof(app_process) - 1)) { - first_app_process = false; - pr_info("%s: exec app_process, /data prepared, second_stage: %d\n", __func__, init_second_stage_executed); - on_post_fs_data(); - stop_execve_hook(); - } - - return 0; + return native; } -int ksu_handle_pre_ksud(const char *filename) +/* + * 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) { - if (likely(!ksu_execveat_hook)) - return 0; + int i = 0; - // not /system/bin/init, not /init, not /system/bin/app_process (64/32 thingy) - // return 0; - if (likely(strcmp(filename, "/system/bin/init") && strcmp(filename, "/init") - && !strstarts(filename, "/system/bin/app_process") )) - return 0; + if (argv.ptr.native != NULL) { + for (;;) { + const char __user *p = get_user_arg_ptr(argv, i); - if (!current || !current->mm) - return 0; + if (!p) + break; - // https://elixir.bootlin.com/linux/v4.14.1/source/include/linux/mm_types.h#L429 - // unsigned long arg_start, arg_end, env_start, env_end; - unsigned long arg_start = current->mm->arg_start; - unsigned long arg_end = current->mm->arg_end; - unsigned long env_start = current->mm->env_start; - unsigned long env_end = current->mm->env_end; + if (IS_ERR(p)) + return -EFAULT; - size_t arg_len = arg_end - arg_start; - size_t envp_len = env_end - env_start; + if (i >= max) + return -E2BIG; + ++i; - if (arg_len <= 0 || envp_len <= 0) // this wont make sense, filter it - return 0; + if (fatal_signal_pending(current)) + return -ERESTARTNOHAND; + } + } + return i; +} - #define ARGV_MAX 32 // this is enough for argv1 - #define ENVP_MAX 256 // this is enough for INIT_SECOND_STAGE - char args[ARGV_MAX]; - size_t argv_copy_len = (arg_len > ARGV_MAX) ? ARGV_MAX : arg_len; - char envp[ENVP_MAX]; - size_t envp_copy_len = (envp_len > ENVP_MAX) ? ENVP_MAX : envp_len; +static void on_post_fs_data_cbfun(struct callback_head *cb) +{ + on_post_fs_data(); +} - // we cant use strncpy on here, else it will truncate once it sees \0 - if (ksu_copy_from_user_retry(args, (void __user *)arg_start, argv_copy_len)) - return 0; +static struct callback_head on_post_fs_data_cb = { .func = + on_post_fs_data_cbfun }; - if (ksu_copy_from_user_retry(envp, (void __user *)env_start, envp_copy_len)) - return 0; +// 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) +{ +#ifndef KSU_KPROBES_HOOK + if (!ksu_execveat_hook) { + return 0; + } +#endif + struct filename *filename; - args[ARGV_MAX - 1] = '\0'; - envp[ENVP_MAX - 1] = '\0'; + static const char app_process[] = "/system/bin/app_process"; + static bool first_app_process = true; - // we only need argv1 ! - // abuse strlen here since it only gets length up to \0 - char *argv1 = args + strlen(args) + 1; - if (argv1 >= args + argv_copy_len) // out of bounds! - argv1 = ""; + /* This applies to versions Android 10+ */ + static const char system_bin_init[] = "/system/bin/init"; + /* This applies to versions between Android 6 ~ 9 */ + static const char old_system_init[] = "/init"; + static bool init_second_stage_executed = false; - return ksu_handle_bprm_ksud(filename, argv1, envp, envp_copy_len); + if (!filename_ptr) + return 0; + + filename = *filename_ptr; + if (IS_ERR(filename)) { + return 0; + } + + if (unlikely(!memcmp(filename->name, system_bin_init, + sizeof(system_bin_init) - 1) && + argv)) { + // /system/bin/init executed + int argc = count(*argv, MAX_ARG_STRINGS); + pr_info("/system/bin/init argc: %d\n", argc); + if (argc > 1 && !init_second_stage_executed) { + const char __user *p = get_user_arg_ptr(*argv, 1); + if (p && !IS_ERR(p)) { + char first_arg[16]; + strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); + pr_info("/system/bin/init first arg: %s\n", first_arg); + if (!strcmp(first_arg, "second_stage")) { + pr_info("/system/bin/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } else { + pr_err("/system/bin/init parse args err!\n"); + } + } + } else if (unlikely(!memcmp(filename->name, old_system_init, + sizeof(old_system_init) - 1) && + argv)) { + // /init executed + int argc = count(*argv, MAX_ARG_STRINGS); + pr_info("/init argc: %d\n", argc); + if (argc > 1 && !init_second_stage_executed) { + /* This applies to versions between Android 6 ~ 7 */ + const char __user *p = get_user_arg_ptr(*argv, 1); + if (p && !IS_ERR(p)) { + char first_arg[16]; + strncpy_from_user_nofault(first_arg, p, sizeof(first_arg)); + pr_info("/init first arg: %s\n", first_arg); + if (!strcmp(first_arg, "--second-stage")) { + pr_info("/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } else { + pr_err("/init parse args err!\n"); + } + } else if (argc == 1 && !init_second_stage_executed && envp) { + /* This applies to versions between Android 8 ~ 9 */ + int envc = count(*envp, MAX_ARG_STRINGS); + if (envc > 0) { + int n; + for (n = 1; n <= envc; n++) { + const char __user *p = get_user_arg_ptr(*envp, n); + if (!p || IS_ERR(p)) { + continue; + } + char env[256]; + // Reading environment variable strings from user space + if (strncpy_from_user_nofault(env, p, sizeof(env)) < 0) + continue; + // Parsing environment variable names and values + char *env_name = env; + char *env_value = strchr(env, '='); + if (env_value == NULL) + continue; + // Replace equal sign with string terminator + *env_value = '\0'; + env_value++; + // Check if the environment variable name and value are matching + if (!strcmp(env_name, "INIT_SECOND_STAGE") && + (!strcmp(env_value, "1") || + !strcmp(env_value, "true"))) { + pr_info("/init second_stage executed\n"); + apply_kernelsu_rules(); + init_second_stage_executed = true; + } + } + } + } + } + + if (unlikely(first_app_process && !memcmp(filename->name, app_process, + sizeof(app_process) - 1))) { + first_app_process = false; + pr_info("exec app_process, /data prepared, second_stage: %d\n", + init_second_stage_executed); + struct task_struct *init_task; + rcu_read_lock(); + 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(); + + stop_execve_hook(); + } + + return 0; } static ssize_t (*orig_read)(struct file *, char __user *, size_t, loff_t *); @@ -239,426 +334,326 @@ static struct file_operations fops_proxy; static ssize_t read_count_append = 0; static ssize_t read_proxy(struct file *file, char __user *buf, size_t count, - loff_t *pos) + loff_t *pos) { - bool first_read = file->f_pos == 0; - ssize_t ret = orig_read(file, buf, count, pos); - if (first_read) { - pr_info("read_proxy append %ld + %ld\n", ret, - read_count_append); - ret += read_count_append; - } - return ret; + bool first_read = file->f_pos == 0; + ssize_t ret = orig_read(file, buf, count, pos); + if (first_read) { + pr_info("read_proxy append %ld + %ld\n", ret, read_count_append); + ret += read_count_append; + } + return ret; } static ssize_t read_iter_proxy(struct kiocb *iocb, struct iov_iter *to) { - bool first_read = iocb->ki_pos == 0; - ssize_t ret = orig_read_iter(iocb, to); - if (first_read) { - pr_info("read_iter_proxy append %ld + %ld\n", ret, - read_count_append); - ret += read_count_append; - } - return ret; + bool first_read = iocb->ki_pos == 0; + ssize_t ret = orig_read_iter(iocb, to); + if (first_read) { + pr_info("read_iter_proxy append %ld + %ld\n", ret, read_count_append); + ret += read_count_append; + } + return ret; } -int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, - size_t *count_ptr, loff_t **pos) +static int ksu_handle_vfs_read(struct file **file_ptr, char __user **buf_ptr, + size_t *count_ptr, loff_t **pos) { -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_vfs_read_hook) { - return 0; - } +#ifndef KSU_KPROBES_HOOK + if (!ksu_vfs_read_hook) { + return 0; + } #endif - struct file *file; - char __user *buf; - size_t count; + struct file *file; + char __user *buf; + size_t count; - if (strcmp(current->comm, "init")) { - // we are only interest in `init` process - return 0; - } + if (strcmp(current->comm, "init")) { + // we are only interest in `init` process + return 0; + } - file = *file_ptr; - if (IS_ERR(file)) { - return 0; - } + file = *file_ptr; + if (IS_ERR(file)) { + return 0; + } - if (!d_is_reg(file->f_path.dentry)) { - return 0; - } + if (!d_is_reg(file->f_path.dentry)) { + return 0; + } - const char *short_name = file->f_path.dentry->d_name.name; - if (strcmp(short_name, "atrace.rc")) { - // we are only interest `atrace.rc` file name file - return 0; - } - char path[256]; - char *dpath = d_path(&file->f_path, path, sizeof(path)); + const char *short_name = file->f_path.dentry->d_name.name; + if (strcmp(short_name, "atrace.rc")) { + // we are only interest `atrace.rc` file name file + return 0; + } + char path[256]; + char *dpath = d_path(&file->f_path, path, sizeof(path)); - if (IS_ERR(dpath)) { - return 0; - } + if (IS_ERR(dpath)) { + return 0; + } - if (strcmp(dpath, "/system/etc/init/atrace.rc")) { - return 0; - } + if (strcmp(dpath, "/system/etc/init/atrace.rc")) { + return 0; + } - // we only process the first read - static bool rc_inserted = false; - if (rc_inserted) { - // we don't need this kprobe, unregister it! - stop_vfs_read_hook(); - return 0; - } - rc_inserted = true; + // we only process the first read + static bool rc_inserted = false; + if (rc_inserted) { + // we don't need this kprobe, unregister it! + stop_vfs_read_hook(); + return 0; + } + rc_inserted = true; - // now we can sure that the init process is reading - // `/system/etc/init/atrace.rc` - buf = *buf_ptr; - count = *count_ptr; + // now we can sure that the init process is reading + // `/system/etc/init/atrace.rc` + buf = *buf_ptr; + count = *count_ptr; - size_t rc_count = strlen(KERNEL_SU_RC); + size_t rc_count = strlen(KERNEL_SU_RC); - pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath, - current->comm, count, rc_count); + pr_info("vfs_read: %s, comm: %s, count: %zu, rc_count: %zu\n", dpath, + current->comm, count, rc_count); - if (count < rc_count) { - pr_err("count: %zu < rc_count: %zu\n", count, rc_count); - return 0; - } + if (count < rc_count) { + pr_err("count: %zu < rc_count: %zu\n", count, rc_count); + return 0; + } - size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count); - if (ret) { - pr_err("copy ksud.rc failed: %zu\n", ret); - return 0; - } + size_t ret = copy_to_user(buf, KERNEL_SU_RC, rc_count); + if (ret) { + pr_err("copy ksud.rc failed: %zu\n", ret); + return 0; + } - // we've succeed to insert ksud.rc, 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; - read_count_append = rc_count; + // we've succeed to insert ksud.rc, 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; + read_count_append = rc_count; - *buf_ptr = buf + rc_count; - *count_ptr = count - rc_count; + *buf_ptr = buf + rc_count; + *count_ptr = count - rc_count; - return 0; + return 0; } int ksu_handle_sys_read(unsigned int fd, char __user **buf_ptr, - size_t *count_ptr) + size_t *count_ptr) { - struct file *file = fget(fd); - if (!file) { - return 0; - } - int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL); - fput(file); - return result; + struct file *file = fget(fd); + if (!file) { + return 0; + } + int result = ksu_handle_vfs_read(&file, buf_ptr, count_ptr, NULL); + fput(file); + return result; } static unsigned int volumedown_pressed_count = 0; static bool is_volumedown_enough(unsigned int count) { - return count >= 3; + return count >= 3; } int ksu_handle_input_handle_event(unsigned int *type, unsigned int *code, - int *value) + int *value) { -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_input_hook) { - return 0; - } +#ifndef KSU_KPROBES_HOOK + if (!ksu_input_hook) { + return 0; + } #endif - if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) { - int val = *value; - pr_info("KEY_VOLUMEDOWN val: %d\n", val); - if (val && is_boot_phase) { - // key pressed, count it - volumedown_pressed_count += 1; - if (is_volumedown_enough(volumedown_pressed_count)) { - stop_input_hook(); - } - } - } + if (*type == EV_KEY && *code == KEY_VOLUMEDOWN) { + int val = *value; + pr_info("KEY_VOLUMEDOWN val: %d\n", val); + if (val && is_boot_phase) { + // key pressed, count it + volumedown_pressed_count += 1; + if (is_volumedown_enough(volumedown_pressed_count)) { + stop_input_hook(); + } + } + } - return 0; + 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; - } + static bool safe_mode = false; + if (safe_mode) { + // don't need to check again, userspace may call multiple times + return true; + } - // stop hook first! - stop_input_hook(); + // 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; - } + 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; + return false; } -#ifdef CONFIG_KSU_KPROBES_HOOK +#ifdef KSU_KPROBES_HOOK + static int sys_execve_handler_pre(struct kprobe *p, struct pt_regs *regs) { - /* - asmlinkage int sys_execve(const char __user *filenamei, - const char __user *const __user *argv, - const char __user *const __user *envp, struct pt_regs *regs) - */ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - const char __user *filename_user = (const char __user *)PT_REGS_PARM1(real_regs); - const char __user *const __user *__argv = (const char __user *const __user *)PT_REGS_PARM2(real_regs); - const char __user *const __user *__envp = (const char __user *const __user *)PT_REGS_PARM3(real_regs); - char path[32]; + 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]; - if (!filename_user) - return 0; + if (!filename_user) + return 0; -// filename stage - if (ksu_copy_from_user_retry(path, filename_user, sizeof(path))) - return 0; + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, 32); + filename_in.name = path; - path[sizeof(path) - 1] = '\0'; - - // not /system/bin/init, not /init, not /system/bin/app_process (64/32 thingy) - // we dont care !! - if (likely(strcmp(path, "/system/bin/init") && strcmp(path, "/init") - && !strstarts(path, "/system/bin/app_process") )) - return 0; - -// argv stage - char argv1[32] = {0}; - // memzero_explicit(argv1, 32); - if (__argv) { - const char __user *arg1_user = NULL; - // grab argv[1] pointer - // this looks like - /* - * 0x1000 ./program << this is __argv - * 0x1001 -o - * 0x1002 arg - */ - if (ksu_copy_from_user_retry(&arg1_user, __argv + 1, sizeof(arg1_user))) - goto no_argv1; // copy argv[1] pointer fail, probably no argv1 !! - - if (arg1_user) - ksu_copy_from_user_retry(argv1, arg1_user, sizeof(argv1)); - } - -no_argv1: - argv1[sizeof(argv1) - 1] = '\0'; - -// envp stage - #define ENVP_MAX 256 - char envp[ENVP_MAX] = {0}; - char *dst = envp; - size_t envp_len = 0; - int i = 0; // to track user pointer offset from __envp - - // memzero_explicit(envp, ENVP_MAX); - - if (__envp) { - do { - const char __user *env_entry_user = NULL; - // this is also like argv above - /* - * 0x1001 PATH=/bin - * 0x1002 VARIABLE=value - * 0x1002 some_more_env_var=1 - */ - - // check if pointer exists - if (ksu_copy_from_user_retry(&env_entry_user, __envp + i, sizeof(env_entry_user))) - break; - - // check if no more env entry - if (!env_entry_user) - break; - - // probably redundant to while condition but ok - if (envp_len >= ENVP_MAX - 1) - break; - - // copy strings from env_entry_user pointer that we collected - // also break if failed - if (ksu_copy_from_user_retry(dst, env_entry_user, ENVP_MAX - envp_len)) - break; - - // get the length of that new copy above - // get lngth of dst as far as ENVP_MAX - current collected envp_len - size_t len = strnlen(dst, ENVP_MAX - envp_len); - if (envp_len + len + 1 > ENVP_MAX) - break; // if more than 255 bytes, bail - - dst[len] = '\0'; - // collect total number of copied strings - envp_len = envp_len + len + 1; - // increment dst address since we need to put something on next iter - dst = dst + len + 1; - // pointer walk, __envp + i - i++; - } while (envp_len < ENVP_MAX); - } - - /* - at this point, we shoul've collected envp from - * 0x1001 PATH=/bin - * 0x1002 VARIABLE=value - * 0x1002 some_more_env_var=1 - to - * 0x1234 PATH=/bin\0VARIABLE=value\0some_more_env_var=1\0\0\0\0 - */ - - envp[ENVP_MAX - 1] = '\0'; - -#ifdef CONFIG_KSU_DEBUG - pr_info("%s: filename: %s argv[1]:%s envp_len: %zu\n", __func__, path, argv1, envp_len); -#endif - return ksu_handle_bprm_ksud(path, argv1, envp, envp_len); + 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); - char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs); - size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs); + struct pt_regs *real_regs = PT_REAL_REGS(regs); + unsigned int fd = PT_REGS_PARM1(real_regs); + char __user **buf_ptr = (char __user **)&PT_REGS_PARM2(real_regs); + size_t count_ptr = (size_t *)&PT_REGS_PARM3(real_regs); - return ksu_handle_sys_read(fd, buf_ptr, count_ptr); + return ksu_handle_sys_read(fd, buf_ptr, count_ptr); } static int input_handle_event_handler_pre(struct kprobe *p, - struct pt_regs *regs) + 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); + 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, + .symbol_name = SYS_EXECVE_SYMBOL, + .pre_handler = sys_execve_handler_pre, }; static struct kprobe vfs_read_kp = { - .symbol_name = SYS_READ_SYMBOL, - .pre_handler = sys_read_handler_pre, + .symbol_name = SYS_READ_SYMBOL, + .pre_handler = sys_read_handler_pre, }; static struct kprobe input_event_kp = { - .symbol_name = "input_event", - .pre_handler = input_handle_event_handler_pre, + .symbol_name = "input_event", + .pre_handler = input_handle_event_handler_pre, }; - static void do_stop_vfs_read_hook(struct work_struct *work) { - unregister_kprobe(&vfs_read_kp); + unregister_kprobe(&vfs_read_kp); } static void do_stop_execve_hook(struct work_struct *work) { - unregister_kprobe(&execve_kp); + unregister_kprobe(&execve_kp); } static void do_stop_input_hook(struct work_struct *work) { - unregister_kprobe(&input_event_kp); + unregister_kprobe(&input_event_kp); } #endif static void stop_vfs_read_hook() { -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_vfs_read_work); - pr_info("unregister vfs_read kprobe: %d!\n", ret); +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_vfs_read_work); + pr_info("unregister vfs_read kprobe: %d!\n", ret); #else - ksu_vfs_read_hook = false; - pr_info("stop vfs_read_hook\n"); + ksu_vfs_read_hook = false; + pr_info("stop vfs_read_hook\n"); #endif } static void stop_execve_hook() { -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_execve_hook_work); - pr_info("unregister execve kprobe: %d!\n", ret); +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_execve_hook_work); + pr_info("unregister execve kprobe: %d!\n", ret); #else - pr_info("stop execve_hook\n"); + ksu_execveat_hook = false; + pr_info("stop execve_hook\n"); #endif - ksu_execveat_hook = false; } static void stop_input_hook() { - static bool input_hook_stopped = false; - if (input_hook_stopped) { - return; - } - input_hook_stopped = true; -#ifdef CONFIG_KSU_KPROBES_HOOK - bool ret = schedule_work(&stop_input_hook_work); - pr_info("unregister input kprobe: %d!\n", ret); + static bool input_hook_stopped = false; + if (input_hook_stopped) { + return; + } + input_hook_stopped = true; +#ifdef KSU_KPROBES_HOOK + bool ret = schedule_work(&stop_input_hook_work); + pr_info("unregister input kprobe: %d!\n", ret); #else - ksu_input_hook = false; - pr_info("stop input_hook\n"); + ksu_input_hook = false; + pr_info("stop input_hook\n"); #endif } // ksud: module support void ksu_ksud_init() { -#ifdef CONFIG_KSU_KPROBES_HOOK - int ret; +#ifdef KSU_KPROBES_HOOK + int ret; - ret = register_kprobe(&execve_kp); - pr_info("ksud: execve_kp: %d\n", ret); + ret = register_kprobe(&execve_kp); + pr_info("ksud: execve_kp: %d\n", ret); - ret = register_kprobe(&vfs_read_kp); - pr_info("ksud: vfs_read_kp: %d\n", ret); + ret = register_kprobe(&vfs_read_kp); + pr_info("ksud: vfs_read_kp: %d\n", ret); - ret = register_kprobe(&input_event_kp); - pr_info("ksud: input_event_kp: %d\n", ret); + ret = register_kprobe(&input_event_kp); + pr_info("ksud: input_event_kp: %d\n", ret); - INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook); - INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook); - INIT_WORK(&stop_input_hook_work, do_stop_input_hook); + INIT_WORK(&stop_vfs_read_work, do_stop_vfs_read_hook); + INIT_WORK(&stop_execve_hook_work, do_stop_execve_hook); + INIT_WORK(&stop_input_hook_work, do_stop_input_hook); #endif } void ksu_ksud_exit() { -#ifdef CONFIG_KSU_KPROBES_HOOK - unregister_kprobe(&execve_kp); - // this should be done before unregister vfs_read_kp - // unregister_kprobe(&vfs_read_kp); - unregister_kprobe(&input_event_kp); +#ifdef KSU_KPROBES_HOOK + unregister_kprobe(&execve_kp); + // this should be done before unregister vfs_read_kp + // unregister_kprobe(&vfs_read_kp); + unregister_kprobe(&input_event_kp); #endif - - is_boot_phase = false; + is_boot_phase = false; } diff --git a/kernel/ksud.h b/kernel/ksud.h index 7dd4cd0..271c354 100644 --- a/kernel/ksud.h +++ b/kernel/ksud.h @@ -3,13 +3,17 @@ #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); -extern u32 ksu_devpts_sid; - -extern bool ksu_execveat_hook __read_mostly; -extern int ksu_handle_pre_ksud(const char *filename); +extern u32 ksu_file_sid; +extern bool ksu_module_mounted; +extern bool ksu_boot_completed; #endif diff --git a/kernel/manager.h b/kernel/manager.h index f27afbb..2421da7 100644 --- a/kernel/manager.h +++ b/kernel/manager.h @@ -13,30 +13,31 @@ extern void ksu_add_manager(uid_t uid, int signature_index); extern void ksu_remove_manager(uid_t uid); extern int ksu_get_manager_signature_index(uid_t uid); -static inline bool ksu_is_manager_uid_valid() +static inline bool ksu_is_manager_uid_valid(void) { - return ksu_manager_uid != KSU_INVALID_UID; + return ksu_manager_uid != KSU_INVALID_UID; } -static inline bool is_manager() +static inline bool is_manager(void) { - return unlikely(ksu_is_any_manager(current_uid().val) || - (ksu_manager_uid != KSU_INVALID_UID && ksu_manager_uid == current_uid().val)); + return unlikely(ksu_is_any_manager(current_uid().val) || + (ksu_manager_uid != KSU_INVALID_UID && ksu_manager_uid == current_uid().val)); } -static inline uid_t ksu_get_manager_uid() +static inline uid_t ksu_get_manager_uid(void) { - return ksu_manager_uid; + return ksu_manager_uid; } static inline void ksu_set_manager_uid(uid_t uid) { - ksu_manager_uid = uid; + ksu_manager_uid = uid; } -static inline void ksu_invalidate_manager_uid() +static inline void ksu_invalidate_manager_uid(void) { - ksu_manager_uid = KSU_INVALID_UID; + ksu_manager_uid = KSU_INVALID_UID; } -#endif \ No newline at end of file +int ksu_observer_init(void); +#endif diff --git a/kernel/manager_sign.h b/kernel/manager_sign.h index b7a8e8c..265ef35 100644 --- a/kernel/manager_sign.h +++ b/kernel/manager_sign.h @@ -9,5 +9,9 @@ #define EXPECTED_SIZE_OTHER 0x300 #define EXPECTED_HASH_OTHER "0000000000000000000000000000000000000000000000000000000000000000" +typedef struct { + unsigned size; + const char *sha256; +} apk_sign_key_t; -#endif /* MANAGER_SIGN_H */ \ No newline at end of file +#endif /* MANAGER_SIGN_H */ diff --git a/kernel/manual_su.c b/kernel/manual_su.c new file mode 100644 index 0000000..c132d31 --- /dev/null +++ b/kernel/manual_su.c @@ -0,0 +1,356 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "manual_su.h" +#include "ksu.h" +#include "allowlist.h" +#include "manager.h" +#include "app_profile.h" + +static bool current_verified = false; +static void ksu_cleanup_expired_tokens(void); +static bool is_current_verified(void); +static void add_pending_root(uid_t uid); + +static struct pending_uid pending_uids[MAX_PENDING] = {0}; +static int pending_cnt = 0; +static struct ksu_token_entry auth_tokens[MAX_TOKENS] = {0}; +static int token_count = 0; +static DEFINE_SPINLOCK(token_lock); + +static char* get_token_from_envp(void) +{ + struct mm_struct *mm; + char *envp_start, *envp_end; + char *env_ptr, *token = NULL; + unsigned long env_len; + char *env_copy = NULL; + + if (!current->mm) + return NULL; + + mm = current->mm; + + down_read(&mm->mmap_lock); + + envp_start = (char *)mm->env_start; + envp_end = (char *)mm->env_end; + env_len = envp_end - envp_start; + + if (env_len <= 0 || env_len > PAGE_SIZE * 32) { + up_read(&mm->mmap_lock); + return NULL; + } + + env_copy = kmalloc(env_len + 1, GFP_KERNEL); + if (!env_copy) { + up_read(&mm->mmap_lock); + return NULL; + } + + if (copy_from_user(env_copy, envp_start, env_len)) { + kfree(env_copy); + up_read(&mm->mmap_lock); + return NULL; + } + + up_read(&mm->mmap_lock); + + env_copy[env_len] = '\0'; + env_ptr = env_copy; + + while (env_ptr < env_copy + env_len) { + if (strncmp(env_ptr, KSU_TOKEN_ENV_NAME "=", strlen(KSU_TOKEN_ENV_NAME) + 1) == 0) { + char *token_start = env_ptr + strlen(KSU_TOKEN_ENV_NAME) + 1; + char *token_end = strchr(token_start, '\0'); + + if (token_end && (token_end - token_start) == KSU_TOKEN_LENGTH) { + token = kmalloc(KSU_TOKEN_LENGTH + 1, GFP_KERNEL); + if (token) { + memcpy(token, token_start, KSU_TOKEN_LENGTH); + token[KSU_TOKEN_LENGTH] = '\0'; + pr_info("manual_su: found auth token in environment\n"); + } + } + break; + } + + env_ptr += strlen(env_ptr) + 1; + } + + kfree(env_copy); + return token; +} + +static char* ksu_generate_auth_token(void) +{ + static char token_buffer[KSU_TOKEN_LENGTH + 1]; + unsigned long flags; + int i; + + ksu_cleanup_expired_tokens(); + + spin_lock_irqsave(&token_lock, flags); + + if (token_count >= MAX_TOKENS) { + for (i = 0; i < MAX_TOKENS - 1; i++) { + auth_tokens[i] = auth_tokens[i + 1]; + } + token_count = MAX_TOKENS - 1; + } + + for (i = 0; i < KSU_TOKEN_LENGTH; i++) { + u8 rand_byte; + get_random_bytes(&rand_byte, 1); + int char_type = rand_byte % 3; + if (char_type == 0) { + token_buffer[i] = 'A' + (rand_byte % 26); + } else if (char_type == 1) { + token_buffer[i] = 'a' + (rand_byte % 26); + } else { + token_buffer[i] = '0' + (rand_byte % 10); + } + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(auth_tokens[token_count].token, token_buffer, KSU_TOKEN_LENGTH + 1); +#else + strlcpy(auth_tokens[token_count].token, token_buffer, KSU_TOKEN_LENGTH + 1); +#endif + auth_tokens[token_count].expire_time = jiffies + KSU_TOKEN_EXPIRE_TIME * HZ; + auth_tokens[token_count].used = false; + token_count++; + + spin_unlock_irqrestore(&token_lock, flags); + + pr_info("manual_su: generated new auth token (expires in %d seconds)\n", KSU_TOKEN_EXPIRE_TIME); + return token_buffer; +} + +static bool ksu_verify_auth_token(const char *token) +{ + unsigned long flags; + bool valid = false; + int i; + + if (!token || strlen(token) != KSU_TOKEN_LENGTH) { + return false; + } + + spin_lock_irqsave(&token_lock, flags); + + for (i = 0; i < token_count; i++) { + if (!auth_tokens[i].used && + time_before(jiffies, auth_tokens[i].expire_time) && + strcmp(auth_tokens[i].token, token) == 0) { + + auth_tokens[i].used = true; + valid = true; + pr_info("manual_su: auth token verified successfully\n"); + break; + } + } + + spin_unlock_irqrestore(&token_lock, flags); + + if (!valid) { + pr_warn("manual_su: invalid or expired auth token\n"); + } + + return valid; +} + +static void ksu_cleanup_expired_tokens(void) +{ + unsigned long flags; + int i, j; + + spin_lock_irqsave(&token_lock, flags); + + for (i = 0; i < token_count; ) { + if (time_after(jiffies, auth_tokens[i].expire_time) || auth_tokens[i].used) { + for (j = i; j < token_count - 1; j++) { + auth_tokens[j] = auth_tokens[j + 1]; + } + token_count--; + pr_debug("manual_su: cleaned up expired/used token\n"); + } else { + i++; + } + } + + spin_unlock_irqrestore(&token_lock, flags); +} + +static int handle_token_generation(struct manual_su_request *request) +{ + if (current_uid().val > 2000) { + pr_warn("manual_su: token generation denied for app UID %d\n", current_uid().val); + return -EPERM; + } + + char *new_token = ksu_generate_auth_token(); + if (!new_token) { + pr_err("manual_su: failed to generate token\n"); + return -ENOMEM; + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(request->token_buffer, new_token, KSU_TOKEN_LENGTH + 1); +#else + strlcpy(request->token_buffer, new_token, KSU_TOKEN_LENGTH + 1); +#endif + + pr_info("manual_su: auth token generated successfully\n"); + return 0; +} + +static int handle_escalation_request(struct manual_su_request *request) +{ + uid_t target_uid = request->target_uid; + pid_t target_pid = request->target_pid; + struct task_struct *tsk; + + rcu_read_lock(); + tsk = pid_task(find_vpid(target_pid), PIDTYPE_PID); + if (!tsk || ksu_task_is_dead(tsk)) { + rcu_read_unlock(); + pr_err("cmd_su: PID %d is invalid or dead\n", target_pid); + return -ESRCH; + } + rcu_read_unlock(); + + if (current_uid().val == 0 || is_manager() || ksu_is_allow_uid_for_current(current_uid().val)) + goto allowed; + + char *env_token = get_token_from_envp(); + if (!env_token) { + pr_warn("manual_su: no auth token found in environment\n"); + return -EACCES; + } + + bool token_valid = ksu_verify_auth_token(env_token); + kfree(env_token); + + if (!token_valid) { + pr_warn("manual_su: token verification failed\n"); + return -EACCES; + } + +allowed: + current_verified = true; + escape_to_root_for_cmd_su(target_uid, target_pid); + return 0; +} + +static int handle_add_pending_request(struct manual_su_request *request) +{ + uid_t target_uid = request->target_uid; + + if (!is_current_verified()) { + pr_warn("manual_su: add_pending denied, not verified\n"); + return -EPERM; + } + + add_pending_root(target_uid); + current_verified = false; + pr_info("manual_su: pending root added for UID %d\n", target_uid); + return 0; +} + +int ksu_handle_manual_su_request(int option, struct manual_su_request *request) +{ + if (!request) { + pr_err("manual_su: invalid request pointer\n"); + return -EINVAL; + } + + switch (option) { + case MANUAL_SU_OP_GENERATE_TOKEN: + pr_info("manual_su: handling token generation request\n"); + return handle_token_generation(request); + + case MANUAL_SU_OP_ESCALATE: + pr_info("manual_su: handling escalation request for UID %d, PID %d\n", + request->target_uid, request->target_pid); + return handle_escalation_request(request); + + case MANUAL_SU_OP_ADD_PENDING: + pr_info("manual_su: handling add pending request for UID %d\n", request->target_uid); + return handle_add_pending_request(request); + + default: + pr_err("manual_su: unknown option %d\n", option); + return -EINVAL; + } +} + +static bool is_current_verified(void) +{ + return current_verified; +} + +bool is_pending_root(uid_t uid) +{ + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].use_count++; + pending_uids[i].remove_calls++; + return true; + } + } + return false; +} + +void remove_pending_root(uid_t uid) +{ + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].remove_calls++; + + if (pending_uids[i].remove_calls >= REMOVE_DELAY_CALLS) { + pending_uids[i] = pending_uids[--pending_cnt]; + pr_info("pending_root: removed UID %d after %d calls\n", uid, REMOVE_DELAY_CALLS); + ksu_temp_revoke_root_once(uid); + } else { + pr_info("pending_root: UID %d remove_call=%d (<%d)\n", + uid, pending_uids[i].remove_calls, REMOVE_DELAY_CALLS); + } + return; + } + } +} + +static void add_pending_root(uid_t uid) +{ + if (pending_cnt >= MAX_PENDING) { + pr_warn("pending_root: cache full\n"); + return; + } + for (int i = 0; i < pending_cnt; i++) { + if (pending_uids[i].uid == uid) { + pending_uids[i].use_count = 0; + pending_uids[i].remove_calls = 0; + return; + } + } + pending_uids[pending_cnt++] = (struct pending_uid){uid, 0}; + ksu_temp_grant_root_once(uid); + pr_info("pending_root: cached UID %d\n", uid); +} + +void ksu_try_escalate_for_uid(uid_t uid) +{ + if (!is_pending_root(uid)) + return; + + pr_info("pending_root: UID=%d temporarily allowed\n", uid); + remove_pending_root(uid); +} \ No newline at end of file diff --git a/kernel/manual_su.h b/kernel/manual_su.h new file mode 100644 index 0000000..419dbfc --- /dev/null +++ b/kernel/manual_su.h @@ -0,0 +1,49 @@ +#ifndef __KSU_MANUAL_SU_H +#define __KSU_MANUAL_SU_H + +#include +#include +#include + +#if LINUX_VERSION_CODE < KERNEL_VERSION(5, 7, 0) +#define mmap_lock mmap_sem +#endif + +#define ksu_task_is_dead(t) ((t)->exit_state != 0) + +#define MAX_PENDING 16 +#define REMOVE_DELAY_CALLS 150 +#define MAX_TOKENS 10 + +#define KSU_SU_VERIFIED_BIT (1UL << 0) +#define KSU_TOKEN_LENGTH 32 +#define KSU_TOKEN_ENV_NAME "KSU_AUTH_TOKEN" +#define KSU_TOKEN_EXPIRE_TIME 150 + +#define MANUAL_SU_OP_GENERATE_TOKEN 0 +#define MANUAL_SU_OP_ESCALATE 1 +#define MANUAL_SU_OP_ADD_PENDING 2 + +struct pending_uid { + uid_t uid; + int use_count; + int remove_calls; +}; + +struct manual_su_request { + uid_t target_uid; + pid_t target_pid; + char token_buffer[KSU_TOKEN_LENGTH + 1]; +}; + +struct ksu_token_entry { + char token[KSU_TOKEN_LENGTH + 1]; + unsigned long expire_time; + bool used; +}; + +int ksu_handle_manual_su_request(int option, struct manual_su_request *request); +bool is_pending_root(uid_t uid); +void remove_pending_root(uid_t uid); +void ksu_try_escalate_for_uid(uid_t uid); +#endif \ No newline at end of file diff --git a/kernel/pkg_observer.c b/kernel/pkg_observer.c new file mode 100644 index 0000000..b632cd1 --- /dev/null +++ b/kernel/pkg_observer.c @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: GPL-2.0 +#include +#include +#include +#include +#include +#include +#include +#include "klog.h" // IWYU pragma: keep +#include "ksu.h" +#include "throne_tracker.h" +#include "throne_comm.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); + if (ksu_uid_scanner_enabled) { + ksu_request_userspace_scan(); + } + 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"); +} \ No newline at end of file diff --git a/kernel/seccomp_cache.c b/kernel/seccomp_cache.c new file mode 100644 index 0000000..286b5ca --- /dev/null +++ b/kernel/seccomp_cache.c @@ -0,0 +1,69 @@ +#include +#include +#include +#include +#include +#include +#include +#include "klog.h" // IWYU pragma: keep +#include "seccomp_cache.h" + + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 2) // Android backport this feature in 5.10.2 +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 +} + +#endif \ No newline at end of file diff --git a/kernel/seccomp_cache.h b/kernel/seccomp_cache.h new file mode 100644 index 0000000..ce88328 --- /dev/null +++ b/kernel/seccomp_cache.h @@ -0,0 +1,12 @@ +#ifndef __KSU_H_SECCOMP_CACHE +#define __KSU_H_SECCOMP_CACHE + +#include +#include + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 2) // Android backport this feature in 5.10.2 +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 + +#endif \ No newline at end of file diff --git a/kernel/selinux/Makefile b/kernel/selinux/Makefile index 870750b..d35413d 100644 --- a/kernel/selinux/Makefile +++ b/kernel/selinux/Makefile @@ -2,15 +2,7 @@ obj-y += selinux.o obj-y += sepolicy.o obj-y += rules.o -ifeq ($(shell grep -q " current_sid(void)" $(srctree)/security/selinux/include/objsec.h; echo $$?),0) -ccflags-y += -DKSU_COMPAT_HAS_CURRENT_SID -endif - -ifeq ($(shell grep -q "struct selinux_state " $(srctree)/security/selinux/include/security.h; echo $$?),0) -ccflags-y += -DKSU_COMPAT_HAS_SELINUX_STATE -endif - -ccflags-y += -Wno-implicit-function-declaration -Wno-strict-prototypes -Wno-int-conversion +ccflags-y += -Wno-strict-prototypes -Wno-int-conversion ccflags-y += -Wno-declaration-after-statement -Wno-unused-function 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 diff --git a/kernel/selinux/rules.c b/kernel/selinux/rules.c index c60159c..98d7475 100644 --- a/kernel/selinux/rules.c +++ b/kernel/selinux/rules.c @@ -6,7 +6,7 @@ #include "selinux.h" #include "sepolicy.h" #include "ss/services.h" -#include "linux/lsm_audit.h" +#include "linux/lsm_audit.h" // IWYU pragma: keep #include "xfrm.h" #define SELINUX_POLICY_INSTEAD_SELINUX_SS @@ -18,119 +18,119 @@ static struct policydb *get_policydb(void) { - struct policydb *db; - struct selinux_policy *policy = selinux_state.policy; - db = &policy->policydb; - return db; + struct policydb *db; + struct selinux_policy *policy = selinux_state.policy; + db = &policy->policydb; + return db; } static DEFINE_MUTEX(ksu_rules); void apply_kernelsu_rules() { - struct policydb *db; + struct policydb *db; - if (!getenforce()) { - pr_info("SELinux permissive or disabled, apply rules!\n"); - } + if (!getenforce()) { + pr_info("SELinux permissive or disabled, apply rules!\n"); + } - mutex_lock(&ksu_rules); + mutex_lock(&ksu_rules); - db = get_policydb(); + db = get_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"); + 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); + // 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 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); - } + // 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); + } - // we need to save allowlist in /data/adb/ksu - ksu_allow(db, "kernel", "adb_data_file", "dir", ALL); - ksu_allow(db, "kernel", "adb_data_file", "file", ALL); - // we need to search /data/app - ksu_allow(db, "kernel", "apk_data_file", "file", "open"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "open"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "read"); - ksu_allow(db, "kernel", "apk_data_file", "dir", "search"); - // we may need to do mount on shell - ksu_allow(db, "kernel", "shell_data_file", "file", ALL); - // we need to read /data/system/packages.list - ksu_allow(db, "kernel", "kernel", "capability", "dac_override"); - // Android 10+: - // http://aospxref.com/android-12.0.0_r3/xref/system/sepolicy/private/file_contexts#512 - ksu_allow(db, "kernel", "packages_list_file", "file", ALL); - // Kernel 4.4 - ksu_allow(db, "kernel", "packages_list_file", "dir", ALL); - // Android 9-: - // http://aospxref.com/android-9.0.0_r61/xref/system/sepolicy/private/file_contexts#360 - ksu_allow(db, "kernel", "system_data_file", "file", ALL); - ksu_allow(db, "kernel", "system_data_file", "dir", ALL); - // our ksud triggered by init - ksu_allow(db, "init", "adb_data_file", "file", ALL); - ksu_allow(db, "init", "adb_data_file", "dir", ALL); // #1289 - ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL); - // we need to umount modules in zygote - ksu_allow(db, "zygote", "adb_data_file", "dir", "search"); + // we need to save allowlist in /data/adb/ksu + ksu_allow(db, "kernel", "adb_data_file", "dir", ALL); + ksu_allow(db, "kernel", "adb_data_file", "file", ALL); + // we need to search /data/app + ksu_allow(db, "kernel", "apk_data_file", "file", "open"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "open"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "read"); + ksu_allow(db, "kernel", "apk_data_file", "dir", "search"); + // we may need to do mount on shell + ksu_allow(db, "kernel", "shell_data_file", "file", ALL); + // we need to read /data/system/packages.list + ksu_allow(db, "kernel", "kernel", "capability", "dac_override"); + // Android 10+: + // http://aospxref.com/android-12.0.0_r3/xref/system/sepolicy/private/file_contexts#512 + ksu_allow(db, "kernel", "packages_list_file", "file", ALL); + // Kernel 4.4 + ksu_allow(db, "kernel", "packages_list_file", "dir", ALL); + // Android 9-: + // http://aospxref.com/android-9.0.0_r61/xref/system/sepolicy/private/file_contexts#360 + ksu_allow(db, "kernel", "system_data_file", "file", ALL); + ksu_allow(db, "kernel", "system_data_file", "dir", ALL); + // our ksud triggered by init + ksu_allow(db, "init", "adb_data_file", "file", ALL); + ksu_allow(db, "init", "adb_data_file", "dir", ALL); // #1289 + ksu_allow(db, "init", KERNEL_SU_DOMAIN, ALL, ALL); + // we need to umount modules in zygote + ksu_allow(db, "zygote", "adb_data_file", "dir", "search"); - // 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"); + // 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"); + // 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"); + // 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"); + // 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"); - // For mounting loop devices, mirrors, tmpfs - ksu_allow(db, "kernel", ALL, "file", "read"); - ksu_allow(db, "kernel", ALL, "file", "write"); + // For mounting loop devices, mirrors, tmpfs + ksu_allow(db, "kernel", ALL, "file", "read"); + ksu_allow(db, "kernel", ALL, "file", "write"); - // Allow all binder transactions - ksu_allow(db, ALL, KERNEL_SU_DOMAIN, "binder", ALL); + // 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"); - // https://android-review.googlesource.com/c/platform/system/logging/+/3725346 - ksu_dontaudit(db, "untrusted_app", KERNEL_SU_DOMAIN, "dir", "getattr"); + // https://android-review.googlesource.com/c/platform/system/logging/+/3725346 + ksu_dontaudit(db, "untrusted_app", KERNEL_SU_DOMAIN, "dir", "getattr"); - mutex_unlock(&ksu_rules); + mutex_unlock(&ksu_rules); } #define MAX_SEPOL_LEN 128 @@ -145,401 +145,333 @@ void apply_kernelsu_rules() #define CMD_TYPE_CHANGE 8 #define CMD_GENFSCON 9 -#ifdef CONFIG_64BIT struct sepol_data { - u32 cmd; - u32 subcmd; - u64 field_sepol1; - u64 field_sepol2; - u64 field_sepol3; - u64 field_sepol4; - u64 field_sepol5; - u64 field_sepol6; - u64 field_sepol7; + u32 cmd; + u32 subcmd; + char __user *sepol1; + char __user *sepol2; + char __user *sepol3; + char __user *sepol4; + char __user *sepol5; + char __user *sepol6; + char __user *sepol7; }; -#ifdef CONFIG_COMPAT -extern bool ksu_is_compat __read_mostly; -struct sepol_compat_data { - u32 cmd; - u32 subcmd; - u32 field_sepol1; - u32 field_sepol2; - u32 field_sepol3; - u32 field_sepol4; - u32 field_sepol5; - u32 field_sepol6; - u32 field_sepol7; -}; -#endif // CONFIG_COMPAT -#else -struct sepol_data { - u32 cmd; - u32 subcmd; - u32 field_sepol1; - u32 field_sepol2; - u32 field_sepol3; - u32 field_sepol4; - u32 field_sepol5; - u32 field_sepol6; - u32 field_sepol7; -}; -#endif // CONFIG_64BIT static int get_object(char *buf, char __user *user_object, size_t buf_sz, - char **object) + char **object) { - if (!user_object) { - *object = ALL; - return 0; - } + if (!user_object) { + *object = ALL; + return 0; + } - if (strncpy_from_user(buf, user_object, buf_sz) < 0) { - return -1; - } + if (strncpy_from_user(buf, user_object, buf_sz) < 0) { + return -EINVAL; + } - *object = buf; + *object = buf; - return 0; + return 0; } - +#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); + 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); + 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(); + selinux_xfrm_notify_policyload(); } int handle_sepolicy(unsigned long arg3, void __user *arg4) { - struct policydb *db; + struct policydb *db; - if (!arg4) { - return -1; - } + if (!arg4) { + return -EINVAL; + } - if (!getenforce()) { - pr_info("SELinux permissive or disabled when handle policy!\n"); - } - - u32 cmd, subcmd; - char __user *sepol1, *sepol2, *sepol3, *sepol4, *sepol5, *sepol6, *sepol7; + if (!getenforce()) { + pr_info("SELinux permissive or disabled when handle policy!\n"); + } -#if defined(CONFIG_64BIT) && defined(CONFIG_COMPAT) - if (unlikely(ksu_is_compat)) { - struct sepol_compat_data compat_data; - if (copy_from_user(&compat_data, arg4, sizeof(struct sepol_compat_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = compat_ptr(compat_data.field_sepol1); - sepol2 = compat_ptr(compat_data.field_sepol2); - sepol3 = compat_ptr(compat_data.field_sepol3); - sepol4 = compat_ptr(compat_data.field_sepol4); - sepol5 = compat_ptr(compat_data.field_sepol5); - sepol6 = compat_ptr(compat_data.field_sepol6); - sepol7 = compat_ptr(compat_data.field_sepol7); - cmd = compat_data.cmd; - subcmd = compat_data.subcmd; - } else { - struct sepol_data data; - if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = data.field_sepol1; - sepol2 = data.field_sepol2; - sepol3 = data.field_sepol3; - sepol4 = data.field_sepol4; - sepol5 = data.field_sepol5; - sepol6 = data.field_sepol6; - sepol7 = data.field_sepol7; - cmd = data.cmd; - subcmd = data.subcmd; - } -#else - // basically for full native, say (64BIT=y COMPAT=n) || (64BIT=n) - struct sepol_data data; - if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { - pr_err("sepol: copy sepol_data failed.\n"); - return -1; - } - sepol1 = data.field_sepol1; - sepol2 = data.field_sepol2; - sepol3 = data.field_sepol3; - sepol4 = data.field_sepol4; - sepol5 = data.field_sepol5; - sepol6 = data.field_sepol6; - sepol7 = data.field_sepol7; - cmd = data.cmd; - subcmd = data.subcmd; -#endif + struct sepol_data data; + if (copy_from_user(&data, arg4, sizeof(struct sepol_data))) { + pr_err("sepol: copy sepol_data failed.\n"); + return -EINVAL; + } - mutex_lock(&ksu_rules); + u32 cmd = data.cmd; + u32 subcmd = data.subcmd; - db = get_policydb(); + mutex_lock(&ksu_rules); - int ret = -1; - if (cmd == CMD_NORMAL_PERM) { - char src_buf[MAX_SEPOL_LEN]; - char tgt_buf[MAX_SEPOL_LEN]; - char cls_buf[MAX_SEPOL_LEN]; - char perm_buf[MAX_SEPOL_LEN]; + db = get_policydb(); - char *s, *t, *c, *p; - if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } + int ret = -EINVAL; + if (cmd == CMD_NORMAL_PERM) { + char src_buf[MAX_SEPOL_LEN]; + char tgt_buf[MAX_SEPOL_LEN]; + char cls_buf[MAX_SEPOL_LEN]; + char perm_buf[MAX_SEPOL_LEN]; - if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } + char *s, *t, *c, *p; + if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } - if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } + if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } - if (get_object(perm_buf, sepol4, sizeof(perm_buf), &p) < - 0) { - pr_err("sepol: copy perm failed.\n"); - goto exit; - } + if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } - bool success = false; - if (subcmd == 1) { - success = ksu_allow(db, s, t, c, p); - } else if (subcmd == 2) { - success = ksu_deny(db, s, t, c, p); - } else if (subcmd == 3) { - success = ksu_auditallow(db, s, t, c, p); - } else if (subcmd == 4) { - success = ksu_dontaudit(db, s, t, c, p); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - ret = success ? 0 : -1; + if (get_object(perm_buf, data.sepol4, sizeof(perm_buf), &p) < + 0) { + pr_err("sepol: copy perm failed.\n"); + goto exit; + } - } else if (cmd == CMD_XPERM) { - char src_buf[MAX_SEPOL_LEN]; - char tgt_buf[MAX_SEPOL_LEN]; - char cls_buf[MAX_SEPOL_LEN]; + bool success = false; + if (subcmd == 1) { + success = ksu_allow(db, s, t, c, p); + } else if (subcmd == 2) { + success = ksu_deny(db, s, t, c, p); + } else if (subcmd == 3) { + success = ksu_auditallow(db, s, t, c, p); + } else if (subcmd == 4) { + success = ksu_dontaudit(db, s, t, c, p); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + ret = success ? 0 : -EINVAL; - char __maybe_unused - operation[MAX_SEPOL_LEN]; // it is always ioctl now! - char perm_set[MAX_SEPOL_LEN]; + } else if (cmd == CMD_XPERM) { + char src_buf[MAX_SEPOL_LEN]; + char tgt_buf[MAX_SEPOL_LEN]; + char cls_buf[MAX_SEPOL_LEN]; - char *s, *t, *c; - if (get_object(src_buf, sepol1, sizeof(src_buf), &s) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (get_object(tgt_buf, sepol2, sizeof(tgt_buf), &t) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (get_object(cls_buf, sepol3, sizeof(cls_buf), &c) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(operation, sepol4, - sizeof(operation)) < 0) { - pr_err("sepol: copy operation failed.\n"); - goto exit; - } - if (strncpy_from_user(perm_set, sepol5, sizeof(perm_set)) < - 0) { - pr_err("sepol: copy perm_set failed.\n"); - goto exit; - } + char __maybe_unused + operation[MAX_SEPOL_LEN]; // it is always ioctl now! + char perm_set[MAX_SEPOL_LEN]; - bool success = false; - if (subcmd == 1) { - success = ksu_allowxperm(db, s, t, c, perm_set); - } else if (subcmd == 2) { - success = ksu_auditallowxperm(db, s, t, c, perm_set); - } else if (subcmd == 3) { - success = ksu_dontauditxperm(db, s, t, c, perm_set); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - ret = success ? 0 : -1; - } else if (cmd == CMD_TYPE_STATE) { - char src[MAX_SEPOL_LEN]; + char *s, *t, *c; + if (get_object(src_buf, data.sepol1, sizeof(src_buf), &s) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (get_object(tgt_buf, data.sepol2, sizeof(tgt_buf), &t) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (get_object(cls_buf, data.sepol3, sizeof(cls_buf), &c) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(operation, data.sepol4, + sizeof(operation)) < 0) { + pr_err("sepol: copy operation failed.\n"); + goto exit; + } + if (strncpy_from_user(perm_set, data.sepol5, sizeof(perm_set)) < + 0) { + pr_err("sepol: copy perm_set failed.\n"); + goto exit; + } - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } + bool success = false; + if (subcmd == 1) { + success = ksu_allowxperm(db, s, t, c, perm_set); + } else if (subcmd == 2) { + success = ksu_auditallowxperm(db, s, t, c, perm_set); + } else if (subcmd == 3) { + success = ksu_dontauditxperm(db, s, t, c, perm_set); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + ret = success ? 0 : -EINVAL; + } else if (cmd == CMD_TYPE_STATE) { + char src[MAX_SEPOL_LEN]; - bool success = false; - if (subcmd == 1) { - success = ksu_permissive(db, src); - } else if (subcmd == 2) { - success = ksu_enforce(db, src); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - if (success) - ret = 0; + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } - } else if (cmd == CMD_TYPE || cmd == CMD_TYPE_ATTR) { - char type[MAX_SEPOL_LEN]; - char attr[MAX_SEPOL_LEN]; + bool success = false; + if (subcmd == 1) { + success = ksu_permissive(db, src); + } else if (subcmd == 2) { + success = ksu_enforce(db, src); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + if (success) + ret = 0; - if (strncpy_from_user(type, sepol1, sizeof(type)) < 0) { - pr_err("sepol: copy type failed.\n"); - goto exit; - } - if (strncpy_from_user(attr, sepol2, sizeof(attr)) < 0) { - pr_err("sepol: copy attr failed.\n"); - goto exit; - } + } else if (cmd == CMD_TYPE || cmd == CMD_TYPE_ATTR) { + char type[MAX_SEPOL_LEN]; + char attr[MAX_SEPOL_LEN]; - bool success = false; - if (cmd == CMD_TYPE) { - success = ksu_type(db, type, attr); - } else { - success = ksu_typeattribute(db, type, attr); - } - if (!success) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; + if (strncpy_from_user(type, data.sepol1, sizeof(type)) < 0) { + pr_err("sepol: copy type failed.\n"); + goto exit; + } + if (strncpy_from_user(attr, data.sepol2, sizeof(attr)) < 0) { + pr_err("sepol: copy attr failed.\n"); + goto exit; + } - } else if (cmd == CMD_ATTR) { - char attr[MAX_SEPOL_LEN]; + bool success = false; + if (cmd == CMD_TYPE) { + success = ksu_type(db, type, attr); + } else { + success = ksu_typeattribute(db, type, attr); + } + if (!success) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; - if (strncpy_from_user(attr, sepol1, sizeof(attr)) < 0) { - pr_err("sepol: copy attr failed.\n"); - goto exit; - } - if (!ksu_attribute(db, attr)) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; + } else if (cmd == CMD_ATTR) { + char attr[MAX_SEPOL_LEN]; - } else if (cmd == CMD_TYPE_TRANSITION) { - char src[MAX_SEPOL_LEN]; - char tgt[MAX_SEPOL_LEN]; - char cls[MAX_SEPOL_LEN]; - char default_type[MAX_SEPOL_LEN]; - char object[MAX_SEPOL_LEN]; + if (strncpy_from_user(attr, data.sepol1, sizeof(attr)) < 0) { + pr_err("sepol: copy attr failed.\n"); + goto exit; + } + if (!ksu_attribute(db, attr)) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(default_type, sepol4, - sizeof(default_type)) < 0) { - pr_err("sepol: copy default_type failed.\n"); - goto exit; - } - char *real_object; - if (sepol5 == NULL) { - real_object = NULL; - } else { - if (strncpy_from_user(object, sepol5, - sizeof(object)) < 0) { - pr_err("sepol: copy object failed.\n"); - goto exit; - } - real_object = object; - } + } else if (cmd == CMD_TYPE_TRANSITION) { + char src[MAX_SEPOL_LEN]; + char tgt[MAX_SEPOL_LEN]; + char cls[MAX_SEPOL_LEN]; + char default_type[MAX_SEPOL_LEN]; + char object[MAX_SEPOL_LEN]; - bool success = ksu_type_transition(db, src, tgt, cls, - default_type, real_object); - if (success) - ret = 0; + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(default_type, data.sepol4, + sizeof(default_type)) < 0) { + pr_err("sepol: copy default_type failed.\n"); + goto exit; + } + char *real_object; + if (data.sepol5 == NULL) { + real_object = NULL; + } else { + if (strncpy_from_user(object, data.sepol5, + sizeof(object)) < 0) { + pr_err("sepol: copy object failed.\n"); + goto exit; + } + real_object = object; + } - } else if (cmd == CMD_TYPE_CHANGE) { - char src[MAX_SEPOL_LEN]; - char tgt[MAX_SEPOL_LEN]; - char cls[MAX_SEPOL_LEN]; - char default_type[MAX_SEPOL_LEN]; + bool success = ksu_type_transition(db, src, tgt, cls, + default_type, real_object); + if (success) + ret = 0; - if (strncpy_from_user(src, sepol1, sizeof(src)) < 0) { - pr_err("sepol: copy src failed.\n"); - goto exit; - } - if (strncpy_from_user(tgt, sepol2, sizeof(tgt)) < 0) { - pr_err("sepol: copy tgt failed.\n"); - goto exit; - } - if (strncpy_from_user(cls, sepol3, sizeof(cls)) < 0) { - pr_err("sepol: copy cls failed.\n"); - goto exit; - } - if (strncpy_from_user(default_type, sepol4, - sizeof(default_type)) < 0) { - pr_err("sepol: copy default_type failed.\n"); - goto exit; - } - bool success = false; - if (subcmd == 1) { - success = ksu_type_change(db, src, tgt, cls, - default_type); - } else if (subcmd == 2) { - success = ksu_type_member(db, src, tgt, cls, - default_type); - } else { - pr_err("sepol: unknown subcmd: %d\n", subcmd); - } - if (success) - ret = 0; - } else if (cmd == CMD_GENFSCON) { - char name[MAX_SEPOL_LEN]; - char path[MAX_SEPOL_LEN]; - char context[MAX_SEPOL_LEN]; - if (strncpy_from_user(name, sepol1, sizeof(name)) < 0) { - pr_err("sepol: copy name failed.\n"); - goto exit; - } - if (strncpy_from_user(path, sepol2, sizeof(path)) < 0) { - pr_err("sepol: copy path failed.\n"); - goto exit; - } - if (strncpy_from_user(context, sepol3, sizeof(context)) < - 0) { - pr_err("sepol: copy context failed.\n"); - goto exit; - } + } else if (cmd == CMD_TYPE_CHANGE) { + char src[MAX_SEPOL_LEN]; + char tgt[MAX_SEPOL_LEN]; + char cls[MAX_SEPOL_LEN]; + char default_type[MAX_SEPOL_LEN]; - if (!ksu_genfscon(db, name, path, context)) { - pr_err("sepol: %d failed.\n", cmd); - goto exit; - } - ret = 0; - } else { - pr_err("sepol: unknown cmd: %d\n", cmd); - } + if (strncpy_from_user(src, data.sepol1, sizeof(src)) < 0) { + pr_err("sepol: copy src failed.\n"); + goto exit; + } + if (strncpy_from_user(tgt, data.sepol2, sizeof(tgt)) < 0) { + pr_err("sepol: copy tgt failed.\n"); + goto exit; + } + if (strncpy_from_user(cls, data.sepol3, sizeof(cls)) < 0) { + pr_err("sepol: copy cls failed.\n"); + goto exit; + } + if (strncpy_from_user(default_type, data.sepol4, + sizeof(default_type)) < 0) { + pr_err("sepol: copy default_type failed.\n"); + goto exit; + } + bool success = false; + if (subcmd == 1) { + success = ksu_type_change(db, src, tgt, cls, + default_type); + } else if (subcmd == 2) { + success = ksu_type_member(db, src, tgt, cls, + default_type); + } else { + pr_err("sepol: unknown subcmd: %d\n", subcmd); + } + if (success) + ret = 0; + } else if (cmd == CMD_GENFSCON) { + char name[MAX_SEPOL_LEN]; + char path[MAX_SEPOL_LEN]; + char context[MAX_SEPOL_LEN]; + if (strncpy_from_user(name, data.sepol1, sizeof(name)) < 0) { + pr_err("sepol: copy name failed.\n"); + goto exit; + } + if (strncpy_from_user(path, data.sepol2, sizeof(path)) < 0) { + pr_err("sepol: copy path failed.\n"); + goto exit; + } + if (strncpy_from_user(context, data.sepol3, sizeof(context)) < + 0) { + pr_err("sepol: copy context failed.\n"); + goto exit; + } + + if (!ksu_genfscon(db, name, path, context)) { + pr_err("sepol: %d failed.\n", cmd); + goto exit; + } + ret = 0; + } else { + pr_err("sepol: unknown cmd: %d\n", cmd); + } exit: - mutex_unlock(&ksu_rules); + mutex_unlock(&ksu_rules); - // only allow and xallow needs to reset avc cache, but we cannot do that because - // we are in atomic context. so we just reset it every time. - reset_avc_cache(); + // only allow and xallow needs to reset avc cache, but we cannot do that because + // we are in atomic context. so we just reset it every time. + reset_avc_cache(); - return ret; -} + return ret; +} \ No newline at end of file diff --git a/kernel/selinux/selinux.c b/kernel/selinux/selinux.c index 17a25da..dfc4831 100644 --- a/kernel/selinux/selinux.c +++ b/kernel/selinux/selinux.c @@ -1,4 +1,6 @@ #include "selinux.h" +#include "linux/cred.h" +#include "linux/sched.h" #include "objsec.h" #include "linux/version.h" #include "../klog.h" // IWYU pragma: keep @@ -7,124 +9,146 @@ static int transive_to_domain(const char *domain) { - struct cred *cred; - struct task_security_struct *tsec; - u32 sid; - int error; + struct cred *cred; + struct task_security_struct *tsec; + u32 sid; + int error; - cred = (struct cred *)__task_cred(current); + cred = (struct cred *)__task_cred(current); - tsec = cred->security; - if (!tsec) { - pr_err("tsec == NULL!\n"); - return -1; - } + tsec = cred->security; + 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; + 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) { - if (transive_to_domain(domain)) { - pr_err("transive domain failed.\n"); - return; - } - - /* we didn't need this now, we have change selinux rules when boot! -if (!is_domain_permissive) { - if (set_domain_permissive() == 0) { - is_domain_permissive = true; - } -}*/ + if (transive_to_domain(domain)) { + pr_err("transive domain failed.\n"); + return; + } } void setenforce(bool enforce) { #ifdef CONFIG_SECURITY_SELINUX_DEVELOP - selinux_state.enforcing = enforce; + selinux_state.enforcing = enforce; #endif } bool getenforce() { #ifdef CONFIG_SECURITY_SELINUX_DISABLE - if (selinux_state.disabled) { - return false; - } + if (selinux_state.disabled) { + return false; + } #endif #ifdef CONFIG_SECURITY_SELINUX_DEVELOP - return selinux_state.enforcing; + return selinux_state.enforcing; #else - return true; + return true; #endif } -#if (LINUX_VERSION_CODE < KERNEL_VERSION(5, 10, 0)) && \ - !defined(KSU_COMPAT_HAS_CURRENT_SID) -/* - * get the subjective security ID of the current task - */ -static inline u32 current_sid(void) +#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) { - const struct task_security_struct *tsec = current_security(); - - return tsec->sid; + return security_secid_to_secctx(secid, &cp->context, &cp->len); } +static void __security_release_secctx(struct lsm_context *cp) +{ + return 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 +bool is_task_ksu_domain(const struct cred* cred) +{ + struct lsm_context ctx; + bool result; + if (!cred) { + return false; + } + const struct task_security_struct *tsec = selinux_cred(cred); + if (!tsec) { + return false; + } + int err = __security_secid_to_secctx(tsec->sid, &ctx); + if (err) { + return false; + } + result = strncmp(KERNEL_SU_DOMAIN, ctx.context, ctx.len) == 0; + __security_release_secctx(&ctx); + return result; +} + bool is_ksu_domain() { - char *domain; - u32 seclen; - bool result; - int err = security_secid_to_secctx(current_sid(), &domain, &seclen); - if (err) { - return false; - } - result = strncmp(KERNEL_SU_DOMAIN, domain, seclen) == 0; - security_release_secctx(domain, seclen); - return result; + current_sid(); + return is_task_ksu_domain(current_cred()); } -bool is_zygote(void *sec) +bool is_context(const struct cred* cred, const char* context) { - struct task_security_struct *tsec = (struct task_security_struct *)sec; - if (!tsec) { - return false; - } - char *domain; - u32 seclen; - bool result; - int err = security_secid_to_secctx(tsec->sid, &domain, &seclen); - if (err) { - return false; - } - result = strncmp("u:r:zygote:s0", domain, seclen) == 0; - security_release_secctx(domain, seclen); - return result; + if (!cred) { + return false; + } + const struct task_security_struct * tsec = selinux_cred(cred); + if (!tsec) { + return false; + } + struct lsm_context ctx; + bool result; + int err = __security_secid_to_secctx(tsec->sid, &ctx); + if (err) { + return false; + } + result = strncmp(context, ctx.context, ctx.len) == 0; + __security_release_secctx(&ctx); + return result; } -#define DEVPTS_DOMAIN "u:object_r:ksu_file:s0" - -u32 ksu_get_devpts_sid() +bool is_zygote(const struct cred* cred) { - u32 devpts_sid = 0; - int err = security_secctx_to_secid(DEVPTS_DOMAIN, strlen(DEVPTS_DOMAIN), - &devpts_sid); - if (err) { - pr_info("get devpts sid err %d\n", err); - } - return devpts_sid; + return is_context(cred, "u:r:zygote:s0"); +} + +bool is_init(const struct cred* cred) { + return is_context(cred, "u:r:init:s0"); +} + +#define KSU_FILE_DOMAIN "u:object_r:ksu_file:s0" + +u32 ksu_get_ksu_file_sid() +{ + u32 ksu_file_sid = 0; + int err = security_secctx_to_secid(KSU_FILE_DOMAIN, strlen(KSU_FILE_DOMAIN), + &ksu_file_sid); + if (err) { + pr_info("get ksufile sid err %d\n", err); + } + return ksu_file_sid; } diff --git a/kernel/selinux/selinux.h b/kernel/selinux/selinux.h index 88f1e7d..431e044 100644 --- a/kernel/selinux/selinux.h +++ b/kernel/selinux/selinux.h @@ -3,6 +3,7 @@ #include "linux/types.h" #include "linux/version.h" +#include "linux/cred.h" void setup_selinux(const char *); @@ -10,12 +11,18 @@ void setenforce(bool); bool getenforce(); +bool is_task_ksu_domain(const struct cred* cred); + bool is_ksu_domain(); -bool is_zygote(void *cred); +bool is_zygote(const struct cred* cred); + +bool is_init(const struct cred* cred); void apply_kernelsu_rules(); -u32 ksu_get_devpts_sid(); +u32 ksu_get_ksu_file_sid(); + +int handle_sepolicy(unsigned long arg3, void __user *arg4); #endif diff --git a/kernel/selinux/sepolicy.c b/kernel/selinux/sepolicy.c index 7759602..e31fc08 100644 --- a/kernel/selinux/sepolicy.c +++ b/kernel/selinux/sepolicy.c @@ -6,7 +6,6 @@ #include "sepolicy.h" #include "../klog.h" // IWYU pragma: keep #include "ss/symtab.h" -#include "../kernel_compat.h" // Add check Huawei Device #define KSU_SUPPORT_ADD_TYPE @@ -15,44 +14,44 @@ ////////////////////////////////////////////////////// static struct avtab_node *get_avtab_node(struct policydb *db, - struct avtab_key *key, - struct avtab_extended_perms *xperms); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + 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); + bool permissive); static void add_typeattribute_raw(struct policydb *db, struct type_datum *type, - struct type_datum *attr); + struct type_datum *attr); static bool add_typeattribute(struct policydb *db, const char *type, - const char *attr); + const char *attr); ////////////////////////////////////////////////////// // Implementation @@ -63,18 +62,18 @@ static bool add_typeattribute(struct policydb *db, const char *type, #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) + 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) + 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) + ksu_hash_for_each(htab->htable, htab->size, cur) #endif // symtab_search is introduced on 5.9.0: @@ -85,186 +84,186 @@ static bool add_typeattribute(struct policydb *db, const char *type, #endif #define avtab_for_each(avtab, cur) \ - ksu_hash_for_each(avtab.htable, avtab.nslot, 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_key *key, + struct avtab_extended_perms *xperms) { - struct avtab_node *node; + 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); - } + /* 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 = {}; - /* + 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); + 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; - } + 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; + 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) + 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; + 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 (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 (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 (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; - } + 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; + 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) + 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; + 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; - } - } + 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) @@ -275,183 +274,183 @@ static void add_rule_raw(struct policydb *db, struct type_datum *src, #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) + 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; + 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; + 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); - } - } + 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; + 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 *)(kmalloc( - sizeof(xperms), GFP_KERNEL)); - if (!datum->u.xperms) { - pr_err("alloc xperms failed\n"); - return; - } - memcpy(datum->u.xperms, &xperms, sizeof(xperms)); - } - } + if (datum->u.xperms == NULL) { + datum->u.xperms = + (struct avtab_extended_perms *)(kmalloc( + 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) + const char *c, const char *range, int effect, + bool invert) { - struct type_datum *src = NULL, *tgt = NULL; - struct class_datum *cls = NULL; + 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 (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 (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 (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; + 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; - } + 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; + 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) + const char *c, const char *d, int effect) { - struct type_datum *src, *tgt, *def; - struct class_datum *cls; + 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; - } + 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_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; + struct avtab_node *node = get_avtab_node(db, &key, NULL); + node->datum.u.data = def->value; - return true; + return true; } // 5.9.0 : static inline int hashtab_insert(struct hashtab *h, void *key, void @@ -460,287 +459,287 @@ static bool add_type_rule(struct policydb *db, const char *s, const char *t, #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; + const struct filename_trans_key *ft = k; + unsigned long hash; + unsigned int byte_num; + unsigned char focus; - hash = ft->ttype ^ ft->tclass; + hash = ft->ttype ^ ft->tclass; - byte_num = 0; - while ((focus = ft->name[byte_num++])) - hash = partial_name_hash(focus, hash); - return hash; + 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; + 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->ttype - ft2->ttype; + if (v) + return v; - v = ft1->tclass - ft2->tclass; - if (v) - return v; + v = ft1->tclass - ft2->tclass; + if (v) + return v; - return strcmp(ft1->name, ft2->name); + return strcmp(ft1->name, ft2->name); } static const struct hashtab_key_params filenametr_key_params = { - .hash = filenametr_hash, - .cmp = filenametr_cmp, + .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) + const char *t, const char *c, const char *d, + const char *o) { - struct type_datum *src, *tgt, *def; - struct class_datum *cls; + 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; - } + 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_key key; + key.ttype = tgt->value; + key.tclass = cls->value; + key.name = (char *)o; - struct filename_trans_datum *last = NULL; + 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; - } + 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(sizeof(*trans), - 1, GFP_ATOMIC); - struct filename_trans_key *new_key = - (struct filename_trans_key *)kmalloc(sizeof(*new_key), - GFP_ATOMIC); - *new_key = key; - new_key->name = kstrdup(key.name, GFP_ATOMIC); - trans->next = last; - trans->otype = def->value; - hashtab_insert(&db->filename_trans, new_key, trans, - filenametr_key_params); - } + if (trans == NULL) { + trans = (struct filename_trans_datum *)kcalloc(1 ,sizeof(*trans), + GFP_ATOMIC); + struct filename_trans_key *new_key = + (struct filename_trans_key *)kmalloc(sizeof(*new_key), + GFP_ATOMIC); + *new_key = key; + new_key->name = kstrdup(key.name, GFP_ATOMIC); + 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; + 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) + const char *path, const char *context) { - return false; + return false; } static void *ksu_realloc(void *old, size_t new_size, size_t old_size) { - // we can't use krealloc, because it may be read-only - void *new = kzalloc(new_size, GFP_ATOMIC); - if (!new) { - return NULL; - } - if (old_size) { - memcpy(new, old, old_size); - } - // we can't use kfree, because it may be read-only - // there maybe some leaks, maybe we can check ptr_write, but it's not a big deal - // kfree(old); - return new; + // we can't use krealloc, because it may be read-only + void *new = kzalloc(new_size, GFP_ATOMIC); + if (!new) { + return NULL; + } + if (old_size) { + memcpy(new, old, old_size); + } + // we can't use kfree, because it may be read-only + // there maybe some leaks, maybe we can check ptr_write, but it's not a big deal + // kfree(old); + return new; } 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; - } + 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_ATOMIC); - if (!type) { - pr_err("add_type: alloc type_datum failed.\n"); - return false; - } + u32 value = ++db->p_types.nprim; + type = (struct type_datum *)kzalloc(sizeof(struct type_datum), + GFP_ATOMIC); + if (!type) { + pr_err("add_type: alloc type_datum failed.\n"); + return false; + } - type->primary = 1; - type->value = value; - type->attribute = attr; + type->primary = 1; + type->value = value; + type->attribute = attr; - char *key = kstrdup(type_name, GFP_ATOMIC); - if (!key) { - pr_err("add_type: alloc key failed.\n"); - return false; - } + char *key = kstrdup(type_name, GFP_ATOMIC); + 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; - } + 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_realloc(db->type_attr_map_array, - value * sizeof(struct ebitmap), - (value - 1) * sizeof(struct ebitmap)); + struct ebitmap *new_type_attr_map_array = + ksu_realloc(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; - } + 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_realloc(db->type_val_to_struct, - sizeof(*db->type_val_to_struct) * value, - sizeof(*db->type_val_to_struct) * (value - 1)); + struct type_datum **new_type_val_to_struct = + ksu_realloc(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; - } + 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_realloc(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; - } + char **new_val_to_name_types = + ksu_realloc(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_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->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; + 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); - } + 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; + return true; } static bool set_type_state(struct policydb *db, const char *type_name, - bool permissive) + 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; + 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 type_datum *attr) { - struct ebitmap *sattr = &db->type_attr_map_array[type->value - 1]; - ebitmap_set_bit(sattr, attr->value - 1, 1); + 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); - } - } - } - }; + 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) + 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 *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; - } + 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; + add_typeattribute_raw(db, type_d, attr_d); + return true; } ////////////////////////////////////////////////////////////////////////// @@ -748,106 +747,106 @@ static bool add_typeattribute(struct policydb *db, const char *type, // 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); + 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); + return add_type(db, name, true); } bool ksu_permissive(struct policydb *db, const char *type) { - return set_type_state(db, type, true); + return set_type_state(db, type, true); } bool ksu_enforce(struct policydb *db, const char *type) { - return set_type_state(db, type, false); + 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); + return add_typeattribute(db, type, attr); } bool ksu_exists(struct policydb *db, const char *type) { - return symtab_search(&db->p_types, type) != NULL; + 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) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, false); + 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) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_ALLOWED, true); + 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) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITALLOW, false); + 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) + const char *cls, const char *perm) { - return add_rule(db, src, tgt, cls, perm, AVTAB_AUDITDENY, true); + 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) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_ALLOWED, - false); + 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) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_AUDITALLOW, - false); + 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) + const char *cls, const char *range) { - return add_xperm_rule(db, src, tgt, cls, range, AVTAB_XPERMS_DONTAUDIT, - false); + 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) + 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); - } + 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) + const char *cls, const char *def) { - return add_type_rule(db, src, tgt, cls, def, AVTAB_CHANGE); + 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) + const char *cls, const char *def) { - return add_type_rule(db, src, tgt, cls, def, AVTAB_MEMBER); + 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) + const char *ctx) { - return add_genfscon(db, fs_name, path, ctx); + return add_genfscon(db, fs_name, path, ctx); } diff --git a/kernel/selinux/sepolicy.h b/kernel/selinux/sepolicy.h index 675d149..fd062ce 100644 --- a/kernel/selinux/sepolicy.h +++ b/kernel/selinux/sepolicy.h @@ -15,32 +15,32 @@ 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); + const char *cls, const char *perm); bool ksu_deny(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); bool ksu_auditallow(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + const char *cls, const char *perm); bool ksu_dontaudit(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *perm); + 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); + const char *cls, const char *range); bool ksu_auditallowxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range); + const char *cls, const char *range); bool ksu_dontauditxperm(struct policydb *db, const char *src, const char *tgt, - const char *cls, const char *range); + 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); + 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); + 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); + 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); + const char *ctx); #endif diff --git a/kernel/setuid_hook.c b/kernel/setuid_hook.c new file mode 100644 index 0000000..44dffd9 --- /dev/null +++ b/kernel/setuid_hook.c @@ -0,0 +1,171 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "allowlist.h" +#include "setuid_hook.h" +#include "feature.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" +#include "app_profile.h" + +static bool ksu_enhanced_security_enabled = false; + +static int enhanced_security_feature_get(u64 *value) +{ + *value = ksu_enhanced_security_enabled ? 1 : 0; + return 0; +} + +static int enhanced_security_feature_set(u64 value) +{ + bool enable = value != 0; + ksu_enhanced_security_enabled = enable; + pr_info("enhanced_security: set to %d\n", enable); + return 0; +} + +static const struct ksu_feature_handler enhanced_security_handler = { + .feature_id = KSU_FEATURE_ENHANCED_SECURITY, + .name = "enhanced_security", + .get_handler = enhanced_security_feature_get, + .set_handler = enhanced_security_feature_set, +}; + +static inline bool is_allow_su() +{ + if (is_manager()) { + // we are manager, allow! + return true; + } + return ksu_is_allow_uid_for_current(current_uid().val); +} + +int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid) +{ + 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 old process is root, ignore it. + if (old_uid != 0 && ksu_enhanced_security_enabled) { + // disallow any non-ksu domain escalation from non-root to root! + // euid is what we care about here as it controls permission + if (unlikely(euid == 0)) { + if (!is_ksu_domain()) { + pr_warn("find suspicious EoP: %d %s, from %d to %d\n", + current->pid, current->comm, old_uid, new_uid); + force_sig(SIGKILL); + return 0; + } + } + // disallow appuid decrease to any other uid if it is not allowed to su + if (is_appuid(old_uid)) { + if (euid < current_euid().val && !ksu_is_allow_uid_for_current(old_uid)) { + pr_warn("find suspicious EoP: %d %s, from %d to %d\n", + current->pid, current->comm, old_uid, new_uid); + force_sig(SIGKILL); + return 0; + } + } + return 0; + } + + // if on private space, see if its possibly the manager + if (new_uid > PER_USER_RANGE && new_uid % PER_USER_RANGE == ksu_get_manager_uid()) { + ksu_set_manager_uid(new_uid); + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 10, 0) + if (ksu_get_manager_uid() == new_uid) { + pr_info("install fd for manager: %d\n", new_uid); + ksu_install_fd(); + 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); + 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); + } +#else + if (ksu_is_allow_uid_for_current(new_uid)) { + spin_lock_irq(¤t->sighand->siglock); + disable_seccomp(); + spin_unlock_irq(¤t->sighand->siglock); + + if (ksu_get_manager_uid() == new_uid) { + pr_info("install fd for ksu manager(uid=%d)\n", + new_uid); + ksu_install_fd(); + } + + return 0; + } +#endif + + // Handle kernel umount + ksu_handle_umount(old_uid, new_uid); + + return 0; +} + +void ksu_setuid_hook_init(void) +{ + ksu_kernel_umount_init(); + if (ksu_register_feature_handler(&enhanced_security_handler)) { + pr_err("Failed to register enhanced security feature handler\n"); + } +} + +void ksu_setuid_hook_exit(void) +{ + pr_info("ksu_core_exit\n"); + ksu_kernel_umount_exit(); + ksu_unregister_feature_handler(KSU_FEATURE_ENHANCED_SECURITY); +} diff --git a/kernel/setuid_hook.h b/kernel/setuid_hook.h new file mode 100644 index 0000000..fc5b93a --- /dev/null +++ b/kernel/setuid_hook.h @@ -0,0 +1,14 @@ +#ifndef __KSU_H_KSU_CORE +#define __KSU_H_KSU_CORE + +#include +#include +#include "apk_sign.h" +#include + +void ksu_setuid_hook_init(void); +void ksu_setuid_hook_exit(void); + +int ksu_handle_setresuid(uid_t ruid, uid_t euid, uid_t suid); + +#endif diff --git a/kernel/sucompat.c b/kernel/sucompat.c index ca94a60..009d610 100644 --- a/kernel/sucompat.c +++ b/kernel/sucompat.c @@ -1,337 +1,188 @@ -#include -#include +#include "linux/compiler.h" +#include "linux/printk.h" #include #include -#include #include -#include #include #include #include #include +#include -#include "objsec.h" #include "allowlist.h" -#include "arch.h" +#include "feature.h" #include "klog.h" // IWYU pragma: keep #include "ksud.h" -#include "kernel_compat.h" +#include "sucompat.h" +#include "app_profile.h" +#include "syscall_hook_manager.h" + + +#include "sulog.h" #define SU_PATH "/system/bin/su" #define SH_PATH "/system/bin/sh" -extern void escape_to_root(); +bool ksu_su_compat_enabled __read_mostly = true; -#ifndef CONFIG_KSU_KPROBES_HOOK -static bool ksu_sucompat_hook_state __read_mostly = true; -#endif +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; + // 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; + return copy_to_user(p, d, len) ? NULL : p; } static char __user *sh_user_path(void) { - static const char sh_path[] = "/system/bin/sh"; + static const char sh_path[] = "/system/bin/sh"; - return userspace_stack_buffer(sh_path, sizeof(sh_path)); + return userspace_stack_buffer(sh_path, sizeof(sh_path)); } static char __user *ksud_user_path(void) { - static const char ksud_path[] = KSUD_PATH; + static const char ksud_path[] = KSUD_PATH; - return userspace_stack_buffer(ksud_path, sizeof(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) + int *__unused_flags) { - const char su[] = SU_PATH; + const char su[] = SU_PATH; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } + 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)))) { +#if __SULOG_GATE + ksu_sulog_report_syscall(current_uid().val, NULL, "faccessat", path); #endif + pr_info("faccessat su->sh!\n"); + *filename_user = sh_user_path(); + } - if (!ksu_is_allow_uid(current_uid().val)) { - return 0; - } - - char path[sizeof(su) + 1]; - memset(path, 0, sizeof(path)); - ksu_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; + 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; + // const char sh[] = SH_PATH; + const char su[] = SU_PATH; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } -#endif - if (!ksu_is_allow_uid(current_uid().val)) { - return 0; - } + if (!ksu_is_allow_uid_for_current(current_uid().val)) { + return 0; + } - if (unlikely(!filename_user)) { - return 0; - } + if (unlikely(!filename_user)) { + return 0; + } - char path[sizeof(su) + 1]; - memset(path, 0, sizeof(path)); + char path[sizeof(su) + 1]; + memset(path, 0, sizeof(path)); // Remove this later!! we use syscall hook, so this will never happen!!!!! #if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 18, 0) && 0 - // it becomes a `struct filename *` after 5.18 - // https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216 - const char sh[] = SH_PATH; - struct filename *filename = *((struct filename **)filename_user); - if (IS_ERR(filename)) { - return 0; - } - if (likely(memcmp(filename->name, su, sizeof(su)))) - return 0; - pr_info("vfs_statx su->sh!\n"); - memcpy((void *)filename->name, sh, sizeof(sh)); + // it becomes a `struct filename *` after 5.18 + // https://elixir.bootlin.com/linux/v5.18/source/fs/stat.c#L216 + const char sh[] = SH_PATH; + struct filename *filename = *((struct filename **)filename_user); + if (IS_ERR(filename)) { + return 0; + } + if (likely(memcmp(filename->name, su, sizeof(su)))) + return 0; + pr_info("vfs_statx su->sh!\n"); + memcpy((void *)filename->name, sh, sizeof(sh)); #else - ksu_strncpy_from_user_nofault(path, *filename_user, 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(); - } + if (unlikely(!memcmp(path, su, sizeof(su)))) { +#if __SULOG_GATE + ksu_sulog_report_syscall(current_uid().val, NULL, "newfstatat", path); +#endif + pr_info("newfstatat su->sh!\n"); + *filename_user = sh_user_path(); + } #endif - return 0; + return 0; } -int ksu_handle_execveat(int *fd, struct filename **filename_ptr, void *argv, - void *envp, int *flags) +int ksu_handle_execve_sucompat(const char __user **filename_user, + void *__never_use_argv, void *__never_use_envp, + int *__never_use_flags) { - return ksu_handle_execveat_sucompat(fd, filename_ptr, argv, envp, flags); -} + const char su[] = SU_PATH; + char path[sizeof(su) + 1]; -// the call from execve_handler_pre won't provided correct value for __never_use_argument, use them after fix execve_handler_pre, keeping them for consistence for manually patched code -int ksu_handle_execveat_sucompat(int *fd, struct filename **filename_ptr, - void *__never_use_argv, void *__never_use_envp, - int *__never_use_flags) -{ - struct filename *filename; - const char sh[] = KSUD_PATH; - const char su[] = SU_PATH; + if (unlikely(!filename_user)) + return 0; -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) { - return 0; - } -#endif - if (unlikely(!filename_ptr)) - return 0; + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - filename = *filename_ptr; - if (IS_ERR(filename)) { - return 0; - } + if (likely(memcmp(path, su, sizeof(su)))) + return 0; - if (likely(memcmp(filename->name, su, sizeof(su)))) - return 0; +#if __SULOG_GATE + bool is_allowed = ksu_is_allow_uid_for_current(current_uid().val); + ksu_sulog_report_syscall(current_uid().val, NULL, "execve", path); + + if (!is_allowed) + return 0; - if (!ksu_is_allow_uid(current_uid().val)) - return 0; - - pr_info("do_execveat_common su found\n"); - memcpy((void *)filename->name, sh, sizeof(sh)); - - escape_to_root(); - - return 0; -} - -int ksu_handle_execve_sucompat(int *fd, const char __user **filename_user, - void *__never_use_argv, void *__never_use_envp, - int *__never_use_flags) -{ - const char su[] = SU_PATH; - char path[sizeof(su) + 1]; - -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state){ - return 0; - } -#endif - if (unlikely(!filename_user)) - return 0; - - memset(path, 0, sizeof(path)); - ksu_strncpy_from_user_nofault(path, *filename_user, sizeof(path)); - - if (likely(memcmp(path, su, sizeof(su)))) - return 0; - - if (!ksu_is_allow_uid(current_uid().val)) - return 0; - - pr_info("sys_execve su found\n"); - *filename_user = ksud_user_path(); - - escape_to_root(); - - return 0; -} - -// dummified -int ksu_handle_devpts(struct inode *inode) -{ - return 0; -} - -int __ksu_handle_devpts(struct inode *inode) -{ - -#ifndef CONFIG_KSU_KPROBES_HOOK - if (!ksu_sucompat_hook_state) - return 0; -#endif - - if (!current->mm) { - return 0; - } - - uid_t uid = current_uid().val; - if (uid % 100000 < 10000) { - // not untrusted_app, ignore it - return 0; - } - - if (likely(!ksu_is_allow_uid(uid))) - return 0; - - struct inode_security_struct *sec = selinux_inode(inode); - - if (ksu_devpts_sid && sec) - sec->sid = ksu_devpts_sid; - - return 0; -} - -#ifdef CONFIG_KSU_KPROBES_HOOK -static int faccessat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int *dfd = (int *)&PT_REGS_PARM1(real_regs); - const char __user **filename_user = - (const char **)&PT_REGS_PARM2(real_regs); - int *mode = (int *)&PT_REGS_PARM3(real_regs); - - return ksu_handle_faccessat(dfd, filename_user, mode, NULL); -} - -static int newfstatat_handler_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct pt_regs *real_regs = PT_REAL_REGS(regs); - int *dfd = (int *)&PT_REGS_PARM1(real_regs); - const char __user **filename_user = - (const char **)&PT_REGS_PARM2(real_regs); - int *flags = (int *)&PT_REGS_SYSCALL_PARM4(real_regs); - - return ksu_handle_stat(dfd, filename_user, flags); -} - -static int 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); - - return ksu_handle_execve_sucompat(AT_FDCWD, filename_user, NULL, NULL, - NULL); -} - -static struct kprobe *su_kps[4]; -static int pts_unix98_lookup_pre(struct kprobe *p, struct pt_regs *regs) -{ - struct inode *inode; -#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 6, 0) - struct file *file = (struct file *)PT_REGS_PARM2(regs); - inode = file->f_path.dentry->d_inode; + ksu_sulog_report_su_attempt(current_uid().val, NULL, path, is_allowed); #else - inode = (struct inode *)PT_REGS_PARM2(regs); + if (!ksu_is_allow_uid_for_current(current_uid().val)) { + return 0; + } #endif - return ksu_handle_devpts(inode); + pr_info("sys_execve su found\n"); + *filename_user = ksud_user_path(); + + escape_with_root_profile(); + + return 0; } -static struct kprobe *init_kprobe(const char *name, - kprobe_pre_handler_t handler) -{ - struct kprobe *kp = kzalloc(sizeof(struct kprobe), GFP_KERNEL); - if (!kp) - return NULL; - kp->symbol_name = name; - kp->pre_handler = handler; - - int ret = register_kprobe(kp); - pr_info("sucompat: register_%s kprobe: %d\n", name, ret); - if (ret) { - kfree(kp); - return NULL; - } - - return kp; -} - -static void destroy_kprobe(struct kprobe **kp_ptr) -{ - struct kprobe *kp = *kp_ptr; - if (!kp) - return; - unregister_kprobe(kp); - synchronize_rcu(); - kfree(kp); - *kp_ptr = NULL; -} - -#endif - -// sucompat: permited process can execute 'su' to gain root access. +// sucompat: permitted process can execute 'su' to gain root access. void ksu_sucompat_init() { -#ifdef CONFIG_KSU_KPROBES_HOOK - su_kps[0] = init_kprobe(SYS_EXECVE_SYMBOL, execve_handler_pre); - su_kps[1] = init_kprobe(SYS_FACCESSAT_SYMBOL, faccessat_handler_pre); - su_kps[2] = init_kprobe(SYS_NEWFSTATAT_SYMBOL, newfstatat_handler_pre); - su_kps[3] = init_kprobe("pts_unix98_lookup", pts_unix98_lookup_pre); -#else - ksu_sucompat_hook_state = true; - pr_info("ksu_sucompat_init: hooks enabled: execve/execveat_su, faccessat, stat\n"); -#endif + if (ksu_register_feature_handler(&su_compat_handler)) { + pr_err("Failed to register su_compat feature handler\n"); + } } void ksu_sucompat_exit() { -#ifdef CONFIG_KSU_KPROBES_HOOK - int i; - for (i = 0; i < ARRAY_SIZE(su_kps); i++) { - destroy_kprobe(&su_kps[i]); - } -#else - ksu_sucompat_hook_state = false; - pr_info("ksu_sucompat_exit: hooks disabled: execve/execveat_su, faccessat, stat\n"); -#endif -} + ksu_unregister_feature_handler(KSU_FEATURE_SU_COMPAT); +} \ No newline at end of file diff --git a/kernel/sucompat.h b/kernel/sucompat.h new file mode 100644 index 0000000..82161f7 --- /dev/null +++ b/kernel/sucompat.h @@ -0,0 +1,18 @@ +#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 \ No newline at end of file diff --git a/kernel/sulog.c b/kernel/sulog.c new file mode 100644 index 0000000..ef2263f --- /dev/null +++ b/kernel/sulog.c @@ -0,0 +1,340 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" +#include "sulog.h" +#include "ksu.h" + +#if __SULOG_GATE + +struct dedup_entry dedup_tbl[SULOG_COMM_LEN]; +static DEFINE_SPINLOCK(dedup_lock); +static LIST_HEAD(sulog_queue); +static struct workqueue_struct *sulog_workqueue; +static struct work_struct sulog_work; +static bool sulog_enabled = true; + +static void get_timestamp(char *buf, size_t len) +{ + struct timespec64 ts; + struct tm tm; + + ktime_get_real_ts64(&ts); + time64_to_tm(ts.tv_sec - sys_tz.tz_minuteswest * 60, 0, &tm); + + snprintf(buf, len, "%04ld-%02d-%02d %02d:%02d:%02d", + tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, + tm.tm_hour, tm.tm_min, tm.tm_sec); +} + +static void ksu_get_cmdline(char *full_comm, const char *comm, size_t buf_len) +{ + if (!full_comm || buf_len <= 0) + return; + + if (comm && strlen(comm) > 0) { + KSU_STRSCPY(full_comm, comm, buf_len); + return; + } + + if (in_atomic() || in_interrupt() || irqs_disabled()) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + if (!current->mm) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + int n = get_cmdline(current, full_comm, buf_len); + if (n <= 0) { + KSU_STRSCPY(full_comm, current->comm, buf_len); + return; + } + + for (int i = 0; i < n && i < buf_len - 1; i++) { + if (full_comm[i] == '\0') + full_comm[i] = ' '; + } + full_comm[n < buf_len ? n : buf_len - 1] = '\0'; +} + +static void sanitize_string(char *str, size_t len) +{ + if (!str || len == 0) + return; + + size_t read_pos = 0, write_pos = 0; + + while (read_pos < len && str[read_pos] != '\0') { + char c = str[read_pos]; + + if (c == '\n' || c == '\r') { + read_pos++; + continue; + } + + if (c == ' ' && write_pos > 0 && str[write_pos - 1] == ' ') { + read_pos++; + continue; + } + + str[write_pos++] = c; + read_pos++; + } + + str[write_pos] = '\0'; +} + +static bool dedup_should_print(uid_t uid, u8 type, const char *content, size_t len) +{ + struct dedup_key key = { + .crc = dedup_calc_hash(content, len), + .uid = uid, + .type = type, + }; + u64 now = ktime_get_ns(); + u64 delta_ns = DEDUP_SECS * NSEC_PER_SEC; + + u32 idx = key.crc & (SULOG_COMM_LEN - 1); + spin_lock(&dedup_lock); + + struct dedup_entry *e = &dedup_tbl[idx]; + if (e->key.crc == key.crc && + e->key.uid == key.uid && + e->key.type == key.type && + (now - e->ts_ns) < delta_ns) { + spin_unlock(&dedup_lock); + return false; + } + + e->key = key; + e->ts_ns = now; + spin_unlock(&dedup_lock); + return true; +} + +static void sulog_work_handler(struct work_struct *work) +{ + struct file *fp; + struct sulog_entry *entry, *tmp; + LIST_HEAD(local_queue); + loff_t pos = 0; + unsigned long flags; + + spin_lock_irqsave(&dedup_lock, flags); + list_splice_init(&sulog_queue, &local_queue); + spin_unlock_irqrestore(&dedup_lock, flags); + + if (list_empty(&local_queue)) + return; + + fp = filp_open(SULOG_PATH, O_WRONLY | O_CREAT | O_APPEND, 0640); + if (IS_ERR(fp)) { + pr_err("sulog: failed to open log file: %ld\n", PTR_ERR(fp)); + goto cleanup; + } + + if (fp->f_inode->i_size > SULOG_MAX_SIZE) { + if (vfs_truncate(&fp->f_path, 0)) + pr_err("sulog: failed to truncate log file\n"); + pos = 0; + } else { + pos = fp->f_inode->i_size; + } + + list_for_each_entry(entry, &local_queue, list) + kernel_write(fp, entry->content, strlen(entry->content), &pos); + + vfs_fsync(fp, 0); + filp_close(fp, 0); + +cleanup: + list_for_each_entry_safe(entry, tmp, &local_queue, list) { + list_del(&entry->list); + kfree(entry); + } +} + +static void sulog_add_entry(char *log_buf, size_t len, uid_t uid, u8 dedup_type) +{ + struct sulog_entry *entry; + unsigned long flags; + + if (!sulog_enabled || !log_buf || len == 0) + return; + + if (!dedup_should_print(uid, dedup_type, log_buf, len)) + return; + + entry = kmalloc(sizeof(*entry), GFP_ATOMIC); + if (!entry) + return; + + KSU_STRSCPY(entry->content, log_buf, SULOG_ENTRY_MAX_LEN); + + spin_lock_irqsave(&dedup_lock, flags); + list_add_tail(&entry->list, &sulog_queue); + spin_unlock_irqrestore(&dedup_lock, flags); + + if (sulog_workqueue) + queue_work(sulog_workqueue, &sulog_work); +} + +void ksu_sulog_report_su_grant(uid_t uid, const char *comm, const char *method) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SU_GRANT: UID=%d COMM=%s METHOD=%s PID=%d\n", + timestamp, uid, full_comm, method ? method : "unknown", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SU_GRANT); +} + +void ksu_sulog_report_su_attempt(uid_t uid, const char *comm, const char *target_path, bool success) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SU_EXEC: UID=%d COMM=%s TARGET=%s RESULT=%s PID=%d\n", + timestamp, uid, full_comm, target_path ? target_path : "unknown", + success ? "SUCCESS" : "DENIED", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SU_ATTEMPT); +} + +void ksu_sulog_report_permission_check(uid_t uid, const char *comm, bool allowed) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] PERM_CHECK: UID=%d COMM=%s RESULT=%s PID=%d\n", + timestamp, uid, full_comm, allowed ? "ALLOWED" : "DENIED", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_PERM_CHECK); +} + +void ksu_sulog_report_manager_operation(const char *operation, uid_t manager_uid, uid_t target_uid) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, NULL, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] MANAGER_OP: OP=%s MANAGER_UID=%d TARGET_UID=%d COMM=%s PID=%d\n", + timestamp, operation ? operation : "unknown", manager_uid, target_uid, full_comm, current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), manager_uid, DEDUP_MANAGER_OP); +} + +void ksu_sulog_report_syscall(uid_t uid, const char *comm, const char *syscall, const char *args) +{ + char log_buf[SULOG_ENTRY_MAX_LEN]; + char timestamp[32]; + char full_comm[SULOG_COMM_LEN]; + + if (!sulog_enabled) + return; + + get_timestamp(timestamp, sizeof(timestamp)); + ksu_get_cmdline(full_comm, comm, sizeof(full_comm)); + + sanitize_string(full_comm, sizeof(full_comm)); + + snprintf(log_buf, sizeof(log_buf), + "[%s] SYSCALL: UID=%d COMM=%s SYSCALL=%s ARGS=%s PID=%d\n", + timestamp, uid, full_comm, syscall ? syscall : "unknown", + args ? args : "none", current->pid); + + sulog_add_entry(log_buf, strlen(log_buf), uid, DEDUP_SYSCALL); +} + +int ksu_sulog_init(void) +{ + sulog_workqueue = alloc_workqueue("ksu_sulog", WQ_UNBOUND | WQ_HIGHPRI, 1); + if (!sulog_workqueue) { + pr_err("sulog: failed to create workqueue\n"); + return -ENOMEM; + } + + INIT_WORK(&sulog_work, sulog_work_handler); + pr_info("sulog: initialized successfully\n"); + return 0; +} + +void ksu_sulog_exit(void) +{ + struct sulog_entry *entry, *tmp; + unsigned long flags; + + sulog_enabled = false; + + if (sulog_workqueue) { + flush_workqueue(sulog_workqueue); + destroy_workqueue(sulog_workqueue); + sulog_workqueue = NULL; + } + + spin_lock_irqsave(&dedup_lock, flags); + list_for_each_entry_safe(entry, tmp, &sulog_queue, list) { + list_del(&entry->list); + kfree(entry); + } + spin_unlock_irqrestore(&dedup_lock, flags); + + pr_info("sulog: cleaned up successfully\n"); +} + +#endif // __SULOG_GATE diff --git a/kernel/sulog.h b/kernel/sulog.h new file mode 100644 index 0000000..1569a8a --- /dev/null +++ b/kernel/sulog.h @@ -0,0 +1,93 @@ +#ifndef __KSU_SULOG_H +#define __KSU_SULOG_H + +#include +#include +#include // needed for function dedup_calc_hash + +#define __SULOG_GATE 1 + +#if __SULOG_GATE + +extern struct timezone sys_tz; + +#define SULOG_PATH "/data/adb/ksu/log/sulog.log" +#define SULOG_MAX_SIZE (128 * 1024 * 1024) // 128MB +#define SULOG_ENTRY_MAX_LEN 512 +#define SULOG_COMM_LEN 256 +#define DEDUP_SECS 10 + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 10, 0) +static inline size_t strlcpy(char *dest, const char *src, size_t size) +{ + return strscpy(dest, src, size); +} +#endif + +#define KSU_STRSCPY(dst, src, size) \ + do { \ + if (LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0)) { \ + strscpy(dst, src, size); \ + } else { \ + strlcpy(dst, src, size); \ + } \ + } while (0) + +#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 8, 0) +#include + +static inline void time64_to_tm(time64_t totalsecs, int offset, struct tm *result) +{ + struct rtc_time rtc_tm; + rtc_time64_to_tm(totalsecs, &rtc_tm); + + result->tm_sec = rtc_tm.tm_sec; + result->tm_min = rtc_tm.tm_min; + result->tm_hour = rtc_tm.tm_hour; + result->tm_mday = rtc_tm.tm_mday; + result->tm_mon = rtc_tm.tm_mon; + result->tm_year = rtc_tm.tm_year; +} +#endif + +struct dedup_key { + u32 crc; + uid_t uid; + u8 type; + u8 _pad[1]; +}; + +struct dedup_entry { + struct dedup_key key; + u64 ts_ns; +}; + +enum { + DEDUP_SU_GRANT = 0, + DEDUP_SU_ATTEMPT, + DEDUP_PERM_CHECK, + DEDUP_MANAGER_OP, + DEDUP_SYSCALL, +}; + +static inline u32 dedup_calc_hash(const char *content, size_t len) +{ + return crc32(0, content, len); +} + +struct sulog_entry { + struct list_head list; + char content[SULOG_ENTRY_MAX_LEN]; +}; + +void ksu_sulog_report_su_grant(uid_t uid, const char *comm, const char *method); +void ksu_sulog_report_su_attempt(uid_t uid, const char *comm, const char *target_path, bool success); +void ksu_sulog_report_permission_check(uid_t uid, const char *comm, bool allowed); +void ksu_sulog_report_manager_operation(const char *operation, uid_t manager_uid, uid_t target_uid); +void ksu_sulog_report_syscall(uid_t uid, const char *comm, const char *syscall, const char *args); + +int ksu_sulog_init(void); +void ksu_sulog_exit(void); +#endif // __SULOG_GATE + +#endif /* __KSU_SULOG_H */ diff --git a/kernel/supercalls.c b/kernel/supercalls.c new file mode 100644 index 0000000..9a91f14 --- /dev/null +++ b/kernel/supercalls.c @@ -0,0 +1,935 @@ +#include "supercalls.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "arch.h" +#include "allowlist.h" +#include "feature.h" +#include "klog.h" // IWYU pragma: keep +#include "ksud.h" +#include "manager.h" +#include "sulog.h" +#include "selinux/selinux.h" +#include "objsec.h" +#include "file_wrapper.h" +#include "syscall_hook_manager.h" +#include "throne_comm.h" +#include "dynamic_manager.h" +#include "umount_manager.h" + +#ifdef CONFIG_KSU_MANUAL_SU +#include "manual_su.h" +#endif + +bool ksu_uid_scanner_enabled = false; + +// 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); +#if __SULOG_GATE + ksu_sulog_report_permission_check(current_uid().val, current->comm, is_allowed); +#endif + return is_allowed; +} + +static void init_uid_scanner(void) +{ + ksu_uid_init(); + do_load_throne_state(NULL); + + if (ksu_uid_scanner_enabled) { + int ret = ksu_throne_comm_init(); + if (ret != 0) { + pr_err("Failed to initialize throne communication: %d\n", ret); + } + } +} + +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 |= 0x1; +#endif + if (is_manager()) { + cmd.flags |= 0x2; + } + 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; + pr_info("post-fs-data triggered\n"); + on_post_fs_data(); + init_uid_scanner(); +#if __SULOG_GATE + ksu_sulog_init(); +#endif + ksu_dynamic_manager_init(); + } + break; + } + case EVENT_BOOT_COMPLETED: { + static bool boot_complete_lock = false; + if (!boot_complete_lock) { + boot_complete_lock = true; + 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(cmd.cmd, (void __user *)cmd.arg); +} + +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_get_allow_list(void __user *arg) +{ + struct ksu_get_allow_list_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + bool success = ksu_get_allow_list((int *)cmd.uids, (int *)&cmd.count, true); + + if (!success) { + return -EFAULT; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_allow_list: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_deny_list(void __user *arg) +{ + struct ksu_get_allow_list_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + return -EFAULT; + } + + bool success = ksu_get_allow_list((int *)cmd.uids, (int *)&cmd.count, false); + + if (!success) { + return -EFAULT; + } + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_deny_list: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +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_uid(void __user *arg) +{ + struct ksu_get_manager_uid_cmd cmd; + + cmd.uid = ksu_get_manager_uid(); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_manager_uid: 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; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("set_app_profile: copy_from_user failed\n"); + return -EFAULT; + } + + if (!ksu_set_app_profile(&cmd.profile, true)) { +#if __SULOG_GATE + ksu_sulog_report_manager_operation("SET_APP_PROFILE", + current_uid().val, cmd.profile.current_uid); +#endif + return -EFAULT; + } + + return 0; +} + +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; + int ret; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("get_wrapper_fd: copy_from_user failed\n"); + return -EFAULT; + } + + struct file* f = fget(cmd.fd); + if (!f) { + return -EBADF; + } + + struct ksu_file_wrapper *data = ksu_create_file_wrapper(f); + if (data == NULL) { + ret = -ENOMEM; + goto put_orig_file; + } + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(6, 12, 0) +#define getfd_secure anon_inode_create_getfd +#else +#define getfd_secure anon_inode_getfd_secure +#endif + ret = getfd_secure("[ksu_fdwrapper]", &data->ops, data, f->f_flags, NULL); + if (ret < 0) { + pr_err("ksu_fdwrapper: getfd failed: %d\n", ret); + goto put_wrapper_data; + } + struct file* pf = fget(ret); + + struct inode* wrapper_inode = file_inode(pf); + // copy original inode mode + wrapper_inode->i_mode = file_inode(f)->i_mode; + struct inode_security_struct *sec = selinux_inode(wrapper_inode); + if (sec) { + sec->sid = ksu_file_sid; + } + + fput(pf); + goto put_orig_file; +put_wrapper_data: + ksu_delete_file_wrapper(data); +put_orig_file: + fput(f); + + return ret; +} + +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; +} + +// 100. GET_FULL_VERSION - Get full version string +static int do_get_full_version(void __user *arg) +{ + struct ksu_get_full_version_cmd cmd = {0}; + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(cmd.version_full, KSU_VERSION_FULL, sizeof(cmd.version_full)); +#else + strlcpy(cmd.version_full, KSU_VERSION_FULL, sizeof(cmd.version_full)); +#endif + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_full_version: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +// 101. HOOK_TYPE - Get hook type +static int do_get_hook_type(void __user *arg) +{ + struct ksu_hook_type_cmd cmd = {0}; + const char *type = "Tracepoint"; + +#if defined(KSU_MANUAL_HOOK) + type = "Manual"; +#endif + +#if LINUX_VERSION_CODE >= KERNEL_VERSION(4, 13, 0) + strscpy(cmd.hook_type, type, sizeof(cmd.hook_type)); +#else + strlcpy(cmd.hook_type, type, sizeof(cmd.hook_type)); +#endif + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_hook_type: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +// 102. ENABLE_KPM - Check if KPM is enabled +static int do_enable_kpm(void __user *arg) +{ + struct ksu_enable_kpm_cmd cmd; + + cmd.enabled = IS_ENABLED(CONFIG_KPM); + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("enable_kpm: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_dynamic_manager(void __user *arg) +{ + struct ksu_dynamic_manager_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("dynamic_manager: copy_from_user failed\n"); + return -EFAULT; + } + + int ret = ksu_handle_dynamic_manager(&cmd.config); + if (ret) + return ret; + + if (cmd.config.operation == DYNAMIC_MANAGER_OP_GET && + copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("dynamic_manager: copy_to_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_get_managers(void __user *arg) +{ + struct ksu_get_managers_cmd cmd; + + int ret = ksu_get_active_managers(&cmd.manager_info); + if (ret) + return ret; + + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("get_managers: copy_from_user failed\n"); + return -EFAULT; + } + + return 0; +} + +static int do_enable_uid_scanner(void __user *arg) +{ + struct ksu_enable_uid_scanner_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("enable_uid_scanner: copy_from_user failed\n"); + return -EFAULT; + } + + switch (cmd.operation) { + case UID_SCANNER_OP_GET_STATUS: { + bool status = ksu_uid_scanner_enabled; + if (copy_to_user((void __user *)cmd.status_ptr, &status, sizeof(status))) { + pr_err("enable_uid_scanner: copy status failed\n"); + return -EFAULT; + } + break; + } + case UID_SCANNER_OP_TOGGLE: { + bool enabled = cmd.enabled; + + if (enabled == ksu_uid_scanner_enabled) { + pr_info("enable_uid_scanner: no need to change, already %s\n", + enabled ? "enabled" : "disabled"); + break; + } + + if (enabled) { + // Enable UID scanner + int ret = ksu_throne_comm_init(); + if (ret != 0) { + pr_err("enable_uid_scanner: failed to initialize: %d\n", ret); + return -EFAULT; + } + pr_info("enable_uid_scanner: enabled\n"); + } else { + // Disable UID scanner + ksu_throne_comm_exit(); + pr_info("enable_uid_scanner: disabled\n"); + } + + ksu_uid_scanner_enabled = enabled; + ksu_throne_comm_save_state(); + break; + } + case UID_SCANNER_OP_CLEAR_ENV: { + // Clear environment (force exit) + ksu_throne_comm_exit(); + ksu_uid_scanner_enabled = false; + ksu_throne_comm_save_state(); + pr_info("enable_uid_scanner: environment cleared\n"); + break; + } + default: + pr_err("enable_uid_scanner: invalid operation\n"); + return -EINVAL; + } + + return 0; +} + +#ifdef CONFIG_KSU_MANUAL_SU +static bool system_uid_check(void) +{ + return current_uid().val <= 2000; +} + +static int do_manual_su(void __user *arg) +{ + struct ksu_manual_su_cmd cmd; + struct manual_su_request request; + int res; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("manual_su: copy_from_user failed\n"); + return -EFAULT; + } + + pr_info("manual_su request, option=%d, uid=%d, pid=%d\n", + cmd.option, cmd.target_uid, cmd.target_pid); + + memset(&request, 0, sizeof(request)); + request.target_uid = cmd.target_uid; + request.target_pid = cmd.target_pid; + + if (cmd.option == MANUAL_SU_OP_GENERATE_TOKEN || + cmd.option == MANUAL_SU_OP_ESCALATE) { + memcpy(request.token_buffer, cmd.token_buffer, sizeof(request.token_buffer)); + } + + res = ksu_handle_manual_su_request(cmd.option, &request); + + if (cmd.option == MANUAL_SU_OP_GENERATE_TOKEN && res == 0) { + memcpy(cmd.token_buffer, request.token_buffer, sizeof(cmd.token_buffer)); + if (copy_to_user(arg, &cmd, sizeof(cmd))) { + pr_err("manual_su: copy_to_user failed\n"); + return -EFAULT; + } + } + + return res; +} +#endif + +static int do_umount_manager(void __user *arg) +{ + struct ksu_umount_manager_cmd cmd; + + if (copy_from_user(&cmd, arg, sizeof(cmd))) { + pr_err("umount_manager: copy_from_user failed\n"); + return -EFAULT; + } + + switch (cmd.operation) { + case UMOUNT_OP_ADD: { + return ksu_umount_manager_add(cmd.path, cmd.check_mnt, cmd.flags, false); + } + case UMOUNT_OP_REMOVE: { + return ksu_umount_manager_remove(cmd.path); + } + case UMOUNT_OP_LIST: { + struct ksu_umount_entry_info __user *entries = + (struct ksu_umount_entry_info __user *)cmd.entries_ptr; + return ksu_umount_manager_get_entries(entries, &cmd.count); + } + case UMOUNT_OP_CLEAR_CUSTOM: { + return ksu_umount_manager_clear_custom(); + } + default: + return -EINVAL; + } +} + +// 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_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_UID, .name = "GET_MANAGER_UID", .handler = do_get_manager_uid, .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_GET_FULL_VERSION,.name = "GET_FULL_VERSION", .handler = do_get_full_version, .perm_check = always_allow}, + { .cmd = KSU_IOCTL_HOOK_TYPE,.name = "GET_HOOK_TYPE", .handler = do_get_hook_type, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_ENABLE_KPM, .name = "GET_ENABLE_KPM", .handler = do_enable_kpm, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_DYNAMIC_MANAGER, .name = "SET_DYNAMIC_MANAGER", .handler = do_dynamic_manager, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_GET_MANAGERS, .name = "GET_MANAGERS", .handler = do_get_managers, .perm_check = manager_or_root}, + { .cmd = KSU_IOCTL_ENABLE_UID_SCANNER, .name = "SET_ENABLE_UID_SCANNER", .handler = do_enable_uid_scanner, .perm_check = manager_or_root}, +#ifdef CONFIG_KSU_MANUAL_SU + { .cmd = KSU_IOCTL_MANUAL_SU, .name = "MANUAL_SU", .handler = do_manual_su, .perm_check = system_uid_check}, +#endif +#ifdef CONFIG_KPM + { .cmd = KSU_IOCTL_KPM, .name = "KPM_OPERATION", .handler = do_kpm, .perm_check = manager_or_root}, +#endif + { .cmd = KSU_IOCTL_UMOUNT_MANAGER, .name = "UMOUNT_MANAGER", .handler = do_umount_manager, .perm_check = manager_or_root}, + { .cmd = 0, .name = NULL, .handler = NULL, .perm_check = NULL} // Sentine +}; + +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); +} + +// downstream: make sure to pass arg as reference, this can allow us to extend things. +int ksu_handle_sys_reboot(int magic1, int magic2, unsigned int cmd, void __user **arg) +{ + struct ksu_install_fd_tw *tw; + + if (magic1 != KSU_INSTALL_MAGIC1) + return 0; + +#ifdef CONFIG_KSU_DEBUG + pr_info("sys_reboot: intercepted call! magic: 0x%x id: %d\n", magic1, magic2); +#endif + + // Check if this is a request to install KSU fd + if (magic2 == KSU_INSTALL_MAGIC2) { + tw = kzalloc(sizeof(*tw), GFP_ATOMIC); + if (!tw) + return 0; + + tw->outp = (int __user *)*arg; + 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; + } + + // extensions + + return 0; +} + +#ifdef KSU_KPROBES_HOOK +// Reboot hook for installing fd +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); + int cmd = (int)PT_REGS_PARM3(real_regs); + void __user **arg = (void __user **)&PT_REGS_SYSCALL_PARM4(real_regs); + + return ksu_handle_sys_reboot(magic1, magic2, cmd, arg); +} + +static struct kprobe reboot_kp = { + .symbol_name = REBOOT_SYMBOL, + .pre_handler = reboot_handler_pre, +}; +#endif + +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); + } +#ifdef KSU_KPROBES_HOOK + int rc = register_kprobe(&reboot_kp); + if (rc) { + pr_err("reboot kprobe failed: %d\n", rc); + } else { + pr_info("reboot kprobe registered successfully\n"); + } +#endif +} + +void ksu_supercalls_exit(void) { +#ifdef KSU_KPROBES_HOOK + unregister_kprobe(&reboot_kp); +#endif +} + +static inline void ksu_ioctl_audit(unsigned int cmd, const char *cmd_name, uid_t uid, int ret) +{ +#if __SULOG_GATE + const char *result = (ret == 0) ? "SUCCESS" : + (ret == -EPERM) ? "DENIED" : "FAILED"; + ksu_sulog_report_syscall(uid, NULL, cmd_name, result); +#endif +} + +// 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); + ksu_ioctl_audit(cmd, ksu_ioctl_handlers[i].name, + current_uid().val, -EPERM); + return -EPERM; + } + // Execute handler + int ret = ksu_ioctl_handlers[i].handler(argp); + ksu_ioctl_audit(cmd, ksu_ioctl_handlers[i].name, + current_uid().val, ret); + return ret; + } + } + + 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); + +#if __SULOG_GATE + ksu_sulog_report_permission_check(current_uid().val, current->comm, fd >= 0); +#endif + + pr_info("ksu fd installed: %d for pid %d\n", fd, current->pid); + + return fd; +} \ No newline at end of file diff --git a/kernel/supercalls.h b/kernel/supercalls.h new file mode 100644 index 0000000..c54585e --- /dev/null +++ b/kernel/supercalls.h @@ -0,0 +1,180 @@ +#ifndef __KSU_H_SUPERCALLS +#define __KSU_H_SUPERCALLS + +#include +#include +#include "ksu.h" +#include "app_profile.h" + +#ifdef CONFIG_KPM +#include "kpm/kpm.h" +#endif + +// 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) +}; + +struct ksu_get_info_cmd { + __u32 version; // Output: KERNEL_SU_VERSION + __u32 flags; // Output: flags (bit 0: MODULE mode) + __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 cmd; // Input: sepolicy command + __aligned_u64 arg; // Input: sepolicy argument pointer +}; + +struct ksu_check_safemode_cmd { + __u8 in_safe_mode; // Output: true if in safe mode, false otherwise +}; + +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_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_uid_cmd { + __u32 uid; // Output: manager UID +}; + +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 + +// Other command structures +struct ksu_get_full_version_cmd { + char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string +}; + +struct ksu_hook_type_cmd { + char hook_type[32]; // Output: hook type string +}; + +struct ksu_enable_kpm_cmd { + __u8 enabled; // Output: true if KPM is enabled +}; + +struct ksu_dynamic_manager_cmd { + struct dynamic_manager_user_config config; // Input/Output: dynamic manager config +}; + +struct ksu_get_managers_cmd { + struct manager_list_info manager_info; // Output: manager list information +}; + +struct ksu_enable_uid_scanner_cmd { + __u32 operation; // Input: operation type (UID_SCANNER_OP_GET_STATUS, UID_SCANNER_OP_TOGGLE, UID_SCANNER_OP_CLEAR_ENV) + __u32 enabled; // Input: enable or disable (for UID_SCANNER_OP_TOGGLE) + void __user *status_ptr; // Input: pointer to store status (for UID_SCANNER_OP_GET_STATUS) +}; + +#ifdef CONFIG_KSU_MANUAL_SU +struct ksu_manual_su_cmd { + __u32 option; // Input: operation type (MANUAL_SU_OP_GENERATE_TOKEN, MANUAL_SU_OP_ESCALATE, MANUAL_SU_OP_ADD_PENDING) + __u32 target_uid; // Input: target UID + __u32 target_pid; // Input: target PID + char token_buffer[33]; // Input/Output: token buffer +}; +#endif + +// 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_GET_ALLOW_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 6, 0) +#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 7, 0) +#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_UID _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) +// Other IOCTL command definitions +#define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0) +#define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0) +#define KSU_IOCTL_ENABLE_KPM _IOC(_IOC_READ, 'K', 102, 0) +#define KSU_IOCTL_DYNAMIC_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 103, 0) +#define KSU_IOCTL_GET_MANAGERS _IOC(_IOC_READ|_IOC_WRITE, 'K', 104, 0) +#define KSU_IOCTL_ENABLE_UID_SCANNER _IOC(_IOC_READ|_IOC_WRITE, 'K', 105, 0) +#ifdef CONFIG_KSU_MANUAL_SU +#define KSU_IOCTL_MANUAL_SU _IOC(_IOC_READ|_IOC_WRITE, 'K', 106, 0) +#endif +#define KSU_IOCTL_UMOUNT_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 107, 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 \ No newline at end of file diff --git a/kernel/syscall_hook_manager.c b/kernel/syscall_hook_manager.c new file mode 100644 index 0000000..14d258a --- /dev/null +++ b/kernel/syscall_hook_manager.c @@ -0,0 +1,374 @@ +#include "linux/compiler.h" +#include "linux/cred.h" +#include "linux/printk.h" +#include "selinux/selinux.h" +#include +#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" + +// 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: + case __NR_clone: + case __NR_clone3: + 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]; + + if (unlikely(!filename_user)) + return 0; + + memset(path, 0, sizeof(path)); + strncpy_from_user_nofault(path, *filename_user, sizeof(path)); + + if (likely(strstr(path, "/app_process") == NULL && strstr(path, "/adbd") == NULL && strstr(path, "/ksud") == NULL)) { + pr_info("hook_manager: unmark %d exec %s", current->pid, path); + ksu_clear_task_tracepoint_flag_if_needed(current); + } + + return 0; +} +#ifdef CONFIG_KSU_MANUAL_SU +#include "manual_su.h" +static inline void ksu_handle_task_alloc(struct pt_regs *regs) +{ + ksu_try_escalate_for_uid(current_uid().val); +} +#endif + +#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))) { +#ifdef KSU_TP_HOOK + 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; + } + } +#endif + + // 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; + } + +#ifdef CONFIG_KSU_MANUAL_SU + // Handle task_alloc via clone/fork + if (id == __NR_clone || id == __NR_clone3) + return ksu_handle_task_alloc(regs); +#endif + } +} +#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(); +} diff --git a/kernel/syscall_hook_manager.h b/kernel/syscall_hook_manager.h new file mode 100644 index 0000000..90245c2 --- /dev/null +++ b/kernel/syscall_hook_manager.h @@ -0,0 +1,47 @@ +#ifndef __KSU_H_HOOK_MANAGER +#define __KSU_H_HOOK_MANAGER + +#include +#include +#include +#include +#include +#include +#include +#include "selinux/selinux.h" + +// 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 diff --git a/kernel/throne_comm.c b/kernel/throne_comm.c index 97d8b18..90067dc 100644 --- a/kernel/throne_comm.c +++ b/kernel/throne_comm.c @@ -7,119 +7,208 @@ #include "klog.h" #include "throne_comm.h" +#include "ksu.h" #define PROC_UID_SCANNER "ksu_uid_scanner" +#define UID_SCANNER_STATE_FILE "/data/adb/ksu/.uid_scanner" static struct proc_dir_entry *proc_entry = NULL; static struct workqueue_struct *scanner_wq = NULL; static struct work_struct scan_work; +static struct work_struct ksu_state_save_work; +static struct work_struct ksu_state_load_work; + // Signal userspace to rescan static bool need_rescan = false; static void rescan_work_fn(struct work_struct *work) { - // Signal userspace through proc interface - need_rescan = true; - pr_info("requested userspace uid rescan\n"); + // Signal userspace through proc interface + need_rescan = true; + pr_info("requested userspace uid rescan\n"); } void ksu_request_userspace_scan(void) { - if (scanner_wq) { - queue_work(scanner_wq, &scan_work); - } + if (scanner_wq) { + queue_work(scanner_wq, &scan_work); + } } void ksu_handle_userspace_update(void) { - // Called when userspace notifies update complete - need_rescan = false; - pr_info("userspace uid list updated\n"); + // Called when userspace notifies update complete + need_rescan = false; + pr_info("userspace uid list updated\n"); +} + +static void do_save_throne_state(struct work_struct *work) +{ + struct file *fp; + char state_char = ksu_uid_scanner_enabled ? '1' : '0'; + loff_t off = 0; + + fp = filp_open(UID_SCANNER_STATE_FILE, O_WRONLY | O_CREAT | O_TRUNC, 0644); + if (IS_ERR(fp)) { + pr_err("save_throne_state create file failed: %ld\n", PTR_ERR(fp)); + return; + } + + if (kernel_write(fp, &state_char, sizeof(state_char), &off) != sizeof(state_char)) { + pr_err("save_throne_state write failed\n"); + goto exit; + } + + pr_info("throne state saved: %s\n", ksu_uid_scanner_enabled ? "enabled" : "disabled"); + +exit: + filp_close(fp, 0); +} + +void do_load_throne_state(struct work_struct *work) +{ + struct file *fp; + char state_char; + loff_t off = 0; + ssize_t ret; + + fp = filp_open(UID_SCANNER_STATE_FILE, O_RDONLY, 0); + if (IS_ERR(fp)) { + pr_info("throne state file not found, using default: disabled\n"); + ksu_uid_scanner_enabled = false; + return; + } + + ret = kernel_read(fp, &state_char, sizeof(state_char), &off); + if (ret != sizeof(state_char)) { + pr_err("load_throne_state read err: %zd\n", ret); + ksu_uid_scanner_enabled = false; + goto exit; + } + + ksu_uid_scanner_enabled = (state_char == '1'); + pr_info("throne state loaded: %s\n", ksu_uid_scanner_enabled ? "enabled" : "disabled"); + +exit: + filp_close(fp, 0); +} + +bool ksu_throne_comm_load_state(void) +{ + return ksu_queue_work(&ksu_state_load_work); +} + +void ksu_throne_comm_save_state(void) +{ + ksu_queue_work(&ksu_state_save_work); } static int uid_scanner_show(struct seq_file *m, void *v) { - if (need_rescan) { - seq_puts(m, "RESCAN\n"); - } else { - seq_puts(m, "OK\n"); - } - return 0; + if (need_rescan) { + seq_puts(m, "RESCAN\n"); + } else { + seq_puts(m, "OK\n"); + } + return 0; } static int uid_scanner_open(struct inode *inode, struct file *file) { - return single_open(file, uid_scanner_show, NULL); + return single_open(file, uid_scanner_show, NULL); } static ssize_t uid_scanner_write(struct file *file, const char __user *buffer, size_t count, loff_t *pos) { - char cmd[16]; - - if (count >= sizeof(cmd)) - return -EINVAL; - - if (copy_from_user(cmd, buffer, count)) - return -EFAULT; - - cmd[count] = '\0'; - - // Remove newline if present - if (count > 0 && cmd[count-1] == '\n') - cmd[count-1] = '\0'; - - if (strcmp(cmd, "UPDATED") == 0) { - ksu_handle_userspace_update(); - pr_info("received userspace update notification\n"); - } - - return count; + char cmd[16]; + + if (count >= sizeof(cmd)) + return -EINVAL; + + if (copy_from_user(cmd, buffer, count)) + return -EFAULT; + + cmd[count] = '\0'; + + // Remove newline if present + if (count > 0 && cmd[count-1] == '\n') + cmd[count-1] = '\0'; + + if (strcmp(cmd, "UPDATED") == 0) { + ksu_handle_userspace_update(); + pr_info("received userspace update notification\n"); + } + + return count; } +#ifdef KSU_COMPAT_HAS_PROC_OPS static const struct proc_ops uid_scanner_proc_ops = { .proc_open = uid_scanner_open, .proc_read = seq_read, - .proc_write = uid_scanner_write, + .proc_write = uid_scanner_write, .proc_lseek = seq_lseek, .proc_release = single_release, }; +#else +static const struct file_operations uid_scanner_proc_ops = { + .owner = THIS_MODULE, + .open = uid_scanner_open, + .read = seq_read, + .write = uid_scanner_write, + .llseek = seq_lseek, + .release = single_release, +}; +#endif int ksu_throne_comm_init(void) { - // Create workqueue - scanner_wq = alloc_workqueue("ksu_scanner", WQ_UNBOUND, 1); - if (!scanner_wq) { - pr_err("failed to create scanner workqueue\n"); - return -ENOMEM; - } - - INIT_WORK(&scan_work, rescan_work_fn); - - // Create proc entry - proc_entry = proc_create(PROC_UID_SCANNER, 0600, NULL, &uid_scanner_proc_ops); - if (!proc_entry) { - pr_err("failed to create proc entry\n"); - destroy_workqueue(scanner_wq); - return -ENOMEM; - } - - pr_info("throne communication initialized\n"); - return 0; + // Create workqueue + scanner_wq = alloc_workqueue("ksu_scanner", WQ_UNBOUND, 1); + if (!scanner_wq) { + pr_err("failed to create scanner workqueue\n"); + return -ENOMEM; + } + + INIT_WORK(&scan_work, rescan_work_fn); + + // Create proc entry + proc_entry = proc_create(PROC_UID_SCANNER, 0600, NULL, &uid_scanner_proc_ops); + if (!proc_entry) { + pr_err("failed to create proc entry\n"); + destroy_workqueue(scanner_wq); + return -ENOMEM; + } + + pr_info("throne communication initialized\n"); + return 0; } void ksu_throne_comm_exit(void) { - if (proc_entry) { - proc_remove(proc_entry); - proc_entry = NULL; - } - - if (scanner_wq) { - destroy_workqueue(scanner_wq); - scanner_wq = NULL; - } - - pr_info("throne communication cleaned up\n"); + if (proc_entry) { + proc_remove(proc_entry); + proc_entry = NULL; + } + + if (scanner_wq) { + destroy_workqueue(scanner_wq); + scanner_wq = NULL; + } + + pr_info("throne communication cleaned up\n"); +} + +int ksu_uid_init(void) +{ + INIT_WORK(&ksu_state_save_work, do_save_throne_state); + INIT_WORK(&ksu_state_load_work, do_load_throne_state); + return 0; +} + +void ksu_uid_exit(void) +{ + do_save_throne_state(NULL); } \ No newline at end of file diff --git a/kernel/throne_comm.h b/kernel/throne_comm.h index eedf8c1..4deba2a 100644 --- a/kernel/throne_comm.h +++ b/kernel/throne_comm.h @@ -9,4 +9,14 @@ int ksu_throne_comm_init(void); void ksu_throne_comm_exit(void); +int ksu_uid_init(void); + +void ksu_uid_exit(void); + +bool ksu_throne_comm_load_state(void); + +void ksu_throne_comm_save_state(void); + +void do_load_throne_state(struct work_struct *work); + #endif \ No newline at end of file diff --git a/kernel/throne_tracker.c b/kernel/throne_tracker.c index 45e07ad..dc32b79 100644 --- a/kernel/throne_tracker.c +++ b/kernel/throne_tracker.c @@ -10,232 +10,217 @@ #include "allowlist.h" #include "klog.h" // IWYU pragma: keep -#include "ksu.h" #include "manager.h" #include "throne_tracker.h" -#include "kernel_compat.h" +#include "apk_sign.h" #include "dynamic_manager.h" #include "throne_comm.h" uid_t ksu_manager_uid = KSU_INVALID_UID; +static uid_t locked_manager_uid = KSU_INVALID_UID; +static uid_t locked_dynamic_manager_uid = KSU_INVALID_UID; #define KSU_UID_LIST_PATH "/data/misc/user_uid/uid_list" -#define USER_DATA_PATH "/data/user_de/0" -#define USER_DATA_PATH_LEN 256 +#define SYSTEM_PACKAGES_LIST_PATH "/data/system/packages.list" struct uid_data { - struct list_head list; - u32 uid; - char package[KSU_MAX_PACKAGE_NAME]; + struct list_head list; + u32 uid; + char package[KSU_MAX_PACKAGE_NAME]; }; -// Try read whitelist first, fallback if failed -static int read_uid_whitelist(struct list_head *uid_list) +// Try read /data/misc/user_uid/uid_list +static int uid_from_um_list(struct list_head *uid_list) { - struct file *fp; - char *file_content = NULL; - char *line, *next_line; - loff_t file_size; - loff_t pos = 0; - int count = 0; - ssize_t bytes_read; - - fp = ksu_filp_open_compat(KSU_UID_LIST_PATH, O_RDONLY, 0); - if (IS_ERR(fp)) { - pr_info("whitelist not found, fallback needed\n"); - return -ENOENT; - } + struct file *fp; + char *buf = NULL; + loff_t size, pos = 0; + ssize_t nr; + int cnt = 0; - file_size = fp->f_inode->i_size; - if (file_size <= 0) { - pr_info("whitelist file is empty\n"); - filp_close(fp, NULL); - return -ENODATA; - } + fp = filp_open(KSU_UID_LIST_PATH, O_RDONLY, 0); + if (IS_ERR(fp)) + return -ENOENT; - file_content = kzalloc(file_size + 1, GFP_ATOMIC); - if (!file_content) { - pr_err("failed to allocate memory for whitelist file (%lld bytes)\n", file_size); - filp_close(fp, NULL); - return -ENOMEM; - } + size = fp->f_inode->i_size; + if (size <= 0) { + filp_close(fp, NULL); + return -ENODATA; + } - bytes_read = ksu_kernel_read_compat(fp, file_content, file_size, &pos); - if (bytes_read != file_size) { - pr_err("failed to read whitelist file: read %zd bytes, expected %lld bytes\n", - bytes_read, file_size); - kfree(file_content); - filp_close(fp, NULL); - return -EIO; - } + buf = kzalloc(size + 1, GFP_ATOMIC); + if (!buf) { + pr_err("uid_list: OOM %lld B\n", size); + filp_close(fp, NULL); + return -ENOMEM; + } - file_content[file_size] = '\0'; - filp_close(fp, NULL); + nr = kernel_read(fp, buf, size, &pos); + filp_close(fp, NULL); + if (nr != size) { + pr_err("uid_list: short read %zd/%lld\n", nr, size); + kfree(buf); + return -EIO; + } + buf[size] = '\0'; - pr_info("successfully read whitelist file (%lld bytes), parsing lines...\n", file_size); + for (char *line = buf, *next; line; line = next) { + next = strchr(line, '\n'); + if (next) *next++ = '\0'; - line = file_content; - while (line && *line) { - next_line = strchr(line, '\n'); - if (next_line) { - *next_line = '\0'; - next_line++; - } + while (*line == ' ' || *line == '\t' || *line == '\r') ++line; + if (!*line) continue; - char *trimmed_line = line; - while (*trimmed_line == ' ' || *trimmed_line == '\t' || *trimmed_line == '\r') { - trimmed_line++; - } + char *uid_str = strsep(&line, " \t"); + char *pkg = line; + if (!pkg) continue; + while (*pkg == ' ' || *pkg == '\t') ++pkg; + if (!*pkg) continue; - if (strlen(trimmed_line) > 0) { - char *line_copy = trimmed_line; - char *uid_str = strsep(&line_copy, " \t"); - char *package_name = line_copy; - - if (package_name) { - while (*package_name == ' ' || *package_name == '\t') { - package_name++; - } - } - - if (uid_str && package_name && strlen(package_name) > 0) { - u32 uid; - if (!kstrtou32(uid_str, 10, &uid)) { - struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC); - if (data) { - data->uid = uid; - size_t pkg_len = strlen(package_name); - size_t copy_len = min(pkg_len, (size_t)(KSU_MAX_PACKAGE_NAME - 1)); - strncpy(data->package, package_name, copy_len); - data->package[copy_len] = '\0'; - - list_add_tail(&data->list, uid_list); - count++; - - if (count % 100 == 0) { - pr_info("parsed %d packages so far...\n", count); - } - } else { - pr_err("failed to allocate memory for uid_data\n"); - } - } else { - pr_warn("invalid uid format in line: %s\n", trimmed_line); - } - } else { - pr_warn("invalid line format: %s\n", trimmed_line); - } - } + u32 uid; + if (kstrtou32(uid_str, 10, &uid)) { + pr_warn_once("uid_list: bad uid <%s>\n", uid_str); + continue; + } - line = next_line; - } - - kfree(file_content); - pr_info("successfully loaded %d uids from whitelist\n", count); - return count > 0 ? 0 : -ENODATA; + struct uid_data *d = kzalloc(sizeof(*d), GFP_ATOMIC); + if (unlikely(!d)) { + pr_err("uid_list: OOM uid=%u\n", uid); + continue; + } + + d->uid = uid; + strscpy(d->package, pkg, KSU_MAX_PACKAGE_NAME); + list_add_tail(&d->list, uid_list); + ++cnt; + } + + kfree(buf); + pr_info("uid_list: loaded %d entries\n", cnt); + return cnt > 0 ? 0 : -ENODATA; } static 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; + 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; + 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; - } - } - } + 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; + 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; + 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; + 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'; + // Copying the package name + strncpy(pkg, second_last_slash + 1, pkg_len); + pkg[pkg_len] = '\0'; - return 0; + return 0; } static void crown_manager(const char *apk, struct list_head *uid_data, int signature_index) { - 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; - } + 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, signature_index: %d\n", pkg, signature_index); + pr_info("manager pkg: %s, signature_index: %d\n", pkg, signature_index); #ifdef KSU_MANAGER_PACKAGE - // pkg is `/` - if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) { - pr_info("manager package is inconsistent with kernel build: %s\n", - KSU_MANAGER_PACKAGE); - return; - } + // pkg is `/` + if (strncmp(pkg, KSU_MANAGER_PACKAGE, sizeof(KSU_MANAGER_PACKAGE))) { + pr_info("manager package is inconsistent with kernel build: %s\n", + KSU_MANAGER_PACKAGE); + return; + } #endif - struct list_head *list = (struct list_head *)uid_data; - struct uid_data *np; + 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, signature_index=%d)\n", pkg, np->uid, signature_index); - - // Dynamic Sign index (1) or multi-manager signatures (2+) - if (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2) { - ksu_add_manager(np->uid, signature_index); - - if (!ksu_is_manager_uid_valid()) { - ksu_set_manager_uid(np->uid); - } - } else { - ksu_set_manager_uid(np->uid); - } - break; - } - } + list_for_each_entry(np, uid_data, list) { + if (strncmp(np->package, pkg, KSU_MAX_PACKAGE_NAME) == 0) { + bool is_dynamic = (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2); + + if (is_dynamic) { + if (locked_dynamic_manager_uid != KSU_INVALID_UID && locked_dynamic_manager_uid != np->uid) { + pr_info("Unlocking previous dynamic manager UID: %d\n", locked_dynamic_manager_uid); + ksu_remove_manager(locked_dynamic_manager_uid); + locked_dynamic_manager_uid = KSU_INVALID_UID; + } + } else { + if (locked_manager_uid != KSU_INVALID_UID && locked_manager_uid != np->uid) { + pr_info("Unlocking previous manager UID: %d\n", locked_manager_uid); + ksu_invalidate_manager_uid(); // unlock old one + locked_manager_uid = KSU_INVALID_UID; + } + } + + pr_info("Crowning %s manager: %s (uid=%d, signature_index=%d)\n", + is_dynamic ? "dynamic" : "traditional", pkg, np->uid, signature_index); + + if (is_dynamic) { + ksu_add_manager(np->uid, signature_index); + locked_dynamic_manager_uid = np->uid; + + // If there is no traditional manager, set it to the current UID + if (!ksu_is_manager_uid_valid()) { + ksu_set_manager_uid(np->uid); + locked_manager_uid = np->uid; + } + } else { + ksu_set_manager_uid(np->uid); // throne new UID + locked_manager_uid = np->uid; // store locked 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; + char dirpath[DATA_PATH_LEN]; + int depth; + struct list_head list; }; struct apk_path_hash { - unsigned int hash; - bool exists; - struct list_head list; + 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; + 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. @@ -248,414 +233,340 @@ struct my_dir_context { #define FILLDIR_ACTOR_CONTINUE 0 #define FILLDIR_ACTOR_STOP -EINVAL #endif - -struct uid_scan_stats { - size_t total_found; - size_t errors_encountered; -}; - -struct user_data_context { - struct dir_context ctx; - struct list_head *uid_list; - struct uid_scan_stats *stats; -}; - -FILLDIR_RETURN_TYPE user_data_actor(struct dir_context *ctx, const char *name, - int namelen, loff_t off, u64 ino, - unsigned int d_type) -{ - struct user_data_context *my_ctx = - container_of(ctx, struct user_data_context, ctx); - - if (!my_ctx || !my_ctx->uid_list) { - return FILLDIR_ACTOR_STOP; - } - - if (!strncmp(name, "..", namelen) || !strncmp(name, ".", namelen)) - return FILLDIR_ACTOR_CONTINUE; - - if (d_type != DT_DIR) - return FILLDIR_ACTOR_CONTINUE; - - if (namelen >= KSU_MAX_PACKAGE_NAME) { - pr_warn("Package name too long: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - char package_path[USER_DATA_PATH_LEN]; - if (snprintf(package_path, sizeof(package_path), "%s/%.*s", - USER_DATA_PATH, namelen, name) >= sizeof(package_path)) { - pr_err("Path too long for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct path path; - int err = kern_path(package_path, LOOKUP_FOLLOW, &path); - if (err) { - pr_debug("Package path lookup failed: %s (err: %d)\n", package_path, err); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct kstat stat; - err = vfs_getattr(&path, &stat, STATX_UID, AT_STATX_SYNC_AS_STAT); - path_put(&path); - - if (err) { - pr_debug("Failed to get attributes for: %s (err: %d)\n", package_path, err); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - uid_t uid = from_kuid(&init_user_ns, stat.uid); - if (uid == (uid_t)-1) { - pr_warn("Invalid UID for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - struct uid_data *data = kzalloc(sizeof(struct uid_data), GFP_ATOMIC); - if (!data) { - pr_err("Failed to allocate memory for package: %.*s\n", namelen, name); - if (my_ctx->stats) - my_ctx->stats->errors_encountered++; - return FILLDIR_ACTOR_CONTINUE; - } - - data->uid = uid; - size_t copy_len = min(namelen, KSU_MAX_PACKAGE_NAME - 1); - strncpy(data->package, name, copy_len); - data->package[copy_len] = '\0'; - - list_add_tail(&data->list, my_ctx->uid_list); - - if (my_ctx->stats) - my_ctx->stats->total_found++; - - pr_info("UserDE UID: Found package: %s, uid: %u\n", data->package, data->uid); - - return FILLDIR_ACTOR_CONTINUE; -} - -int scan_user_data_for_uids(struct list_head *uid_list) -{ - struct file *dir_file; - struct uid_scan_stats stats = {0}; - int ret = 0; - - if (!uid_list) { - return -EINVAL; - } - - dir_file = ksu_filp_open_compat(USER_DATA_PATH, O_RDONLY, 0); - if (IS_ERR(dir_file)) { - pr_err("UserDE UID: Failed to open %s: %ld\n", USER_DATA_PATH, PTR_ERR(dir_file)); - return PTR_ERR(dir_file); - } - - struct user_data_context ctx = { - .ctx.actor = user_data_actor, - .uid_list = uid_list, - .stats = &stats - }; - - ret = iterate_dir(dir_file, &ctx.ctx); - filp_close(dir_file, NULL); - - if (stats.errors_encountered > 0) { - pr_warn("Encountered %zu errors while scanning user data directory\n", - stats.errors_encountered); - } - - pr_info("UserDE UID: Scanned user data directory, found %zu packages with %zu errors\n", - stats.total_found, stats.errors_encountered); - - return ret; -} - FILLDIR_RETURN_TYPE my_actor(struct dir_context *ctx, const char *name, - int namelen, loff_t off, u64 ino, - unsigned int d_type) + 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]; + 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 (!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 (!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 (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 (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 = kmalloc(sizeof(struct data_path), GFP_ATOMIC); - if (d_type == DT_DIR && my_ctx->depth > 0 && - (my_ctx->stop && !*my_ctx->stop)) { - struct data_path *data = kmalloc(sizeof(struct data_path), GFP_ATOMIC); + if (!data) { + pr_err("Failed to allocate memory for %s\n", dirpath); + return FILLDIR_ACTOR_CONTINUE; + } - 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; +#if LINUX_VERSION_CODE < KERNEL_VERSION(4, 8, 0) + unsigned int hash = full_name_hash(dirpath, strlen(dirpath)); +#else + unsigned int hash = full_name_hash(NULL, dirpath, strlen(dirpath)); +#endif + list_for_each_entry(pos, &apk_path_hash_list, list) { + if (hash == pos->hash) { + pos->exists = true; + 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; - } - } + int signature_index = -1; + bool is_multi_manager = is_dynamic_manager_apk( + dirpath, &signature_index); - int signature_index = -1; - bool is_multi_manager = is_dynamic_manager_apk( - dirpath, &signature_index); + pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n", + dirpath, is_multi_manager, signature_index); + + // Check for dynamic sign or multi-manager signatures + if (is_multi_manager && (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2)) { + crown_manager(dirpath, my_ctx->private_data, signature_index); + + struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); + if (apk_data) { + apk_data->hash = hash; + apk_data->exists = true; + list_add_tail(&apk_data->list, &apk_path_hash_list); + } + } else if (is_manager_apk(dirpath)) { + crown_manager(dirpath, my_ctx->private_data, 0); + *my_ctx->stop = 1; - pr_info("Found new base.apk at path: %s, is_multi_manager: %d, signature_index: %d\n", - dirpath, is_multi_manager, signature_index); - - // Check for dynamic sign or multi-manager signatures - if (is_multi_manager && (signature_index == DYNAMIC_SIGN_INDEX || signature_index >= 2)) { - crown_manager(dirpath, my_ctx->private_data, signature_index); - - struct apk_path_hash *apk_data = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } + // 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 = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); + if (apk_data) { + apk_data->hash = hash; + apk_data->exists = true; + list_add_tail(&apk_data->list, &apk_path_hash_list); + } + } + } + } - } else if (is_manager_apk(dirpath)) { - crown_manager(dirpath, my_ctx->private_data, 0); - *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 = kmalloc(sizeof(struct apk_path_hash), GFP_ATOMIC); - if (apk_data) { - apk_data->hash = hash; - apk_data->exists = true; - list_add_tail(&apk_data->list, &apk_path_hash_list); - } - } - } - } - - return FILLDIR_ACTOR_CONTINUE; + 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; - } + 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); + // 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; + 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; + 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 = ksu_filp_open_compat(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; - } + 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; + } - iterate_dir(file, &ctx.ctx); - filp_close(file, NULL); - } -skip_iterate: - list_del(&pos->list); - if (pos != &data) - kfree(pos); - } - } + // 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; + } + } - // 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); - } - } + 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; + 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 % 100000 && - strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { - exist = true; - break; - } - } - return exist; + bool exist = false; + list_for_each_entry (np, list, list) { + if (np->uid == uid % 100000 && + strncmp(np->package, package, KSU_MAX_PACKAGE_NAME) == 0) { + exist = true; + break; + } + } + return exist; } -void track_throne() +void track_throne(bool prune_only) { - struct list_head uid_list; - INIT_LIST_HEAD(&uid_list); + struct list_head uid_list; + struct uid_data *np, *n; + struct file *fp; + char chr = 0; + loff_t pos = 0; + loff_t line_start = 0; + char buf[KSU_MAX_PACKAGE_NAME]; + static bool manager_exist = false; + static bool dynamic_manager_exist = false; + int current_manager_uid = ksu_get_manager_uid() % 100000; - pr_info("track_throne triggered, attempting whitelist read\n"); - - // Try read whitelist first - int ret = read_uid_whitelist(&uid_list); - - if (ret < 0) { - pr_info("whitelist read failed (%d), request userspace scan, falling back to user_de \n", ret); + // init uid list head + INIT_LIST_HEAD(&uid_list); - int ret_user = scan_user_data_for_uids(&uid_list); - - if (ret_user < 0) { - goto out; - } else { - pr_info("UserDE UID: Successfully loaded %zu packages from user data directory\n", list_count_nodes(&uid_list)); - } - - } else { - pr_info("loaded uids from whitelist successfully\n"); - } + if (ksu_uid_scanner_enabled) { + pr_info("Scanning %s directory..\n", KSU_UID_LIST_PATH); - // now update uid list - struct uid_data *np; - struct uid_data *n; + if (uid_from_um_list(&uid_list) == 0) { + pr_info("Loaded UIDs from %s success\n", KSU_UID_LIST_PATH); + goto uid_ready; + } - // first, check if manager_uid exist! - bool manager_exist = false; - bool dynamic_manager_exist = false; - - list_for_each_entry (np, &uid_list, list) { - // if manager is installed in work profile, the uid in packages.list is still equals main profile - // don't delete it in this case! - int manager_uid = ksu_get_manager_uid() % 100000; - if (np->uid == manager_uid) { - manager_exist = true; - break; - } - } - - // Check for dynamic managers - if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) { - list_for_each_entry (np, &uid_list, list) { - // Check if this uid is a dynamic manager (not the traditional manager) - if (ksu_is_any_manager(np->uid) && np->uid != ksu_get_manager_uid()) { - dynamic_manager_exist = true; - break; - } - } - } + pr_warn("%s read failed, fallback to %s\n", + KSU_UID_LIST_PATH, SYSTEM_PACKAGES_LIST_PATH); + } - if (!manager_exist) { - if (ksu_is_manager_uid_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"); - } else if (!dynamic_manager_exist && ksu_is_dynamic_manager_enabled()) { - // Always perform search when called from dynamic manager rescan - pr_info("Dynamic sign enabled, Searching manager...\n"); - search_manager("/data/app", 2, &uid_list); - pr_info("Search Dynamic sign manager finished\n"); - } + { + 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; + } + 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) { + pr_err("update_uid: package or uid is NULL!\n"); + break; + } + + u32 res; + if (kstrtou32(uid, 10, &res)) { + 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); + } + +uid_ready: + if (prune_only) + goto prune; + + // first, check if manager_uid exist! + list_for_each_entry(np, &uid_list, list) { + if (np->uid == current_manager_uid) { + manager_exist = true; + break; + } + } + + if (!manager_exist && locked_manager_uid != KSU_INVALID_UID) { + pr_info("Manager APK removed, unlock previous UID: %d\n", + locked_manager_uid); + ksu_invalidate_manager_uid(); + locked_manager_uid = KSU_INVALID_UID; + } + + // Check if the Dynamic Manager exists (only check locked UIDs) + if (ksu_is_dynamic_manager_enabled() && + locked_dynamic_manager_uid != KSU_INVALID_UID) { + list_for_each_entry(np, &uid_list, list) { + if (np->uid == locked_dynamic_manager_uid) { + dynamic_manager_exist = true; + break; + } + } + + if (!dynamic_manager_exist) { + pr_info("Dynamic manager APK removed, unlock previous UID: %d\n", + locked_dynamic_manager_uid); + ksu_remove_manager(locked_dynamic_manager_uid); + locked_dynamic_manager_uid = KSU_INVALID_UID; + } + } + + bool need_search = !manager_exist; + if (ksu_is_dynamic_manager_enabled() && !dynamic_manager_exist) + need_search = true; + + if (need_search) { + pr_info("Searching for manager(s)...\n"); + search_manager("/data/app", 2, &uid_list); + pr_info("Manager search finished\n"); + } + prune: - // then prune the allowlist - ksu_prune_allowlist(is_uid_exist, &uid_list); + // 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); - } + // free uid_list + list_for_each_entry_safe(np, n, &uid_list, list) { + list_del(&np->list); + kfree(np); + } } -void ksu_throne_tracker_init() +void ksu_throne_tracker_init(void) { - // nothing to do + // nothing to do } -void ksu_throne_tracker_exit() +void ksu_throne_tracker_exit(void) { - // nothing to do -} + // nothing to do +} \ No newline at end of file diff --git a/kernel/throne_tracker.h b/kernel/throne_tracker.h index 5d7f477..6be7d5f 100644 --- a/kernel/throne_tracker.h +++ b/kernel/throne_tracker.h @@ -5,6 +5,6 @@ void ksu_throne_tracker_init(); void ksu_throne_tracker_exit(); -void track_throne(); +void track_throne(bool prune_only); #endif diff --git a/kernel/umount_manager.c b/kernel/umount_manager.c new file mode 100644 index 0000000..5d1e603 --- /dev/null +++ b/kernel/umount_manager.c @@ -0,0 +1,278 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "klog.h" +#include "kernel_umount.h" +#include "umount_manager.h" + +static struct umount_manager g_umount_mgr = { + .entry_count = 0, + .max_entries = 64, +}; + +static void try_umount_path(struct umount_entry *entry) +{ + try_umount(entry->path, entry->check_mnt, entry->flags); +} + +static struct umount_entry *find_entry_locked(const char *path) +{ + struct umount_entry *entry; + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (strcmp(entry->path, path) == 0) { + return entry; + } + } + + return NULL; +} + +static int init_default_entries(void) +{ + int ret; + + const struct { + const char *path; + bool check_mnt; + int flags; + } defaults[] = { + { "/odm", true, 0 }, + { "/system", true, 0 }, + { "/vendor", true, 0 }, + { "/product", true, 0 }, + { "/system_ext", true, 0 }, + { "/data/adb/modules", false, MNT_DETACH }, + { "/debug_ramdisk", false, MNT_DETACH }, + }; + + for (int i = 0; i < ARRAY_SIZE(defaults); i++) { + ret = ksu_umount_manager_add(defaults[i].path, + defaults[i].check_mnt, + defaults[i].flags, + true); // is_default = true + if (ret) { + pr_err("Failed to add default entry: %s, ret=%d\n", + defaults[i].path, ret); + return ret; + } + } + + pr_info("Initialized %zu default umount entries\n", ARRAY_SIZE(defaults)); + return 0; +} + +int ksu_umount_manager_init(void) +{ + INIT_LIST_HEAD(&g_umount_mgr.entry_list); + spin_lock_init(&g_umount_mgr.lock); + + return init_default_entries(); +} + +void ksu_umount_manager_exit(void) +{ + struct umount_entry *entry, *tmp; + unsigned long flags; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry_safe(entry, tmp, &g_umount_mgr.entry_list, list) { + list_del(&entry->list); + kfree(entry); + g_umount_mgr.entry_count--; + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + pr_info("Umount manager cleaned up\n"); +} + +int ksu_umount_manager_add(const char *path, bool check_mnt, int flags, bool is_default) +{ + struct umount_entry *entry; + unsigned long irqflags; + int ret = 0; + + if (flags == -1) + flags = MNT_DETACH; + + if (!path || strlen(path) == 0 || strlen(path) >= 256) { + return -EINVAL; + } + + spin_lock_irqsave(&g_umount_mgr.lock, irqflags); + + if (g_umount_mgr.entry_count >= g_umount_mgr.max_entries) { + pr_err("Umount manager: max entries reached\n"); + ret = -ENOMEM; + goto out; + } + + if (find_entry_locked(path)) { + pr_warn("Umount manager: path already exists: %s\n", path); + ret = -EEXIST; + goto out; + } + + entry = kzalloc(sizeof(*entry), GFP_ATOMIC); + if (!entry) { + ret = -ENOMEM; + goto out; + } + + strncpy(entry->path, path, sizeof(entry->path) - 1); + entry->check_mnt = check_mnt; + entry->flags = flags; + entry->state = UMOUNT_STATE_IDLE; + entry->is_default = is_default; + entry->ref_count = 0; + + list_add_tail(&entry->list, &g_umount_mgr.entry_list); + g_umount_mgr.entry_count++; + + pr_info("Umount manager: added %s entry: %s\n", + is_default ? "default" : "custom", path); + +out: + spin_unlock_irqrestore(&g_umount_mgr.lock, irqflags); + return ret; +} + +int ksu_umount_manager_remove(const char *path) +{ + struct umount_entry *entry; + unsigned long flags; + int ret = 0; + + if (!path) { + return -EINVAL; + } + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + entry = find_entry_locked(path); + if (!entry) { + ret = -ENOENT; + goto out; + } + + if (entry->is_default) { + pr_err("Umount manager: cannot remove default entry: %s\n", path); + ret = -EPERM; + goto out; + } + + if (entry->state == UMOUNT_STATE_BUSY || entry->ref_count > 0) { + pr_err("Umount manager: entry is busy: %s\n", path); + ret = -EBUSY; + goto out; + } + + list_del(&entry->list); + g_umount_mgr.entry_count--; + kfree(entry); + + pr_info("Umount manager: removed entry: %s\n", path); + +out: + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return ret; +} + +void ksu_umount_manager_execute_all(const struct cred *cred) +{ + struct umount_entry *entry; + unsigned long flags; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->state == UMOUNT_STATE_IDLE) { + entry->ref_count++; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->ref_count > 0 && entry->state == UMOUNT_STATE_IDLE) { + try_umount_path(entry); + } + } + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (entry->ref_count > 0) { + entry->ref_count--; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); +} + +int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, u32 *count) +{ + struct umount_entry *entry; + struct ksu_umount_entry_info info; + unsigned long flags; + u32 idx = 0; + u32 max_count = *count; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry(entry, &g_umount_mgr.entry_list, list) { + if (idx >= max_count) { + break; + } + + memset(&info, 0, sizeof(info)); + strncpy(info.path, entry->path, sizeof(info.path) - 1); + info.check_mnt = entry->check_mnt; + info.flags = entry->flags; + info.is_default = entry->is_default; + info.state = entry->state; + info.ref_count = entry->ref_count; + + if (copy_to_user(&entries[idx], &info, sizeof(info))) { + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return -EFAULT; + } + + idx++; + } + + *count = idx; + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + return 0; +} + +int ksu_umount_manager_clear_custom(void) +{ + struct umount_entry *entry, *tmp; + unsigned long flags; + u32 cleared = 0; + + spin_lock_irqsave(&g_umount_mgr.lock, flags); + + list_for_each_entry_safe(entry, tmp, &g_umount_mgr.entry_list, list) { + if (!entry->is_default && entry->state == UMOUNT_STATE_IDLE && entry->ref_count == 0) { + list_del(&entry->list); + kfree(entry); + g_umount_mgr.entry_count--; + cleared++; + } + } + + spin_unlock_irqrestore(&g_umount_mgr.lock, flags); + + pr_info("Umount manager: cleared %u custom entries\n", cleared); + return 0; +} diff --git a/kernel/umount_manager.h b/kernel/umount_manager.h new file mode 100644 index 0000000..f0299b4 --- /dev/null +++ b/kernel/umount_manager.h @@ -0,0 +1,66 @@ +#ifndef __KSU_H_UMOUNT_MANAGER +#define __KSU_H_UMOUNT_MANAGER + +#include +#include +#include + +struct cred; + +enum umount_entry_state { + UMOUNT_STATE_IDLE = 0, + UMOUNT_STATE_ACTIVE = 1, + UMOUNT_STATE_BUSY = 2, +}; + +struct umount_entry { + struct list_head list; + char path[256]; + bool check_mnt; + int flags; + enum umount_entry_state state; + bool is_default; + u32 ref_count; +}; + +struct umount_manager { + struct list_head entry_list; + spinlock_t lock; + u32 entry_count; + u32 max_entries; +}; + +enum umount_manager_op { + UMOUNT_OP_ADD = 0, + UMOUNT_OP_REMOVE = 1, + UMOUNT_OP_LIST = 2, + UMOUNT_OP_CLEAR_CUSTOM = 3, +}; + +struct ksu_umount_manager_cmd { + __u32 operation; + char path[256]; + __u8 check_mnt; + __s32 flags; + __u32 count; + __aligned_u64 entries_ptr; +}; + +struct ksu_umount_entry_info { + char path[256]; + __u8 check_mnt; + __s32 flags; + __u8 is_default; + __u32 state; + __u32 ref_count; +}; + +int ksu_umount_manager_init(void); +void ksu_umount_manager_exit(void); +int ksu_umount_manager_add(const char *path, bool check_mnt, int flags, bool is_default); +int ksu_umount_manager_remove(const char *path); +void ksu_umount_manager_execute_all(const struct cred *cred); +int ksu_umount_manager_get_entries(struct ksu_umount_entry_info __user *entries, u32 *count); +int ksu_umount_manager_clear_custom(void); + +#endif // __KSU_H_UMOUNT_MANAGER diff --git a/manager/app/build.gradle.kts b/manager/app/build.gradle.kts index ee3b591..d51afa9 100644 --- a/manager/app/build.gradle.kts +++ b/manager/app/build.gradle.kts @@ -2,7 +2,6 @@ import com.android.build.gradle.internal.api.BaseVariantOutputImpl import com.android.build.gradle.tasks.PackageAndroidArtifact -import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { alias(libs.plugins.agp.app) @@ -17,6 +16,7 @@ plugins { val managerVersionCode: Int by rootProject.extra val managerVersionName: String by rootProject.extra +val androidCmakeVersion: String by rootProject.extra apksign { storeFileProperty = "KEYSTORE_FILE" @@ -51,15 +51,12 @@ android { } buildFeatures { + aidl = true buildConfig = true compose = true prefab = true } - kotlin { - jvmToolchain(21) - } - packaging { jniLibs { useLegacyPackaging = true @@ -77,7 +74,8 @@ android { externalNativeBuild { cmake { - path("src/main/cpp/CMakeLists.txt") + path = file("src/main/cpp/CMakeLists.txt") + version = androidCmakeVersion } } @@ -125,6 +123,7 @@ dependencies { implementation(libs.androidx.compose.ui.tooling.preview) implementation(libs.androidx.foundation) implementation(libs.androidx.documentfile) + implementation(libs.androidx.compose.foundation) debugImplementation(libs.androidx.compose.ui.test.manifest) debugImplementation(libs.androidx.compose.ui.tooling) diff --git a/manager/app/proguard-rules.pro b/manager/app/proguard-rules.pro index 8563805..18c49c1 100644 --- a/manager/app/proguard-rules.pro +++ b/manager/app/proguard-rules.pro @@ -43,4 +43,6 @@ -keep class com.dergoogler.mmrl.webui.interfaces.** { *; } -keep class com.sukisu.ultra.ui.webui.WebViewInterface { *; } --keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } \ No newline at end of file +-keep,allowobfuscation class * extends com.dergoogler.mmrl.platform.content.IService { *; } + +-keep interface com.sukisu.zako.** { *; } \ No newline at end of file diff --git a/manager/app/src/main/AndroidManifest.xml b/manager/app/src/main/AndroidManifest.xml index 62fd030..d4f5d53 100644 --- a/manager/app/src/main/AndroidManifest.xml +++ b/manager/app/src/main/AndroidManifest.xml @@ -18,31 +18,82 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" + android:requestLegacyExternalStorage="true" android:supportsRtl="true" android:theme="@style/Theme.KernelSU" - android:requestLegacyExternalStorage="true" tools:targetApi="34"> + + + + + + + + + + + + + + + + + + + + + + + - + + android:roundIcon="@mipmap/ic_launcher_alt_round"> + + + + + + + + + + + + + + + + + + + + getPackages(int start, int maxCount); +} \ No newline at end of file diff --git a/manager/app/src/main/assets/kpimg b/manager/app/src/main/assets/kpimg index 80125ec283a790c3879108a98105240f2a91e238..e64eb858d57407f000b86c38abf0d4379468da6e 100644 GIT binary patch literal 179808 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 a5ZSGaUQBUx&VvB;Vo-XlatEO$w*D{O(mB@v delta 46754 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

}#fyrIk*Ubxs2Re))4E9Nb( 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�u5RawGZutT~*NJ6IfQxq*hE2&g<+|v~o?No9zl$7Uc^%Dqr_W{u8vOE`MQvnp5 z03a7YVFzNJ^R?o?<5e zSWSt_&*J2oDR+WDn0H|zW*W*p2tXmJRK5}?Hiu%R{`PsJ!R1fp<*sYZos^RI9&Fo4 z9kT&EYXn-AonhnqT3Lgr#G2bNrfu1=P@_{gxCiwmv?zzMftI{?nbM!cTl^C50FE+?1R zsrvw=Wa?f6ij;d3K&;#xmb)0`%r52OrM6rO04cW?fRuX+fRy`hx9DTD<}^4_HzlyD zTg{+JE3<>-Rx?O`1Kxw_<5n|BUMshQW|hJ3>0k$q#s65L)pLk_y~_VEt|ThDbhLx! zHqkE^zN=Z^1_bkKV9KRdUZouoX9JL-(_(a0!(DL1uEg1E_0c>z-(76m5mWMMEC-(@P+NoBr zwwwZ@#zzN!EdhWdCfXSf&VxW^sE6?4n6=8c+A?oLcIV-&!$~U1P>HP2w-qOiF9?h) z27n``+f}{~K+scz&g2Z?#WDTLx1Y?DgHB$X1|(Ru(}T`FxQB}{oE%oJO4MbHV_YfRq;O5cdL zf-E5>oo0LGrl8~JN`e)O^vywsKQAVPA6OiOix2}mV#Nd2Ct&h($~=sdKnO+jMT*}U zY3eYp#8=`6S=+QMRr#inc^f{K(8gyf-k6%x~~Rrfp)ZyS4VGpmTw((=_ow&`D&j#t~e|;>oyCcD|*=YA#p7 z+ag>(#=s-iq5>7%Ol}^^UeP8Lk}NkL+*7RcgA!*&pcL5N+{pdX#+&g&Ur=ss;q!i=)NzKu5tK~QAC&tdt(F!I0OjyVtN9%> z&DUANZNj`z1s{uWlOr*+4vKB=4gQ1NDJYwpDHX=qT%m$LkUJIJ!ELx<6_j8;1>7Fi zdr6YJPt(CJ2`I-OC4SWQL(CIhRd6)9i@?3nwjjnWQ9;ph8MvEmYl1QsuK?w#&c?>z zmDF$xDEvktkC=vDDtHgMkAb_qqBXZv1^-4af6$m;$>b3$+d~CEA(uaBJh@H(AA|l+ z1oumDJKH5+tZaMqKe=ClJ1mk>Mnb)K8skrr#@FCJYOM;6qr4Bg}yeG}VKfY)4zL7v+b5QrM;yl`1%a+!5g3USe#Bc_3E> z=aM@P+=58tM*Vq|3N9s=KdpR%%`368St@uxx#xmAH^PlZeToYHo!mcxd$nz8EK?#E zeoiibVEM+f)>f6Npyc< zlqJh94mxkx`6Kxnh|k7AbbU1`S0$rp%44w0mjW`L-{6l+x+#UVd|AZ4^CcyAB0^o? zfRxdU#gs;zaaFYmW%!DRGc=W2`HF|rIqfe1)9Bi?Hvpx7h*Pj9N@r>BT)*q-b{$cl zgJqtJU7dE_faJjpH&fTERNkE%h20!?M8@MOO4Yl4Gm9wMUuD(1{WCigu2b37Ze55U z?O|ZLgH-59`29Tc1_wfW;We7z)~AH_qaAOPKEw;HgFYWC8k(a*{5VeRj2brD4P zGNIceL;kp3M9WhwS*1e!30r}?=}-wY3f$cn%E!nPxY|*v&qJ6?TcL;L-BPQM6srr2YnwZ$>&*e)H~mEaY%+Lf_q+cL z^+HK$^G8aotwQ_UkKIsj2pmTElR$_!D+K=33;h9oAn<27p&ay|z0$d*m{CQ<4 zzk`Lps19|C(7)^(x()smnXl?YJzTP{2{@C&EAO3N3&hq5rfm=hTjfGkD+qxv$RQ z*MzFww9R`rs8crws_Gju*pztLGXLu?`*vnNN3R*iUz5mV<;( zcV*$zU76K{AH|QWxQ|K}*a`*sbXSPq50g>wxEtc9y99nB5aOr11b#9l#0{@Mv-~D6 z#1E8lNrF#zh4|?%f$`}s$qs8Mv;~?|S@bRxbFYn!&t{eOFa%Pc#%VYPqQ5q~(qAna z4}#>cQ6YX2(7=IEcl3{ey-*TX0tU_rEk#TjIIPmvv9k1ZyrG2NaYjjc7=j%>rUNQtqSq0ZNjMXLi}o*z}<2}uZAt$JskCVpt>@|ueOOy zkLnP=+U8OzzS>sM4mAfGbRlaBHc+In1hE!u;D>Sx`Aaszh9rP0?L{;`jU(Dl@c6-N zkag2d2&3XP*s54V;7b7a0|?)b741dXTv4y~RUC-}$(S-6N;gMi{2O8gjW_O^$9cZem$U^8PZ6j>> zBGj8TMw>ASh;EwR4%Yt!bD9VY=%N)r%U^s6fQ0}$X5bV(D)_yc-%fU-sh0}OsyFmZ!aG+P{P4o_XD9}fR#)53%KxiG3g+y6j zFGO_)&IC@F(ckps<8x}U$RKBX4aD)~eZPkIp0u6o=s`7{0q7U@RsH)55f2l$I0g*NGg zg#g$Is^jfQmyx41z9;w0jG z#+9_YLljay7BHX=dlf;U3mFpnFd5zwREM!KHC(CbFhX*)VD6jnp`Ji}z5iOR)y>G9`Y6`gySRr|OtyLw71iuV zDC%RlGU~>h(9G_h=r8VyLXr!9K}F03#ajW;Z>oeYnr00{YoIu%_$0{FW8rH7&{HON zOtfM)cfeIH6U5Sh%_GL4=f+7HhkB|s5lMMxjN$W+R;9I|y&I*;=kF@`l4;sp34j+r+n<;Q>9FYI=@`hblS|rEhzWuhELr zfx>DoKt!#X?mge0Dy< zr?`JD?&$zf{WZc(H!}cqA!iodd^pygGF#|oYO3Myq38va7*Hdx04*XJ?%}gziCj?B zt+j@1a3I=r9DJv*>bVYeNCaKQ2ts(N#~8)e^ZKB7pr=;6D$z^N8v}F@P}-xfo=?Bj^NuC#~o`ps$u6D$w^4HLhbQDgW;Vh5mbP!H~;jE44 zlnJNEa5ltqN`w0(|G1k{G7R514i1jy&<#7xXVReSFHjZHeY>*jr8>0;Q<VI>gV)B#iM{m8Z6Scf^F=Wj>}&m-A+V}HjyGvvO0T5;vr^Be$tm(Cl@ zg-y@lK(EHq#2ag-<7*~$2motbh#XjRFY=!^p6dFj`X4_u^{&xs1(lXc-Q=-ZUfW$x|&45rt1e_%UxVSPE_YtYC- zJ_m2eg%@E}WvFUswh!>x(y7|WoXw)Dc7v^`s{=(%TQYFoPEOAc(NI|sZfAWx&#BW& z7KMhJGEl1pn!;C)F;nnd*q~qMJ(1oIfO&5i=uQS>@5Dh`ebpW48`RW$3eY{6sr5eu zts1OVC4y7$W+(N&5$H)>fPTS?klx<|eTkF%*V3bh*K2h+%l;_nH9)!1rT2;OHlUNp z{TDUYEJo$}l>;!O+cD_&dUpNJ+~Nyh-N}Ge$t^ga`z#W<=a>kmtpv? zzr)4Pv_oC|xR>tGt9boW6_CrHfirEfz9`m9PvaH3{+-w8s`R5U&ejS}{flLqTm;Ud z5-bX$oN-!G=Tb0u;OEpdoL)yl}xx~(B=gPh=HDqK2WM5a{%#;xC_6wZlg(~3!fwQwz z>KRaYH`Xc6fYgfs@^Cu%AMS+RBwcYCt_zZyIO>dTa+TYqCx=L(*}l<*92;i;ppg3q zHQ6YtizOR$X~#ADFuT1ljBGVbs^CfEU(@ayoP(O;!;^++`FJ~UDjj|ya7hY5&8*On zHB>9vs7Te^jcB-em`%^=QhvluuzwkRgx^8)7ta8bn?qflrGZtlITQwEBPf!%w@@yO zvC_!2Ekd#-q(<_>I&#Q}^0Z(0ywE=6mXW#O8}H(>4=MQHZ5fq5iYg-q!+K$>>voC5 zm;mDDO00YtuaN_Rjd)c(zIS2#Ec!?n%B9;ZZeprok@(7Tz-d~|JeOr$`w7}EuFNv7 zoH*RNE+qVhuYk)XP>yEW9LWUN_*YY}IH6F7y6WkY=LYkhN<(pjmfr}{6{2BqxCq4a zL6khwQ8SS=`~X}YDy)b2>IjA!%paf}g`dGZ$`8wXYVN6y zPUfX)FD-i+_F`jnyuG zj&=oB>ZIKrPwQza++=AY@M2V89DW7B6xtIn%;% zJ~1Ad5szGG5wT&9MI;A4W@Bn^lxT9%!ATb1Gd2o*z-r(s2eH8WxzJ0&-Dll*pXmzz!ql#kOBMR z$oK(EIqc%z2%@aCP5}@%WH)X%&I-`T(HOG$(TwPTooJcHlUy<38V$piQ-#^#;HW(} zCS^8yF+ye3u8%R$LX1g7YotDg8fdZux5dh-g2lM?6YQl6ALzxuvVT{33KqkE!kA2% z%B7~=t)0q+aNKU~RE}zuNQuhN&>2?&P5I3fkmcHM{W?Yp%*W73`HdY_!(Vny`CZa; zA~jSFL)#xkZcUMOt*z|IR+T*&)5V9IQ=X)Pn(>$oQZ@^yF%VOpHd()vxxCiq@>unmd<=%Zh)Z zgyiA!X2nD;cOSY0nTH11CJ&N!@|J}jB%L(uE8E@s>6s?}fP+5HjEe^m!u%N$;1nHx z=4gA(Vey>9uqn)dw)hs{yaO7+j~lDitq37~yrv(A&p%wt&!+${7z=#p7>!L^;SV8y z74QVO7JN2;lylAMi)0|4ECX?rvvir3fjCVDBJe%FrxBX^Ozj^``K3#=Y_Ogu-T}Ty z%NFbTR^QScKG_rdTys+l{AT?d+j}6w->-NlIEJs+XCi~?=|0}VYuI!oo@<4nx=C}P z(~#0=6Tu8A-*7Z=MSsmQc+xnn?qd7riaiZyjK^cVoX##b{8xd?LVdo$>n3US9{usM z!Dj=PW&SG$UjzIx2H*~Z{|20QxAp4=KlNBV-Aw+g2A_W%?CTEv9h11eIl=DweXP0d z*D&%#J6(P#(`CbI;Ia+)ss0P1t6|t5wBj9bUF&0iG@K3mt}5WY4E{dw#fWCz&&OzN zxaDL#JdSa##|VG4>{@@Kxm7q?whBL!I2bK^g?kJvo08vZ?n;h)3P~jE1q>}5GOwP1 zggWa8JX{Qhl03VY;Wi!Q%h4B-XD0xVJev$4x-bfJOe4?kLTSmfYXF!$JA!vwrjUOI z^lU|+@j9dULUa~yuN40U0533#KL#Mz7=xEVx}iOKOZH>n|>NSQ7ajd*UC5>yxtblvVyo-RuBzq zAS74uE2YrLSK*WAUsQ6)Y;mj3t_t%@H@HFc9UZ0y?tjVf!}!y1_wEA5H;wTJOxmlPA z-~l@4CJbk}N4OgR?-6!mYn^6jm9*I|9WqWBCKhUR8zGvNh&d8n${Hf z{9WBEut#vHMmS4%i-#h62V-8BDinX8PDZKRd+F%&#u{AD>H?LOzTQKX8XJ}&TVYaP z;3Runqe@cW2;SFN#`>##ImNFdkIJ2l0g#eA8C$>#&jM!dWQb3e*viExU46l>deWh6 zc9A^HhDTT&uSRm*sW*(};+=9|u&SP84tpjXadF<|xT_k>K{uwJ11)Y4a{#Krdtq6_ zJxJsdtNo>QgX^(8X=sOR{t9Q40Y1+D4a0%Y=WH|h|FQQi@KsgE-g}>weK;o%@*pGu z0t5&U6i9dqiZ)1sc@bg)N-ga%+Pkjt<+*KTB{V%V(YEne`d|v=aEG5-qP=WzwhP;XT4^ z(0xcY=(SM866m57aKN_kp`Pyc_LP5y>1c1pfc*#yn&omPzK=M$F)V3qfaI7=*g{ z0|X$^M({)hvkv=9j+T20 z6nI~dk_|P!#DzvC@&4ObWtY2|;JkbyY%e%(C&1goO4uc@D}5hD>q13j;Bzn3AYBvK z?v{nF3Eb!Ap_BRgp!AXOJU)cjj5SIJ?gzt5_yBjaToX7PUcx81o8_9ovG5W;!rd&_ z1WtvQ@EPuAxhCLyDa1cXe2BYQt_fs>m+&d>X1OMi6JEmC1U5s2(x~Tzm*^!d*93~f zOZb|=W-NQEvn;%XuL*3HYXXhoC45a_vs@GC4lm(r0-NQUz?Se5z9z6)t_kc2FX3wf zo8_9oec>fzi2R$ggg&7XeNABVSRqeX0bdi?EY}1MhZXQOfz5JF;8<7zUlZ6Y*91<5 z74S8I&2mk^_irv~Y`!M2S*{6Wgca~Lfz5JFASbL~KF=dh=n__-*-Wkp6onONE}JP- z2`kV{Cf5WS!wNKy$u)uQuma6ua!p`MSb^p+xhAk9tUxoEToZW4EnpeU*92a0^H~3| zQ2oHo)AxEvAZCk6Xat6={hwmR=D9_D^Ks%)U z`aFXQO@$jx-&tY4en-BzztdJI0d z$c3KiW}($yft%-Mx^T^ImfPwL0v&XVTxGh4Vqi!a_;g#&e@r*+^c?QQ@pTZCM0QpMy_<9qw29skpAqhWc#{zoQ z;w?T;L0V0+Wf6>Lp_TU2DcB354bMd}d^F7QA|F3$P>6b-iuj>1o)Y)rB~MxC;U&+a z(8Eif^3cOe^ijODowhVwnUwZNtfpV)qp_;gw967Ce@S-Q{eXFu@-NBBIAGu}&GBW7 z86-fP+6TM{eEd>MbkD7k^!3SY){Q>Si;FQXFd!6gmLd>IWHl4xA+%Q%7k zDwi}}?#pPMDTym;eHqW2rkfjm8EZ{ROS3N{3Cu@NxOiIIeHodEa0Bh#zKnCmO5)0O zzKmPrB+BJ)JsJ)^Jx$zCm zAN)i9gz2xtB~a#hUZ--T2>Fmhv%o=1ftwX@exI7FwU&)6wj=%6g^MF^vSpQ zDaJ-%$2-))hTj_GQ&gQx@kR6(F;wpu?tA?6ErgIe=S(smTc~XvaT~ zF~v0GIZwDBUHi}D`TYn89Z$T)KUPHw_%8mhA)*tIRdq}x2{EGGcEcF_E@Cff!GrJu ze8w>dojCEi|mWP2Mpjk0l9Ll1*fFhLzpHm4qyq}tJr%} zaCU>7WUXQ^PrpgUlps;?WZOzU?dz-X3i0)|Q zk>_F^JQr``L3}!jz*#LHMC?M^J&3rRph7n^>^3=spzHbDsBAqPhLz}!uSShVgs(O1U2dX5xA--XViaYsr#D$CvCFpmmdvD)Jn@WUDA ziqtLb78^dMPcoqRbQ?y6HfgNi5+20#S|SP7wx>i(Eq60rZMMbDf;M|!;I_G$F5G@M z3veF_+%s+_YrO>rCB5GRytOQYugU(rX0i`~H$E=?yk@dnnuWGM|HaJaisr9 z^|x!L`)!*r-5r|gira1L&&2I_vX7a4p)tSH^Jh-Dg^8IdTCa%#q(jL~|q^w(OU2&~mo#f~zfG`+aM* z-i&DVesDmz)@hs_G{^rBj$&-0+Wk^1Ot;&P#}H;_w0g10J#?3vrOHSIqsqViwYY@gZW_Z-N8u&?#{edckgOSOn#J@oYBB03&y_3oxLOX8*AmnG>{(Fo6*w4H- z($9QI=B9a6SeKYqU#UA8Dz@|WUQxS_XG)v62=2O$`AW{%ttHSDZfWS6&fSv740Xf> z03q%ZLL755H6bp~hW)lC(k!tS7^rv~lWqBigWpFqHO6)dH6CGV6mCbF1=eF>=qD6C z8ijr=bNg|it*cAWpN|Nm|5Pvx{S^cU^#2fMl|PMk?nCtu`nSHy*oWYTW)h z7r0%^xMfaf%c$aZi0>r6E;N2z8H^$>C&PDq4kf4H$68A8OUazNWH!?D_Pt9-fC^b@ zlSku%5e!5;yO#COlvT`e_}Abks^V5DaD>4 zHGVMNXG{1pey#(kZ|vfH+n2%5yi)hR;a2E5*Ac!nx{5;6@MXrQ(M@%R8JUzuS76i_ z!T!SGpg(ssk7j#-5KG5MCFgEzJ}v%)OXENd_|Xg!>ULMsVv znm43<201%xBHxg<5m?c0NXx}Lk$@Na3q86%%Ifb8X#m|nE*f;Eyde#RU&xZen-TH= z)8!>-_aei-05AChchg^}C%k>+DP-7%c)7_`E((PZ5=1QSET405nz0$8*WuE%7Oed&0Mxfmh4zVK5Wh-+<=!anSS_c!8LyMvNWaNke2FKFZ7`a zUMj~gl^W_-ulC`C9Lu*>xv#?U^Q&L&#%JqSB%BX ztk`?elzy#C>>flTUh5LKA35@t!?=S4_zV2#_mjl_4$j9X$jehP&*QtBxhjyHnyk{kjzQY~6 z6F=HxHIzJ73FNVYQ^|tIDuFy!^{4@lRRTTC!0{qq0zI$&Z>R^4RT?~233S1H9l@1Z z3G})hjxVwk=yf|BFZU$S>vlMf#uLb6#f?=aC6LGJR;pZ;nm`^a&R>$9a6e$aO!=4O zq?5;r^Wm{dA2Uc2@K~jj$BIkfu}UY86(``aN+*vMC*ZM4Cyx~;;IT?4j}<52u}UY8 z6(``aN+*vMC*ZM4Cyx~;;IT?4j}<52u}UY86(``aN+*vMC*ZM4Cyx~;;IT?4j}<52 zu}UY86(``aN+*vMC*ZM4Cyx~;;IT?i0)2=}$YYgG9xE<^$10sXR-AyxDxEx5oPftF zoml!BszV;D*eiiE{c4?9;((_y2sJ78I^^mX9K{kp>lYj;YJ%k-yj#R18Rf)SwiQQ$ zWLVt8$Qn*Csr*Bksl?7YNepF;py$(KUZ+)KXVQh|nAZuy8oA>S_{HB*{I^?PgqE}u zsg=yz91|V27QD?@QWB}@Q6C~nE5=TwCPwu_%k%YKt~biR26t?^E_FQmCM5Z)Ve)I} zd-Q)G$+9JdCY|+f068fm@X-iT635sLzo!u=KMelKqG90=ONaI?$uA&pxtq5XhY%@z zHcl=uaEsH$ONi&r(;GsM`FpSACH4o@Wwea7;*KF6`wo8SF@NuQVS z6o{Ibxz@H~K*Pj3L=q|T03u|`Wd9WrdSg^Fy$@}RJ0`yOE=wHC9=G5tF}N>)cRW`| zt#leIUH~FhmvgUgkSsatD%*OGBAtlXG>-j<#3Pb@_BtEiZIfQ_`^a7yS;t%uI$I!Q zCFNSl?_tQt-4eDJ$J0v=22D++FG1vFk{>X8#T~NW5yan<|l#)u25}1k*7X@e%P1iDOKYT(I^eJvXqhy&W)JiTKeud_Wa3>)J@*ccbW21hkE#xpj~G7Y7Y{n0`W zX?p?j(WsX(KE}ub#z^Tk*sc)^Entl7Lz3BO!7Reaq#lrV5|vV`qi@BIU>p%*G09oR zNIPMqaD3tpKr%-Df(T=zWRqazYD5?#Twy0wSU5IuUawSm55-*JX;YyGMY;kl_#;%I zC?|31wX*L2j$&T-2NB_QUyUMK_tqq;kVFfmBq#B2roxX9;R@Rk;R;hWhgZm_3Z*%T z&mxy4%Wa5oh077)3hzf$*hm!?PaI>{6FAlZ15XBa7&w3k59jQ4e`M-gKrr3A{4oOi z(x-ttYYMT`9%@H3%H1Kt(^EReAYn#z$J;@cx`9`P-MrT&MWd$@qhbn{%WA~y@IEz)wZ!p(ER*11^#TcW{w z-8^0_L+BjFH{di9u=kE0p8g-&YhMqWLK zL0Votjl6m;!WBGTJ&nA29!9bp*0}(Gh$Jtj#yzx{TCbPI)JUC-O1iy0eeledaowJc8j2Q5+Xe9KLC!W{Sb0o5;ndL9N{ku{|6-D-%&%* zOS8CJ>aaD{eO{FOD#ae32euxO(7>`tU#=UT_#Naj-T#UR5A6KgWMG>RaR+wtbO!bu zYJ|}}`wf~3^eg_`8%9OZeb{#CrwkFdp8~0qMRoe}a0B!32Gr2~Xu8u}t{Iy+>vo}v z2N9;q&j84iZ$yq3-rJ;uaN*s{!keY?3nA&#M`FL}mQ~`eXr!cVBCAb!HoQbEAa1+N zE$_d6uUj6H;o+0yN&lnib<6u7&7OYB zb)QGvlKxvx5)y`wHXKj$-117?77mBf&VtL`^8QD*v7d5_HRcW}VO1FOC-5l#SQTEa zl~=r*=jymQZWeT0n+D5w^IWiIHw$2EG+5BhV^tVO8Yb>sXr0(X{Lo8Z{nhdO(pP_F z?owp=msoi@{w2B96u$&7QuSYwKrekI!_L3VN+5p&$3N{$pqIXK{IaBk@`)1vOhy8| z^p*3MXITmKVqx+*`9Ft=#yvz`(`Q2It*-vf(;86C^7vQg{+#-2v8E%@lKVSKv{`B0 z{;Uz?vmZ*}GPRYeK(R#%R%t4N@?x^n#LWeN0FSB|g0Jb~Wo%JB`2 z2{SMrLXIDAb>&Il3+?USgz2&OPP+j-mb|&tx*kAA)RP(0U_ ze)k&yA#WhwNWBf4VxR53eiDK~o_cg=ukfpLfhhV5fx_WRW3g-f>g_%*#|f5arxbd| zA%ee9&1shBF=SB9CiqL}Pd$epc5)<$&-*-N*jM1)>!3FMX~!!3!XNY*34D<3)^5g^ zA9d6jYR0s#I`4B*;?Ig>?Lx$J2B}b3S_e((ge~+K-K%wVUY%gY> z?>QFtJut4^rZGo#lBeBkln7 z(ny8)+gWkfoh|X>IdSKMgbe?eyts#932{3oX2ul)mgDctjw?hv96vcH?g<0)?zwUM zu{t^by?JrVq47EX*9$E5Wpv6!O)|b&KOXw9&ZYj7{f{F09-`_~{$~*#iT?^;8pGuA z9{j^d&A_7a4E6s3QT+;Z|67RaSD?#_)BP42sK^3bl0Od7*a>K)&`--Q!avM#)p39u z63?aAA|8|Cy9m7{7?GBZ*6kL0XML>45KY1VYlb7`8$bPB4?5MoC z81_at4?3Y1Ux?cw8TX;S-Etj3(&{l$I!90cGJZ1l3cxY|40{Si-$HKkM$CRVdHFEM zD-rnKqA1Oq6d-(-`I5003A^REAHV13fUbjMcL-|FCDdl#i&bHtO9V=q{6+Dje2M^5 zJ_j_}^9a`|i@>_}1%&32G{WW5HVw(1mzcH&P11lf!V88yfm|AE#x?=Z2KcCy%~tXO zYXts}!vE{6v19Mi)6nzS6E=@tn#W1W*u(QEf_%0Y5Z4Y4V6+cV=7P{X7SKHOzzahI zzsMbU(%YzjKOT5tV%ks91`j;EU|8y%GVlTc*8{&ceBhg-2VN_%L;xQ6F3H%B{_K{$ z6Y)EFCJp==G|vN{{0v4r_#Gtg8Aqiw_|a=XsXbGXoEo22b{8JWM>_3RM7B|6+TFNS zgvhWphJRAB$$t#G<&1(GihTxv=KD*v^=A0a|6S zN;_-P*8s-4ugcDubQAE!i6!>rNed0wQaf+bZ_zB*xx}7hu|l~S2AL-;W)DfL(~u{nZzEWXp&kejVAo2?8b!bG1NrL4?!4vBu9z9b`~Mj%BCY6 z-|M8<^bwBlb!yqleiml0Jy>@>pM(p?Gp_=w?7_omec*yLn3Z0i?tLbbS;tJGrqQte zqs3siJP*LM4lFKux9?db^3C6ACe9A8V@*y5MettwO#NPZMu&bc{gY@vleo=a_$;E9 zk0#-x5l;C6h=A9q==IY{>eccc#jpQu{1m`!88l-;&Y-*y(@(cTOh4ThV*2SxA*P?6 z5n}r3*hgG00woEIZ?`;!HDj-%-Ut5_lmFT{O44W(J-4*;nZSA)A5zV118g8rQu_Q> zj2;?Y(%yUIr5Ky2aP%F>J3^9hqmyC6?#riog5zqajijD6r9fM<@{?rn1zQ>Nal=8><)vv@*cWRK~y#haH zTS{+K2I-ATOb>ych3^u9b?7eys;roMkYS(gSF6TySy*AV6~p=sg|~yR=r2@rrWNxm zWKhjVCv%zQyoQiRkYxF2G`j5>_*jr!f6ya2&#(58FeC^b8Zs(P##G|CdH4#K>CtU8 z&Wb6eLR|Lwb{5wT8xB4(wH$@94fr90*mGfQ2cl#Ud(Kk-iHQ%2iN#3;Tfv^fShxw< z3ieD<`r|jTUqR0OAcSXr>~2Kq*x55l-9w5k#!s{HKZb0jzZU6#4)NHR@I&_~JO#0@ zAWHWrJd@SAShkMOPrD5NaRU20eqtbsJQu{$Wtu{;y=S8GVU&)?pMsoNvhV4gh@SIe z=~dkH=NYf`I}z#F*z(UMumb$huE6uD*u{uaEl-Xj0%BM28G@+C&rV>@GcT4#tG{r` zlO>~GrQk>Z%|T$deG}mEf1|~QNP>Ow0R)3LZrqsvJR(-&l4JnI5jO0kaU`4LXe>#i zAr*BrdG|O(2NUM4ly+GUMcgz$43I)~hcxfR?P@T+opLFXSf1Qg3bln5%dK%@C=Dkl ztq@PyseTMfkF7FtY*xx60=R_$Tj+5Idj#Qp$V2Fkh0=(a7$hfxusP=X(r5u1O`HBz+u{jk(NL2cvj8@RqE^OXP$BaKV)z?|v*S$& zhm)-ob0tlA#M>z6?(wO#lZeNDh#xYNV$K^$N9um!>QUykWX`qu7^$5^*AJX)=OaFv z_%mreaPC}74JOfR$((DiB(h}r2znHquIB*2`&r7hlL*7!&xR_Kg4AV6=Kx-!w}W=x z&(iRc=@o9?T5fVozRU$@B|s+0GdpKU5cB7k9px}0|9~WECp(9P@{n7;hEl>@P9DZK z>`_!ml-SA%2evB>X#QU`T@9a0zn5DpXSvXNJV^ zO;>VIzYf9RP@C5wX=NM$(*#`_Yi*M8@5tQZW>(tMhe6xi4CoUm*ya}S;UEL=a`Q;_ zXy{@3@GwIGaWA;#{BnwnROI>ILI3y@xa{%BnoN ziF7R^cB)Q{ibYH1fc3V9Er!;e$k8i_7Xz2+#h|0S7+OP%p^L}jyO%np&27#YGihxH z1t)^1<_;+XfAYGR%j@D%Bzaxv#9wnHlNqbXB#0xHNP-z_qg&Tyta;%avBlIR#@b;L)Xc8%W)Agj=7R8M4okU} zVq(oS^_FB{z`l?a?U1K{rkhD9T}Ui5Wb-%m%i`?|S>RvvEj;)_^8Z3c@hd28A)FR) z@JhW2F>GWj!(a!Z1D&gOVXb@0W>P$`kt)lDx6G zlqYs4n1UyEDbIw&Ay4enob-HKCUzM$l{O1Wp4ewdlj(^qC$nMF4v6cZnaekk*k}^_ zkl5i)Y}_;qn-$y$)^fv-VwJ`Xqvl4iiXZ;rY-A);qa*0Y^3!^>Xr~lo(J_KCXnz4= zc;o6wgEk3B@q8{>8?>HL_!8(0)Wx4F#ZSx%{N!6s1p@D9@I%i*+Cv9v%gv_^S^pjx zYaTkcTfQ>}EuIzIh>5p?z@D>iLU!zTkfeKcp0i>wt~lIhdVVlclY3AY|2GuQLBune zI*RXnP`{o|lEFRtkw!@#L6XcENzVNa-l|3Rq^^4}k}phglm9?+YN+&z@8YQ}8lMJ! z8p#)Dxyel6?WIt%3&}6i3^ee4NL~&^>7?a9@>h%%e_8l%x6vc0o~i`9g6CXCbHNa}GT0jF@UdOzdq)(r(RDJs8ROw~=Jb)DUI{H{qvI z8cl?XyVgpCO1T!f{%w+-ipq8iJ$~wGk1c&j`fBICzJcU=C~Hr97B#LOmc~1%@uc|o z@kkN=J-+c+Mex@QAGJ@<0YP-Z-)ZmAkkVkv4?SSa~6jLS>zb$Tq<9+}c^rw^fd z|ANLl%o;+E;L$uvxRPD%KIgqb(${45FQNH|ZzG6|#GGxinRD2PBNHZ|HcY!vAi6pS1obg^!;1f4Ba}wRcMB@PULex9J{z z66Q--AmMTeYb9)yaLIOEZ;OPfw?~g$r!OC%eo*rFN;p&6iEibXlpmMy^<$)7yo5;~(_Yq{8Y7b> zER%50UD24*>19&hEa7em^9F!FBIU;8o^|@jH+1->gda%Qe80|L@_-I!KB&Xx z5;jYyb3?z4_vv`EgzXaMe^cidNmwS~@o(w;Ea@-Ql8z13-a2XbfP@*+UXFy5C5&$G zl)zh3?vpTn0Qk!#zfrE(nGp|ED6gbyj;Sa67H4oq=Y^hN0Eff zCA?e0!xBaf&$1Hhwww{PsG0|VGk22cO2(DakB|D*ZfpVi<0 z@9h{oXZxJ<(6_IEz6bMBLHkA%ApL__NIDap@zQm3;e z%#kpK8`;BUHF%t)h%p1 zsKZAjG@rwbZa?!!pA6n7?VWD>Nzdu_b0n;g@NNlrO1N9X=<%8Q*-~!mn=jr*!!P-< z&S{o#zl0gjM?>ngfnO$Y%Lk}`K=PwoX_t0`5_U_tZh-z?lKeS8)h(Hi8=U%;PMgmc zV z?IuZ>A))Cn8Xsm{FG)W;f1)9FNoeY2NjuT?mrH)Lgu5in{#i7nPCp{$2PAw(!lM%U zq+Yy)NfKra(B4aue^kO_63#i)fBz??`~wLsfr}XczC!YsNVr_W%LjlrUt!Amh3-FJ z!U74SN7XC&TO`~jq50xVG`#s<(%k~L>to<6eyLkHE@7Umt7Q_JPn2wxwD}0hGm?(( z-?X<}+H3zOw;z=Dx+PpE;YJCg$CLH%Iww!UnGzNpj)v6f>;d5CNq%&#Q<5Je?fE2} zJV5&&M8PM041AfCFPAVVVYh@ECEO`t^!Pq0yy>Sgs@?krXg}v=-QyMscS?Algwf+M z@Oe@`Q$q9p`)K%q+BNUJkA{Cn+B+!WVF`~(cwEAh5=PfI^69@(!uGU2YrbUM0UlfUdqU2ejYk~ZPq zr*ydqkNmLz_Dudsv8zq^rr5hCG#^GWq4}bT3C+hROxSxw_h-Umzt?FKo)mnW(7b+B zLxugmrsJs+&Xmx+YA_6vqrFnTQ^H+;)DVXxjE3JOaC;?uM8X3Sns>~_|3`F9oqk5( zUXt*zsV8A{ts|0uT*8wQelP&M$&Z)uCrM~tsFxw>%mL;%S@O+m_6j6jGyuH8Q}n#e zOZ1}Wb4c)VRKjBto;3J6rfXzL7+rs+g3q_6ogCXkMfeJ&U|IbdrcA{`|@XMYCrHip#6Y0}INk0tFR;;zf%h^Q$TrlvNg96eujK#QzzQg;X%BZ`Pc? zS<`0s&6_^U>T2z5YG|#i&FXAy>j+v+t@UkIZCy=UOM6FMXJ?>uP0Nb5=0Iz8OP$r; z)@ju@wbllz+uJ+Zx&zg|sxQ8!{Xm_{I~#|taxmoF#^logkkTGiDZHI0~g zQ)_2yps6#^T-y<7ZVFl*)vXP6k)<@6+PXkfOG{mCQ+2S;>Zog&W;NDT*Vc6ajWsK~ z+C#HwZfkAGq8}>|Sk)0|tFQ00+M8M$toG_)O=DecU?mbDMqOZ4T}NkATdTFAy0sMz zwRW{wE32EEG2zZYux({sYam#?q8Vf9Yza_eeuBRS z)dhLL5m-dUj_R5qNY)vwYY6~L_#V*o&eiRaqaeaW4Xmp8vckaZnUQc87B5*?Tv%2R zShV<(!obqt)d?P~ziRs#mITIyO>G`H2P3;_RaHGyEqnm}t)tyS1u4-P4+ZmtKp+8gUy zgG_?Xx|+^lM^kG?iAdG3N)d?({Tvj$)6SJap)zWF_v{z$+wXL|Kj)(^4 z%4=znYwN%&!2qq!_Uam5g@L-(TFku@TtK*NnmrQ}ju>}Q zaru(Kr-^)#`3n}87X;9H1aMhF)sjHflEsyU0h-g|g$t_+mqdXqzNmOXV19W)BwNy7W)mVXcELX@C?mKbHwQW! zo9crQbkvBuuX6e+vc0pWqp3YeqK`y;9Wj{}e@JyT2d!$Xwx(LFHiRwJH5eP1 z&Z_Tftuad^m)3G_Td=XNBe$w+WmDDCKq+gr3KFsb7PYy-wzlTZ+|I`8>9b}}tuYLO zK1hr$lt{DQ2E9&(F3N}FkLZAfeu-u@Li0ZQ3Axm4T zA!IrNp=zXNfZ$f$2-8}ip*ncHKx#uI$%x41k$7(JsBa0hw>3A_tcirxg0QNfI7$$L zwjIE5dyUoBZdKMo-q+MO1v;DCR-@C#0Q6Y{4a-_pZM8MmlFTH6Xu)@BTSu+c*{Fw% zkXWO-8OoYg%Pdf|y0+qD#YkgyXQKtBQr~2)WCTX=afEowi=2`vE0|xrP^*27)lRZ! zKy9!mWSdpnM(Ro=RC9G_5IjT3k8F%^xoG~f$V?VS7ez@L&YT~mP3IR>T^OiZivNX6 z7M9Jws49{yEp4mnR)b~rx{pwbSl=)%pg(~+!hT~N)C?>P6NR{Hsctu{=qilnwl@3}kWD(wY{wHb`_Jo9#OyXUD3#+C0Pl1OWm=EVkCPw1cln z!(m-khua@$eE=#|kp&o~h76q+RxGHDL`!FTT}=}tf~%BPbk$pp)vM~ja@xGZ%3-E} zRBeNiu?7;9SFo1gz*>t4a|~D`7rJIel&&G#Rz;4OiC9$JPv%8Yx`!6dG^m|R3M;D$ z1F#e#W=^Fuz&Hg37TITdSb{?u9oAx}td_;~^%ARUgb0NQY;Lyd+euBr@}*f4S}Pa9 z%)z46gzkhz0FxuI1}1DozXqo;>?&cLUmRIVVyUXaf;6dvcr@A_@`Kbe*>5g8)zrgm zWZPH|U27Mnbz6Iolw(A-f_6b33_tkHHQLVkf)TcJ84@<`jW#n300)oLd!MnV{Efwa6^RYN;8 z?3{!^Y~Bd=^lAbdK)S84j130WU)RW@0XCd#dAC^eTWdSonrgFhrcIwSclNAVGbU%9 z-QLiKUl_PS1kFK8v@}y5;j=cYtu?D-){MMav(KrSlvUG={au!9y|Sjwotif_FDs`p z7;Nu6H#Zlkr`D%7w6%dRI@`KBYU-v!RiW$qJ$AG`zYB5N)8_V*7qt%&L$NOphNK3KT}=H5J_5(-+AjOlbXv`w zD?*DaOgWl4b=0-Pd%(s`eRWfFS4SOC4(pkg$%-{F(r9bjSl3M=k8M#=s;op?>sDhQ z!(I`+e`r|&#l-TRq(16kv!{7htRXDYzzQ~lS6ad8Z1abP=ei}#;3CNGlD9k(i%iPC zS<|Ki4J%+QH`N4ccuUjN8qiJ(+RkBYHPylXcPtQtB%)ZDtZ*$PAga??1F-s90Raz1 zl-7mp!`3}o(XyzhSX@@T;M3TyRYtjqXq$d%aY13Aw6L%O24zQ6HN-NLwm*4a)6wD* zxUtEqY$Z(@1m8gp=-rR(u-dCTAQm-O>T&W~(z_#;-o(DmO|4zsFnZXRA{yG2#$oOh zLld%!*+h~ZZn)Z9p>agYlVmGPWZv;&jO;|9O>YEIX<5uKsEo{HuLg8~1b4Fq4~>i| zMnW4CGcs3ePwaaKusrKpqkK~lospoHl}O^aPZ@1Ir@2&U5Na8(Soo$cZuXb~L;IiJY>tAby21)VnfGY%O^D(3ea^V+!y#9veptkoF(iheo8= znvNDJ^+o38C)gUo8o<6V*k-{bu3Je05Gr|%*7=4_R$$=a*}RYI<9`gmMWT7m_3%Wn z2d(WHBu8k>q!cM$SKVTv5|&zfQ*B^XtLtT{Nmi+$=ZC!sNu4BgpFkxEfmPzaN$*Ebf zHZo5yNw_YwjRhwXH)74djxN>(R7_asj%g%r} zqRpyl=xD>91v)kusBi07X_2cXKs%S}E(`>_v(>Ihj9l5|TDQ=qSRUlta0MvEX`e{q zW96!-jURRl&CSur5HMEBji-12B;vJG_ml7`TEz&#r*v)vm#vZd5on|p)Q|l+8xn+E z>F5}`y&ArnHQ4`(J_e28IOSapZ%wg}ZWl=vtp&6Ns->85x$3+Y7~_`f&BtD;&pCr@ zkFYb8y^=(38jqa)^iiHc41{efNb>-1SL>TQF$a=U+Us)RrCcCfDxpbPAqM)!yOSmr3*ija|KtFe}e@V$g0;3R&oF9rU{a~ zu7w;v7;Tu(g&odx-lL@#lY&ksLR(Rw23BpuDn0`uSHUNd^$_iK9mF`{Q(;|G9_4)r zwrRnx#l+0GkL6GWAgc+5ku@S0%GA41S^PB zl|tCB5Ej7>l4inj#0_11w8BTY?2(|UV{4-~E4)X8LdFc)bwC}W@<)qe5{eOoF&edi zolS*hQEpgfwb@qJwApY~;wUBjaF+R~-xJx8Ma1q3@`Zz5B**&hDVZ}0qkLT2@V&5j zflEA6!eY#d!ir#)>e`NWI96TXSG)E&h(P9TMRivYHV|k8<;EL5xV()iidnFO7PYW1 z2AXT@R&+JM4MCe@956RZ1Zt2dM-stoqC>2~J|67H*vCUk5*sWu#Z$&2q9YFw6a?gv zgP?GF(a|#VD=V5PKOj5g$`@C-NYJ)|QCbmnG$%YWrqc;eL=o>T;KU4VA`J7_T^U*j zbQ-L+9WcSWXBJ-KhI8#dZNSZ^b0T1cclanu9f9D~jZE=sY_w%N+}TBhq0>0X2l6epV(T*CR=YosqwyHS93l4C1{TR-2n<{x zU~dF;1XKlcn6R~Mj&PXX+6fy1p2<%ld`V2z8c&vy6vV{9D?~bB9R2ZB;pWk5aRr1L z2B&RY8AaI9faFwF!1f(s4jb+E)D?lJHxphLxo+TwC9jO1|g zE@E*+g0k-^>d-UfPJ-V#LLLy`OzTH^@G+&%2gi1R=OJ;yxZ1GOR$l1bvb}K)yXbK% zfbBb47A{hGdrCr^)BywvWH_q>ZP;0KjewRzKX#s8Lxp4`zy-~x$&4P12*%n^x5gaGhsqd1?9LOZ)LOO^e!Ak(8e{BNE@7Hx~$kUCY#z zAz;YlzA#&4KFlQRRL>K>x^P#FUegw~=Jhy}>!3qKS_-s=NexE!C>L}p3zsadj5_%= z{-g-z;Wb?mbygR4n;`mNqNTcXr7PrW*eMOCp|Cewb1O0Wnz~l({U92kPDAHZM0k<` zB>kf$KCY6`?HIoC1BnDPzCE~x9f&a3se)ciY!c%1oXp=yn-=(};VVvTKgfWzuY;%h?;Cx5Vs0Z+-={gCa?}~ax}KEesLBj3PWEtVH<KhC6>J7{^CI0P1qC%y_62-J0Sw1Ro+tS*uf z5txpW!vncS=110|MIPnP=XIaQ2Z=aY58~*Wuio1Bi;43C zJ`s^_=m)dJF%%yG()};8oQXPgc=!pHB4kKQTPxOLATYnOaz0$=LFZyJ>^F7JEUh-VmmmL4XTqjj}*iuXCM zjF25s$|;h!qM3mf9S(4*#RxW_r7n92;sR4f>pOC5AttQxdAPm@?4-@^0gIpR!gS(3 zgWT$*WnL5P*2=OI8n3AvMPdz*tpze$@FS381|tqmvuF{MRHm(-b~CVi?xXaDKU~oq zK@K`dB;-aeK3>KB+{~Tc&;D*9E)!sD#`q!zf`Ae!iaRrWnM_XV0_95=x$bHiRG6Ws zoo_Ts&eYZ>t2BL;6_+k?dQWaGi~C~5AJL0?BaA4PA}<{W2F(Ip;IY7#?USfXykn=V;HZ?bHGH+-oh| z8Vj8Uw&N0G2d=f_fDi|?L|?v-!WNZl2=k?OVFk8Xfd?29ecZ<(N@LgC*OQ6TO*@1!*wZ(Q<8pRcY)fgn>x(-0W_UHy4TL7i2JDeQloZ_YIlp- zR!3OZ;vmuD3C^1S?gwb40sU=`jJ3;@JMCf_$5HIE$*qK4P{j1*ZqCArs^Ucju%fWR zt8U>7@$iGfEDT(T%f)hD$R`;jPT6I~+juyo=qUgIYA>XS|6pV%MtGJ*Y7Vww`wDhV zsmjF=mwjkqb}!*qugRxq{94+F>3Z-&%7>Iad>4^73CY9VRikfV@U;lD6VV1wJ!C0a zFtG2iU!r@>VXMgsKa*z7Pe*ZdWv^i^i#4%C`yOqJZweG^s>hQV`bgZm0`=fE;3aI% z*df;4hb72zV>6ZSijqeJ%LaFy$(O}wSQXB{Fv{+V zC|!!@Ho`n%Od2!LPJP`$tLcbQ%X3$*#{VeMN{di-so*d<@#OohXKDy|~Z~ zMsAq?Np^EAh#+Cx@xTWV%6@E-;;w6e{A4&{B1427DS-g{yCOF7&~j8wUnnAVC1hl6 zM(b9)!LezeZ6^p13tFEEllKT|cmg1`HY&Ur-D>pUTST_~_rVDi&;yM4Z=b%}&BG;)GmgWyf8y*0pc5voJJH@mo)bRzR}+fd?UdDTy>5+}3F@s~Q8#q>MxT4zGJbM{5fcyEa_+`Gt`Q0in9 zJHtzztX?Oh$Vn~RwX(<18vLv?;2Qh{^5u-K30Zhp-6Cu>bxop% zmLwaH|GdR{n(O9F3LI$P&Rn=%gRmqI8~Nx~(N!S@&V#8#df%sRuzU0fUT~l*%ADkjXb^sA zulvy2M0J>zS*cfjgB$-9l0&@8U*aTqPf#~16B()&`iL@yApe9j5qdGpTSD{nszYn2 zgDkbj+d~;hmmnRh_EN*|I7y43p#=0G7Y$bX&{D!XPGUKV_UTrh-KU_T&I% zTd8ladL2WF-RKN2b7Fn3>YxLGSKYB;pEJ^{-qz(KbvXwe4g%*1vVOM0IswqT07}3V z)eT4w_Nu=lO;*_mWS6LGwF&bNvY~p=Yl>HW2!Lb&_<{4$?;^b(>eixYXU9jdI5lvKcOw4VJ8?S?gVglWW6sK!kPIpkNT=@JA-3A*;p|z zqs9oVBHMS=8C>KkNOmTA@`pR=)NYS6l%|a6CTD0d^)(!Q{2aae)q@yrt`~zt?-RW@ zpbe0!i_rH1O&fgrFd&fpF~F0M=)rI(j%n}1@IWA{UG88QgTZWAxlZB5&Xgi2xyYFa zQh@gAiS@LW)zhf#S3%!AWIeZ@aHlhAj_9ZpLq`KW;DXg9x+&d;y_p8^*!mM86qt_@ zVJ+LvkXYYd;GD)3x5!CU*Iadiv6&7Y9E0o;-c4Th5XOu(@nei(G4e1@5bFn?9w**+ zzLWH*lb{~=U}Wksq8H@Ovj7_MHk#E^XCKQR`i<-HBpL2?t>TS?Y$p*`an+NUAtVFF z`$0EGp?U|EIE%a&Q^vd9CC)7MdCCB;K@m)ne-nu`=nmqp%o+a!=CMc#>-Mt^{lsdD zzt!F2#Cg^BLHnF>i=5a($GMJwJ@e9?VKC0$KzAV09fLr|N$S0I`+(PL6-m>-uhaJW z5SBPz^`GlVP++XelMjX}bX+T& zBG7y&OYII3_eBtQoL7Ap#EoUiLBF0kX-wIlY07?AQ}#EYW-M?@lm$k4Cw7J@3yN+! zU5b9KABy^W2r)nWCsGt7RA-e?tIu^q>5^c1%tiOp5$HDGBU4WR-Q!|CdFfatCD2sY zc-4N5*&hP4UiGLuzLH43*yr-a_5H2u5MOAXOIGKDlh8#nxdNPI7Cz%3HAxA>Dw^a~ z2iNt$?CEhPfrtYw%JJ$So)a#Mo$XaOVQKweSf*IkJzn*^i`|DJuxq~RrhWqKro?)3 zhB$t$Pxf?|#0(~j4GI_{#8JQTVlgE5qWpYkI7`T#;QHa}2qYw=2PL2#e?O=P)soa5 zY@rQT#~_d$7+{;d>TW=Q(0`<%z*P0Bl~B-Oh=v_Wvce$VQeOvoxdL>mSKZkirclh+ z=hQLgig%zdiD-K0x1%1!#R&{dCUYZl#(ULYk%N}5rxYj1>>0ZeTF=RaCgijiq-^r zS&{S)3GGj~IJH6`v+sJc&fi!Ko`m4N)koGc^Wj^XFn9WTz-8)>t9?h+ohY>sHT>fw z#nK+^Zmp)(QJ7_5J9XE37_)@ZH>v6E>-Q1M{KW>7WntI`^_h{QtV7GCCkh?7sgDFvjBM`ik+FPa2yDikvebWdrtCu(4jB|F`xIT)9Xz8 zFyYk*tlRgm?jhWL2%`>$A({VVAHg@MKD?S4>W(5VATg#gE=sx z>tQ&7@&9@?Y&MAA8z7m^#`va|I290bQN@0w87>3qp zuPOpndI)J=wMilVpD4^n6zq!mUx@in0{$`5dG1J4F^KnJ5ad%8QfRRAfM`@FLwLqM zv4hbMJOwwal1(&3bhQ^7G^j$|*+CBqAHj$rk)HREYR!KegD7zd(IxFqGqGF5FC(YY zs~*ruIY^*XQG2;DRVV+?I_;=~z-fQg$up1)#6E;tz}bt$iPT;Oh~dEwYd7-2QNQ;1 zf;3ij@71uZD2?sIbzl!CcRlSy3LvzJ8L`2;fYyt;Q;*^`YVR(@9rYSG=&q|Vv+b}> zW^0BR%&RUBW+>*;CF%go1q!}0#wiCs0MU<## zA-~gLU%`GEwI41f4DM-E#xC{=WPQ5Sxd;`Cft3=c7F!Thueyd)!{B9rcLSBNp?eIi zp!n|(T_bF$5Iq;yV{+v4D6zD!mF&bHEg=VE;5P%zxC9fcI_tJ9u38*?|zmo&tawkcv z(HR(FiI?^Sz?bhqedyvlho%YxVT_irT(*XEw(C?fAFfq9u-AoG>9GyGw!XEthvxs1 z1GdY+{u`Xo%fFudRCw3cvSE2+3^Ap84wM|Cz7J(UWADerEUU!DdJ?A~ezc55w1Tk2v3xxRSO*(Hfef{gNQ+G|B$J~$ zKs$9KJXEpj1RPXD)J^bQ{R^oH4tU2RDG%(B@Kb+Zi_yVUx{mhf?*qN5h{8FG2u=Te z!pd-%1`Y|7_tt_0zz;bbfwFhk0t-OdEgEIO_jQbKERn%^M!KtJLsh0 zvDhX%&`Ed0^24%wm2}OQp?4r4-exn9#Ob-4*yEYfjpsWcFkmvSEX z8-Qu=qfD&%ZV&9JlgtSyG>{()*Ck z?2P^;7bC*Y`mGp`lfXU;IayFY#PM4OmfjfB?M?+Ppzb2OC2CJ(x&iV9&ym9F6fo=R+lt4G3G6b{3OWM-Pr4OPsUl z*lZuRX6k-enmFOY-bJ;*Cfww}4~t7yvx;b!G)%Rk)%VdViob+bGfzqhROGA3hAMu* zrjt}2TP=X!LM{f4voAUZqp`wNd>Wmg#$yp>+KQxo6N-2f{{b#hgA)6 z3&;dSki*N>l;+PnH!ss5a8g!Lsu*HLK~p%y)>XK%Zj(!msYGdwE64(~)JUnoeYm-% z4{Tkfn+)ov*EGluM7;tXF4<~IrqZ~jgnQaVR~4Mp#ckn2g#EaGSLF>`e%cqyJ1)jsAc6uDe@4_%^h>{xXS zS75MnR$s)G7-Xpbu=hzCj#CIuuF0U`)vr-DR6_+I$EydW=FjYsUSxa@8RUG2RMnNk z>pg6`{Q`HhNc|a-@hsZu2ymaC{_e5SXtsKGJxu1u(XykS;#)fqnEQF`pH&dAR0bIv zDuWjW^(H_uz?j{S%TdqRB?x;cq)telqt-vxm8c#?ppef8*7rr!77vPIu@!jdYEnk< z_1_I29Qh<|C`3oKV^|+q1Z6Qmr4vyFUiAk!OrwC)X{Rv-*hGFTd9}IY{y@X>P8ZId zNLXDr&l#$)IVB$iwyg<0>h-H}9Hw@_pKLGg%TGX)Kh~Ge$7{bPRKdBw4^IA2%omg_ z)^GRY0!R@&2LFaBw$VRr-C8Cm)zDg^#AhTU^Q-lYv%+hOs7imJ}eJ1p6Yy(qcRXe!nRb1y7jvL^( z|3Wt}upLtO(gyA~$OqT#qxiFQ%TXwa{o z!X7a4@Nse2)ZA73V?C&cMCf-!}S zEU2ostivkP8*B2}?V~k+5Z-tu2blI*7*oO~ZciJr=Ff`V6jH5``g3!P-KWY8Bqn0t zb;jZXAr5J%Zv?O{OVn4crk&_3KsH=95H3$J3lNDzKu)q8EE8=B$;4db{{`c`&x~^~ z*Xq;nSGixf$&;T7wWeOgp)nfi!4b$VtaqWmC+h(a{K z7n0VUAZFJSHn(4E+MgbjrGA8Q>KU@^QV05<OmPV_pq z_v6864VU6{kof#L2r%Mv-5m|659^#{tUiWbuEL6fraKK@d4Q*`-qEf}s*{X^gFZ|{ zh(3-zP-qcs1f?RGDCvDL>(Jxl05 zWc@i$^Ia^KAcVc+ue_En_dr*B6*i!jvOqfHK`qc}hYWXb6^>t$x=3)Na7R zn-lw6CNRFtjF{;X4ZZ{YF4r+MQuRaKoM~h*PMo3} zBT|YC*GnAeYwownRGvKe_iLo>Ld)Br2?%-_N6#8Tj}3$%S}-pwBMSt~C~7cQb4X?L zI)9XL-iIL`XRN)83W@BMco2s!;ICgoLBXf+Eu5t|NRXEWGDh_;(ilg?F14ZgGHx?@ zcyJzF310pRa5x;-#nBk-GvH6XLn1UnbcLbd<`_nSrEWz%TfOCFdx&~j9qJ_v={U93 z7`s7ZOrt_`%)>EM*k|r1^xWw|I?)^nz2M%LAcy-W%hP&H7{iK)F(AP>9>rt8N@&A; z-^Rtaz6v68zKo@(V;D`A`dv58^s9q>M==w0(m%E}cVX@P@~Q#W4zY$b8UhQ)#~c0Q z<6hjV!zKPt5#|5AoAy@N0pW^l^7U|`;i{USFT-JaWV-zf)58ioFbIq5019QQVfJ1a zG5_Quu9Xl+QavQjFxnJDya(Jlf#kXp^ll@mN-(7QslH73W1xtrjXt+Sb%YU%5&9>M zP|Tg1v(?Z0pYKn3zVE6GEeyx1A1t48sV5s0br>HN}G=*O>t{J;$nFp36Us0>Xy9HPeP}5j)Rb@qy0fI}qK!h5Xm{U(oDy#SO9TsvLa-1leM;EW zy*%U*rHqbVV|^d7o$?%kBqZuuBhrsCXx*^$-!5~;b+jk6ez&i9U!~rKFbiB1*dPLY zLVP+EB90%_Tg!PfyP5$%DEbDIzpNG{`z$x6B00g=TtAI>S#)2vH;4at7e?z5e2iI7 zh_7*G+ns>rnl4)UOkt2{J%I8utsi}#{7R$unQA8T)*tQ{6<}DS0$<|vxc_c(H;1@C z7KY=uoRG4--NDGJiQc53MbT|6qysqwK{aX}PEjY0UMyI+S7{Jei1cmPcE81pzM#VH zAZg7MMBk-exx%JBfa^$K$I%vj&c zAL~cyu`>70KP~K)CdE$`{5fFRg~yqB{3?GQFQw%Og z>FAp<1M&8Fz;;d&J9YVgpon$kdq^4y`UqmivFqcC`SA#(E!&7_U`;ou+@*xX?>od} zE!av<87%pWPG-O8ggFeN_0I zC%FVcjOeaTqBrq>h?b!5Dk;Usm@WJu9}sO~aV?7e$n>s^_rT~`06D2Gf^!Ne>^k%Z z*wVZAnZykH?^~6hQyqi!w!U54`cLt|1F}1dp6keMI_030k0P~QsVfQXDp!bpmWsgy zG!QL~3=;3sD+fY-cuw=7H3BDlP3S*gmesoFs4jY{Bka(+wy`FBM^x`UkNwz4IqUgP zG$szkS&td5;q=9fJskG?_NjBol&*y2t||t+gwOIGx>6jZp$VpPQ{$i%sr&!DA4WBf zZd){Fd*E>9mt}9DJM+UECTwU-e{wJ_BnYG(1!4E_NN1$wC`o+%27=t+M# zny@fBXDh!kDR2+s1msyPr61Ov(`3*0r5A82DWH^C5LIH5fXd|gbrlNu? z;$h0j>FxXbFY_0@h&jqQX*3X|X^NtieNUrC{UW*9TUe#d)j@q8yi7L}945jMjAxmZ z9TDOxG5A#Uprl?Xcjd=c#pB6H3H{~E&;jiiJx7Q^1+Pif@4iedFp2&JS?{*2e@UT> zini0_a%x%4^@1Ls1<~h7uq65|>2aa`DzOo@wVxP?`U@|f8O~&#Un9R>QL)HAyRb6# zXO}udzvJ7!w?_X;_kv3)M1N>xB)are>L}{9D4xl^^u_T27*(1qd>lZ5|hXPG)2X0BAlJ{a14<|b#(6a%2o{LiZ}7iTk&a~vDHf4mqG zV8O~|ySPwDyIx7jjn;pHqZ4&8boTB=K@1Js{!pNv{xg<)Lzm%5_# zLg`PH(`3f1q*b*pF4L=L#H*C56x5&}4&mA6{22vB2sIcwi`GUZ&navgxFVy-rIjKkv01zohQj92%iFiT{o2W^4L} z_rF%$Im^PQy;jI81Y5gMI#ogRT_^2#a`X{(r&e1%rZ||@mW#N>5ypL3&u)aCrC7c( zq%?X!DNI-7{*zV+p|f04pulgeO`qN$4?XR(6i`%aQBVC1ySg~Kg>(1;(igVKjlTig zkeKD0V6pC8v^8P>a>qOt{!OQkZXDQ(59(lkWy?viqCw&bZCQYZ1gYGt8 zB=d1{@i^6O1c4zajoy>i7op#J6p_5)kOz>U7(ejGr$QNjfjU|tlT-AIwG@KkvQKPF z(Iuq$h|Vc9mrdnuqrCl^CLMwl<+in5dAh$;Qbq(S#4q*MK+(%Vh2W6!!UxyVg?B2|a%NbPFqEAj| zj($V(Tn4+9+i-2s1=>I+^O@W($TbU8lyzvy1}hgtoirhk0&E7BjMRa@5H5Z)mIm%IWV*`Cst$Gl$&&9MI-E#tTxjiW6%^KQeh3oSy?wR zbU^BJ^|9}<&!1q zcU;)Spst4W7xgFLjaga-?g{I}%r1j=;yT&iA+C z%l(YXb@3QwixyrLSImVL9O~2mKF38iDMcJe%soBo{;}xyG_mYr=5>4Bbg3vb$LdvB zb5=ZEWswy|5}%$bG%9?}}FO(5+G14r=T>>cTSYR`~0>>ryS`p!7p>g2rWOc}al z^kcDD^d(N}>71jm!9m|FiEfw0@Y`5p)Oq=SC_Wm)5#!d0t(DR9ze*qP`!W1CG%9Cw7l$Z*147X6gwA1WG9Wzy-nDlaX%O~=K6 z=!xDk?e&lD4Q`n>%eeK({7}dORC<}%-DL9EdWo!KaQEmfV8Eb@7ZyKex*-VQm64TH zUeHuZqno*GIUu@5xRtNKSWMucgStoeQ3bf^om2s{6O8u4=+~@|LX?Vv-lq+%nHLYJ ziCH(~?$ikk8m6?=py6eLjvmI!NQ%ea@*T44&UQfbRk9rziP@J>%U@}E7U3akvEq{v z{)@s9NC8>&UZGF}qrGcESSqBdJKR>JF?aUy1lo!HIYc$l<~}}+otMbqVloj^C9|m9 zA4mXMkn&5T29OC>z+D_LN0ziaMmy#Jf(hM)SD_kT*= zBVoe4|4Ww&`^^RH7nmrC{{`XXg#X&@-5bsQYwx~Y-7t7>c=xsH9);e8FYjAYJ}>nf zBvQXUzX-hIPWV}t2Cm%Y5II9$du5WmGev>D$t4He8b0B}s>7m*hD@lSv)~ zyUU6>noVtuSS&gwI?f+ou@asK&5=sQyL zcXSW&t$5`G`zP{}=Obo+J-+eDEDOz-lgds+z_i_sJr_R|Yq zmo4R(=njbK*DksoWGId9)J2V&{i>UQpJWmu?fq*)=e!OH1@3+3zTUg{s(X}JcToS= z)Xgk(qj$fmZU}y}ckdCmol;)8I}rRXBe)FJ=z5yJe^i2@RFNC=CdNoxJxv?_<_<|& z5HZsm3nMW~F^(!wXWh0d0>$KNscxC|6jYDERX>US6*{vbOqx*8w^jO>%r7@G9Z50U zq(m_WJ6I{0d3<1@(Nh?-VB^1uK^F|_@A-A8=oYPL-&Zt&_h`4nP|wspERJrY!llub zI;ztOOz%$(6hzum<|o#ec%qMp`3}4o3j9bA^GO{=nioAGlD4hL`*?vOeO2DKP33)$ zXUY4v1bN>J1#*tNhSQEiagH0#Z1e*ifB%ErLvl)V5Ejz^4kwl=!}c`HVXM~P`@}Ns za5??VJ>#&goMBtk_w;!DUXi_ED}#ZpXFnIDj(%=_DfIs%dZBI87lCZEaB0!tt%w%5 zNY>=fsmd1i>BA7-iQtpDU>Z{0u6Q1^L=i%>!oAfp?G8oZcrdHG+{x&-NUdiuzAk+^ zq(4TE>eh)=o?{z-DH!k1n)mVIOf((9J;sS+`2VS9IOS9AjWA#>m$Sy@XE;fj%BjuM$No458p%^yj% zz7mQoXKh5~YPpXqiXVQv%F<0rW1=z)`b5h}GX)<5FCL8+7-1QeMJh5b8YAwHce{@f zH_O~0q_R-;qcT|&ktKg<^bsx~MiZE;`pfBIq2wdyLD0H}Hx%RwRFqBFngtR=OxWMe ztPJO|i-}6iK*?bdq2jAVDTTH?XCEI(8MKf81yO}^a`#^*iydA+g&$>7yhq$7RKD`J zq5i+~_1~tJ^Um*zj)CFr55b0_&xm1z85D9TLI7!z;n7K5EE&qzj{DBh+%e!xaQ6whF*Hg&o=tqj&7Z9mZIZT$;X_nThJbIXUUbYP|?wc|yRUSP{ zZsMht5{nO%40}alc6(6t44$X>4S1^9+X*A(E@M6MydtT7DI?Xg=$Awk&Ss=ic?-Th zRJ9^{fnY z+8>aNzvlbHXVlFh_}kvSA=N*=rc2z*hiHit4Iy3M_+CXD^qQI4|-Xx1@#~;FM_tftSE&ng9r~hQWmLmL?}fc&EuO<=4b00i z|Dp4VowNmrAI0*F0*uT%<@*)nw3~riI%hGb9puc+A>$lFG=z}vWgQT-KyV+@U^g&{^tJ*gqo~e$GrtnO;G2%>rO;%=dL>JEbn-$t1hEte{b#)zlKr40%Nf@_q(m4jy_SkFgj!rlk_dZqQYu?Q~KEV&^>}A89hZ7N5Yi!>bK5PzvHz~ zc;p~siX!xLF;B~S-=}Z)u6h(7aoEZyN?1=8a*V788^SywcY!+?Pg_7G=+nPIxE|S> z4Iq##s7KIC22@=IX&a?a^ouIlJ)gZV=J1G^alf-{`{@e-6Sbd84k!}+6J%-F>C=Rv zp_o#W6I6ZOmi{V~4d{vb%7-Gt1@8-950!h@w1yV)O&F0DV#=?NXn4(+jpKmmgHk#& z(dP2MCDOfEuYfrzE1@FAj1**qawZT?f<;m0APXg0!o_kNy0D-s94fNH`9sBXbj2#_ zb7(eYVGhzo>#)s$|5&H$R@7oc=5PJlWZsqIu(ecHj)z~pGktkFY_;NRqs@okwodoL ze%LC)KuW3jrfrxrHp$!@7*_OcxpK?$#4cbi*Z#4w&199*2yEROX1YNVb}nHTVh$_1 z2$SRb#I;ad?5~jJmN2RHD!59ox3GY1vB)EO#YYx#8dfI@a)J-RBK^`W75Kz_%t znX4kMp0%Bb11VYx(*R!TU@q3 z9>wQ6#q{J@_>kcX!Z(uo#Ily4kl6?O4RYX6h%OR$SGi}kPOhWNez=$q8mULH`d^?Mdiu;p*(ySv_gzx&f%0{P;{ziH((jg)RW|yWW!Q$x-12S(QH=0b+Roj z@w%XAO7fgYoUX}#C@uM22SV~*Ft;Hd$mu8_c^WFQ2eRhtI|-?P;;eY!+Drrk)t6J1 zYHNlw-w5eFs|)?Xrx)DW;e)ddkp%nYoV(o^JX5yhg&^&#Ee&g@dY8hHny zNktSN>E;-C9kf98LxkHcCQhlCP@W%i-Yv#o=f_7@^PN2IQ_-@NF-Uqf+tX0%V>DOI ztFEq^TT@-dG5(UcSGCvc{EyT}-h@iK{}g)EGxVrIjGe*Nd<}O_JeVPLw(2wW=?zOs zNcUC<`7$AgiE{ZZYyvoJd`099!W)f&*O3z})jZMfnGL9-E(~HBxTq%}8-d+^)%mN8 z+DbQAs;`r3u}op|Z8)ZuKlx3&e#cgoN+IF~1le!Xfki=s9Z5Mfyvm1u4{<~}k~aZG zl*6nvoKJ307+L|5%jk&vSmGRAP>p)fJnF|$a&D%*>v}zEVSoa`@;_U5;UCC7*KQDN5tBmig4s~1BNs7)5(_iOK;wj-bh4AMD ze4akL!w1+^>r1KI8PYNQEPWZ~1{)Bb|E$je?t&Sa9_QoiMb2*psqW3eyIG&zxW#G@ zc>l-{|6ChzK2DkOw;|v+WZ^diyd(?vU$*J(mSo{;*)jC)UF=o_`V0xV+!XLj1705R z+rWGE?xs)QfPX85zd3|23HZG{dum^?J?dx|`{K4b`Pr&3{oKxRhWO`0`oKnQ{-TDj(BJf*Jve%zgJ*P# zoqd7T;>dg}{noxm+Bfv0Lb|u$_SMbL2D~ib_GQVAfS(X>``qHYA%ht@OuEyzd`}Iy zrEmDGfLr>8&k4AtZ}>uRDW`o>sSg~4{M85CETj8$om&&|J{vWd+a1E+5^(#<$rjx5 zcW1!us~@8Ret*F2lN($5`S3f{zu4KQ`aJ5~&jN1Wlr#L70k^NA8UA9x?Q3I(?+Lhl zp3Cr818#IM{6N6_Y&66_-wC+YH|4ACS?sL7h8G0f>T7s^aP2?Ap8Mv|A4Z4pMt=)` zY{1R_yWtZRAMVZ!?P2&-#mn4OEw7=@ouc>%_hsgT^uK>nyxd)o4)Q)n@!WA&rFibR zJ4^B0arZBZSGX&)=C_Z7_cZR7YxrVk_OPv3jf(ekB~uLX&x;fzth#-p>c+?uM*+>=q55 zOXrP>=hFEu#dGO=kKz^Xp0SzvejU6gI&anR#m5-0U11{+i;$-3xxl$Gr$(>1n^$2pSwDYms!DMUI=dnR4sgUz|EkE;b$w}osQw>1^mgZ@%RbF zhr8#lu#o;)uXvf;@PSPHlZuaUA2>S`Z&ke9y?#U{zEbgAI$x@IE}gp+&!zL#idVQl zo{*XTb>KbG`6dlt?ChGV#rk}}?GCBo8v||^ISs!PT;_kf+UNN{=bjF@=>pvmBJ2<8 zSa?h4?SR|eKEvM+xLvO^Jl1)t*zJDY5dZ8KaJ!ddz5@epcVP@K1())h-lOr8$As{P zTlfaIuV)(-WHw6#FpHRG?yZPtoh+*ijQ#dPcqZ_rQ+po z*7!{PMa6U5Wsl;y?eePPx$Sa5@d~%GE;Idizc80h)gdZ1hJN?@p@G0Puubq?z*qsz`OW$x5H8i}XZ}=GjxAYC48*n?A zGaTVCf5YA9|1Gl}mndH5t``A9+TXc~k8lsk1}*LHe8tOMNo5+yYpvqB?YLa=+;(hI zJhvU&6|ZpXKAD;RYVe-g5j|u6ik*q)E!KvBo8;Z_e+{@v(hdJiz)hlU_)P&fQMBQo z54eex4d1AEsoRsq_ui>^KlfbLymYtX!`(Z#XSU0KC|>4vW(iilsrU$29mKPi?>5EZ zZ%;JDKOa^+w_U!gcy7Bqt$1#`JfnDpn|@Jd`p<#))GjY-_+n>bWQ+A%aN&#G)PRI) z|4zgAbFXLdGp~kpOwelS><_q!RSkbD;3gO~{JoHVD&OGITlvo6ZpFQs^#6e3W$sr; zS_=NzU-1!cB(+HY8>l$rVp2Nl+)%}H>0hpRF8z;IJeU5*DPH0J&&8SPSAzF)-Qjf_ zB*FJPQzkz=K2P{bA-u`QIzmC`mIOM#oG1O?=fU^p!QaY*zn=#$pkwsZ-v;NwM<~wzGzi+~hw@L%6Mk97^w+dfO=zw$RV;I{93H%vNb2Hf^>UT*8$l7Jh(R}#{x54i15cWCO)tqr)zooswv z8E}(3*}n4HfSVkNYUs6z>ROeI)wkPfE%AbKj5z^9vo?pH}l}{<-z;I=k(O> zgY)3S^WaD2!H>yUCMI>UjEHLU0qwp(&X}n=E^!Zc2(tMmuT;(ZRs@pGg$ZjddDr$#GXML_QEEmFe9<(U ztonsdM`~WRwQY4PXj#D$$>zEi8MukM##+WK0hE?hngpk7Qqz?@GxU9imVSnkZHC9S zFf){FGqgxEP6-BkwU2h!;}ncdj;=3(M~9)5 zUQd(RUrt$kXPDXPFj-!xeLknyGEEa)%dS1~uT&tBn0xjaHFFYk7M+<$_$2*$;Sy=9 zK~Ip(?YgEUn0A-eF^C#Eh*Znk+0c<#!7|a9Y)f3+#)QaZ>|y-t+gpMsj=EK~A%bh| zSkacOt;e~nrR_re+E~Nd;SUUvmSnzVu_7y4jb*R7uBp{^PMyNs-Z^6$zl|-+8OJ)= zqbuFg$(-v4525Q(19wt33EO^gt7}R&$*f$<)V+F5G7;7jpSagcmmqbmtKG7;WRen* zV<$qzqhHP!hV#E^BE`vY21o*3iMSR@c}fy?c#<%hw1Q z$;kU^M$6auAgOd4F05;5Ue>VO1?psuQkPa>n& z_)F>Qlc@%E#tpDgE{FR{D8C>x18fl=bjRYd(1bS_B^W6`b3v>`$LiLk(@LZ&08O-C ztoBt&sF7%Runy)N=_c~JLP>mKYeN&JoCDvese~r?lXRl971}0AVY!pm(iC`C5+#^h z($X_5WY?V$EiPTF!9VDl>Ix6v?X;IKYiNX%YHV2E?6eUXYxEbNtiS*6`{7DkBWhw?&rm=l*HpEjn^h2*BPMsN^;oUcfx!}X#u!L1h zEQK?sb(eKE*SV!q)==bn7L`O|Wmqn)Uidq>ujGYpSx2%FCM^sPy%Zv`d1;}_s<57* zBR97*0X4bAnIBnnM$LlRX&bTs;j4%TxW{Ksqr14ztATA=bRD9DnwPz5A z5yJnOyvf|6H=7>+hOp1I=U59Ep4su`7=SHYD*r7ZzCAAw5kx*B^)xw&?Y~b3d;k6S zTde*khXD4x0Dt+jd@YoN`1Z783d`Tp(M0(iEg?R~8Tx1EH1A5s;BQlhf7)+${u}X-{4M_GcYMgrTm5r) zR{68*e`kmvg@jG7Z^yTN{OsRZS-?HGCI8UODZN08|72GF+4X;rxKehDZ|7H6?*QcQ za!H&&i)T-($0PWq=im23K0;sd;7`V9`k%!&?7O_A$DbAA&kFHZh5RjicKn@q2xjHq zGTTSk@~Dq7-vqZIfvn`*3*hPboBqGWw|*X?r|4krS9nR!-}L@>hWxWN&a=z^`;h-6 zOE5ew{tvQnE5AMWhxl2++^vs!J{HeVQN#5$HN-zRdBrF8d?4RdF)=T zc1h!$)6>(nFEjJc|IhsMzviFWyZUr}srZ+$s4#K3Blw>5kS5_|aKn1*%*u(KT5ry4h%Rc_x`p zp{vp1QwOT|D;I55E4T9iQ(?KYJzaXgv!O zAY7dUPG+A7KR*e)Gzq*q3H;V1@VX@MKS=_=Jqi5oB=EK*aBmWLZxVPU34B))_?{&2 z&nAI?J_-DbN#Ktqfj^Z5{e*Kam9fPf6fEP6B5vA0Gr zb_yHgE#a>>piXu!L$>Te;d1%B?Hw+!-y24~W2f%Y!^TeD2pXMU1cD$SoUm#>KPtY)E(q6sO2zftgfIp1x@96Dwb#!%? zP`?g#BH<9tN^p*$cXU&~%cFaJj22fA3b4kFwd<(SLk*N}A63L>_>?H^6iDz<``ghZ z@Z)jt=mQ!qa1Kz+t6a2cX`*SG~vF-?6gS>PBd_W8c3()gr# z^>J9>*8L;H0;j!GeHL5bYOkX{%>tjJkWuDY;OQ24kp+I01zu@^tF529ofbI7T=i+N zz~?C-%FPz|H5T|?7P#^S>Ta{Z=Uec*EO4t|_gdhY7W|zSIPEv;({F*R+>845S>V?y zWR&|Y@I@B*qZYWzb*Ov50{@r=|BwZ4_3L2^{CW$%;n&-JhTG*a^l&6-gxI>~ru7>) zxNh6B)wN~omi1d)HCtPoT{U%e)a2m0`t>zi8(UqQnzuH!rnBx)#1)E!JfT>vT9Y5$ z?WR6gH^l@vsvh=sz@7WFHH8!a3Im=(kJo3o0zsGC8#Fq?fneWSZH?C-z`t%|JO1@k zwtK( zwQ8-lr8f}pXyGnH3q-;QKpI+wy}>|7VMmV}i!ii2f55-0v)j8u3j}3Xdms?*2>3nT zPA!l3c!Gf*P1g`^!y1Vu(R6$@tF-kPr6=Gvw32FVV_0*0L!`=(){RNq4XrB>=niQ; zIuP3XG-xSb)BSGE>qq$3-T9V*S;z>xB$K3y7*iIcs3`ejYus+bt&tA39bTVL!+epR zK*S#glrI7HqhW*4plxmI#<~y$qlYy;XlPB%ty(BT9f2K&TQCKVX|Y&#Pp=R9)r3r7 zkZz2RNyOj@*a>w7c4%w18^Mq_;NKl@&AYejL9gx)uT4Oy7Ub7^FiUPB>`E`x-R*@C z6;E@?aADi-;C2@+G*=4;H0(UkP7qqtLS5LT#9~S50$Xzjy~jfGRW0bU9&VCdwU+_u`W zQN!j)%R!?@hs}d)k`1QTbgW28fii?>+=!d5#So#uIu$oGtRJ$VG$CpgAtR`{4G*7) zaAt%DRv5yGh1FA@!AZUTPT^NG;v7k$`HuRQ#+tj=Z%Lmispie~O|F*ux{WRM>!wNT zo}0H#(`$(ac}Yu5v`S@oqRa*Vdc+q-FjBQxf;3%5Yu0V5TU(uANNH7Tigp+8F4;{k z;o5=Z3~O~!fLO}}H!feGlg%`>O_5Mo3r75;A+qcQ6IcwPBhb@BaioW+pbxZreFphG zchoiU0eUU05c#5}BdH5=oeFE_Dutk+9tLcTzSGKdR>px0+_(+Dc(`~BH}&SZnc)XJd~Txlp5kwoS6ZH0?d}J=}hiP ziB`mQwAp9qi0kkz1W|YgaX_1=CG>LVQ64D#Ddt}>kD@$7`V$4eAkwdZKVJ)Aq~H#( z+&cpPT<*w*$Abh6yH`@i;^dpJQVyZZ0HQwm+_dzAB83P(%1Jz4PeAUHtOc7N*_t>8 z&WOmvH4ZdzL?-d3<^&;$Lxm*QTacS6zK; z&AJ9F1JQ9sW#vj`+>mQ9)T?6y;<6>{*Zol!6ptzWXjjR6-JRu@s~ zxFn%f#p7Df!YcKQI}!gLaPq_O70wJ=!0+pmn~>O35ii@}H_6WpwDV1ixK|K`u6(hh zOAqQDlyu@E!3&oZM>F9YLAWLk+7$c%z&VaGS)9dqR3f(}PqxfLflUUh9^{&s1G^ou zrJ|uV1%!t$$9CZjhQbJA?Mx;%>LzoBb5UXtf0+_@C=!_}BHlEEwce=Ys&8n35%3qd zEm^B2k&C5t(b~P~jIJr%k&c9!f7ELDDG!lz`EmqGdD7#}c4~IVx@(h%U`pLrl_c_A z5u&m0$-ST>X*!{TqLjGMDl4`qE6OsDB_Bz2#U-U>kf1C|P%SAbEiEg{g|relJf8@; zhRQH_;D=RtkzRx%?W-sfhN6bn>>6)`2u1p_y*RLQJ|`G_T~F1}k(M?U7Db zQ18L=yDcmvIDT~D9EX5O`6CW>?ublSTa>r>*U|?}Q!Y?=x-3t|a zhFgFvY{H)@A@bn#i)1L&?d>K1ru>o&2lp#h{a7@O0Hbb>kTB6s#&3{{SW^K06Nq$n zsT@=|Nz|Cg4tXQCP0rl(wrp;0Uhmr4RI{l*!8M5JMaI>*v8mogdtq~Ps|aZcup4R+ z+G;jzT=)9|+$M^ubXf?F#dUx(VtloXucqdZ6h*>ed{ zMC;PGLs2{qwi}28hPK_SBi*rn4X~Dile#tH!|@)YvZS@FMffukY6>$+&1ar z#8qE25=Jdox@0bQmZ)3uq>-Brq>PP^-))Fg5(a{=g*uS{(y=%3g`!*yg$(*~Mjn^E zWJKy46s0x8->%~}C|~0zETZ7Y;pqDKbHx)*d?lnVMxw{x6_#bk-ys)W z`3`xSL;ayX93Kv@OposhZP(jB(SVY^{~f5occ&;Xq3lOFx0bO3DDzMrLRpED-ii1t zl<%N?dmUpp;0x}@?qKXLl*r=OdJloV+pNTxccEl;tLo-C8|L1a z@kGkw`2Lz`=tZDj^sDb!2}bWk5sc{RL$EeHI{{Pk(EB%GdH`!h$;Z|(w)Sw!fz&6` z9(RQ2W@`=e0DtAxpZ|;z{?m9K1wQf1>gE+S5;n(8x0l)GW*0TiW3T-DXLa;FHu3c; zp5s{WQToCdoNu0cqmxGeV%pz09!Y;}?i2GK&q%kA&Yqj?+ziTBUwi$HH;J6)%D^0J zwu0WyX=$z^PV0Cg{qec>0lS>;)z`orK2I*Qzjh<*|H(~wM(xZ5dDg8?BP;N*w^_h(ckRTAZM745Hph$Ev)HdkA7oz}wfEoh5X6zK* z#U_>j|1FGn#-2S?+g3ZVER7v-VXijDj*Q#c5KGB^{kYxrI>FAMO|)!kDuc>{|B;$; zLmJDXK58$OZQ^Tk*YkrHn{9)W7#r_AjSz34Kdqf`fG3A7^8#LeXmw1kd)PbA@le(* z_VS87u1lKj*4M}ESwrEesc&9vt{I%%b>h$q7>nkgI?1q3+u(bdY+`a9Gq;R<;2S?qpW8f!g1H8sNOD$N>kVgO^>l!QbAQ0)4@bAgigXoG(v3nEvt|QpWGuZ9}Iq z) zRUGyx;Z;joImg3*jovHqE|ze-&l7B$#2a41VSfo&n}?Ng{_Yk0?WOVNv+}a%KG(wa zy)?xwcRB+L+X(+~RyEwk%G>@z*wDaU*d*-wsXJNgS)zd* z?)lI*zg9~z6<@^D?L-fLM(vK3WY>zP<1OF`__s)&oQ16XP>Qo^--E1d>>=l|eS3xN zKlLE&mTardW-dx)Sx+v2Kfn*3T+8m?<6y0A&?R5T+t--YG}dhJh_#)8j0(_hyOx!Q z=Q$_xVB;KbS(SNciKB}2PW=O=YRy?<&EP*D@UaaIe9eb8v+}XqxSb7+-o_@zn&(V> z!N#(p^ETXQ-T-_pm6exD{m}Zq0sWAFpM?zcPYM0Z171`&xgzHE|H)(q^;crds9hcw zdQStsbC$H3a5c9N8({qz(q>NI!eN(FG43K(h8WC!FBfA`yj60TdU4Lg=v#Yl8U1%W zKgII~o>%bv7|)A%{t3@XJTKt+4xVr0`39b^<9Qa(Gkb6ODdsr77`h={lRgeJ(@~Rd z+H8OR_uJs>I%GZynV-$>Zy|YbJ3^amXcV+;6Fb7?zki7dpB?%!jW=7asI1zAZ+;7~0mRS{^nZr@J%i+$IW!FT zaVF2A~qGFchPn8(x`t}z3E zX|UPt z7vOv6-k39S;hDxk@};p?@VJ~4=f9D8Z0z6XOuX}zronS>&6zm&?54pBQ|P;Z=hN6T z-UaMr|FQGG1kQVa{SNI9=1knH_`&>SYxS6Kpo$&2h&Jhx_&I=mfcorOe@p)oW~Qq0g|}i@>kdH%F)7UmxK49iI2_yo=`oo?qhmBYpN)hzDFh zzY_XUx*@%kf*<1h9Lc`ZIYD|MeVl_1&O`3&!ROcp=fn!=bdc7A7*bl#ZdZ0g_Cj_; zG46~#b0yi$>lA1QufiFJ@}yO67B!`9jq1mx`}<=yx6=B`+Y6;eaf*; z9im*|e@jkJOF3!3m?24A$u;HgVft%S$@=laPE!g(I{ za*_5E$U3rs?_DD~*po)>3x_@jTBl9zM+>9-(LUlq?nikyu=3FjY$5_VBA!NRcO{|i zv(PqN2ihl*BV0F~)|Z6#J_~KvV$ixU?@iNbwJdy*Zan5AMF z#V(2&Dwcno&xM$QI8J_W9L6SZ^7>b@j^TjFO+}k zYi;wb``OqA$HdnyW1ab_7;E6e%h;1`^ZylNMe|{qBRoU+A{QCSkvYOaz?^`6#>Nih zfQIjbn49(%jz5bXp?#eDrMt+az4&WHvw$7pXIbpm?;-a&Ok*Ptc@E|KXj7f|8%Sg3 z-=RD{pB>2o?pefyx6nuV(hGoRvxOI)TF6=}ux_Gx^1kxQZ)4qu&_`=O^C63_`z-PX zlJ))6hx%8jl(G5DD{}q6y_`WbU&31Wo>pcv`+#?Ta%vGjYhVt_A@F;#17~s0;yM2F zu{mh`yDj79ye!5YeeZI{9^gF~lj}%MZxPS4ZU=rp&cu+>aL(vT4r>JLxXN=6XK>gC zz?=?|YpIw^I##x&|IjvH*;6HO>5M_=i!(UqQoKF>E_4eVyicFS_hsmX_@6e$iP#vq zmNC>BW#B(C25_=u-)TF11mloiU4YS$!-XhwOm&JJ=YlDGz6M(p!np(Jkl7F2;7Fq<-Q(d>=Er z{_7gzVI=1;*-_R|yTsdz@}k5$;AdvT#3I5Q&RKRd$4f~Q<1IsZUg4d_G2UD{ z|6)zEb%A*SJC(PXUn+=3Fo--!S3J2wQr%*np z@U}yKzit+Iy(H(XA?v(PD?H5GFn*_)w{j`ROObL}hVsh_?;zy&u1k#9X6JYZCEh8N z`xV}|AirOA65eo5-K`wYDi?~Qq~8OO%h)>sF>eF;f7VcnT*oq$yA@tLmLW- z$NlBS<`jNs^(xkV;QY-dpJVTh{9I=P9&g`eGn-U>Y^iyhsvoW}8&H1@e0}pJlEuQI zI<#NGSfL-%y;0^6azrsk{~YrcQ6Fb?|A9K;<1T^w;A+t~%rf%LQc)jaW4UILsE@Ov zIp!(_|CeRvN(FDd+I$07>738Sr6%r_McuKQuVF;AF>Zl*qoUux!X)`|8lS^lr0O>m zbDEQC4ChLdBY@GFe%yg*!%L`5XWKG}}+07J*%M`BrI&%`^LeAp{ z?zoEE&4l7@?kCLOqE2I96tHoYmu~)=!?K3n6|fQZ=FR3i67Rf%onCJKQq+gpy`|>c z3Z8$Hc}CS6XPG}ooyLDtz{c6d1?Io1`ar7ry6l(o8)v7}%$HUFr7ZI$Q6FJz(#;oD zJ)CBqmUKT*u*St^RDS=m*u>ohpBHxtLN*y|&F_f%FgsLe9+myyQm_NJB0u7|``PI= z=CJI4PQe~6H=k9w`E$)fqCUcQW}1Ji>U-y#2T`Z>4k+BiHuG`Szi+nrWd+}RwfTsu ze|MF6K-G(`Gfh$7&-z!JxJw}ZhuOYt6W;+)eLr&-oBKq4gzdl9{0migILrqWer1{Y zX;mMsHg~J~(K547)r&IC?W#VenITnouQ7X3=XTLt3x9P^Ozt{)s2j0>$!k_C5w;E=|6+^ z5=;AYXxCcWpGUjh(*6S44_MlNgZ4p7`^#vbu(Y2*`>dsX5bczOvH8D>_7Y2b2<=)+ z`#H4RE$zQY`vFV)TWBA&wEqF^6PEV#XrHyTPoSN0ZEXG@puNP>{vq16mi9|%w_Dmj zLHhwq`*pMrTH61H_6gBedi|f4Hifw6impu}sNb+ImhA~S9`-B#StQFMNthzr`(%Z_zN63SQ=(Lct>#s~wLK~kuM%kd zdrzv)lI?N6%-X9VAP1eZ=vNCmsD1*E-x?5*KY3Z#E;-^ZV&jvD12dR^!NzY zju+eAcu7eMmX{Wlude7=p>_ClyxyXI<|0Pb^18y|-cWU6A>IL^zn)Rh83=UZXPkjZ zu)`?8doqRe1~gt~r9Zsk+upMZugBbnA8Ke-75eI`s&-FVWpRmKT(G0Pn>a1xh4dye=s)3VMC0 z;8hS^)Y?NKR^aZ#Z(4(_!02*$@P?$}>T;vi>5mjhS-a50-zPAFy^od%PVCZ%2ZYu-=YE(OWhcB_K-vZ@iIx zgtiAX6dP6i!S~DjqssWWr{bu6LX@S7e+oQ|ij{u9jAyD`rVx|R zBe!4_`_%j@9;$Lcc2Jll-c_7dad{_TaM*yNSMg7kmFTCB;#=XV@=o+otW<3kFIA}# zF+OU3HI9HX_SdNV)+QhqbI~GshE9Y11R;A*De3wuZ{qQL? z@ENWXW!&e+Nv_|33cLfS!0#^N+~C&oWXkrj6$}zCBmYUnJ0@|0??X;uuyw*=Cx}sCy{{|txjJ^N> diff --git a/manager/app/src/main/assets/ksu_susfs_1.5.8 b/manager/app/src/main/assets/ksu_susfs_1.5.8 deleted file mode 100644 index a588f43efe0abc68d28accca19cf3a2410e99d92..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22184 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 literal 0 HcmV?d00001 diff --git a/manager/app/src/main/cpp/CMakeLists.txt b/manager/app/src/main/cpp/CMakeLists.txt index abeb419..7fc4fdc 100644 --- a/manager/app/src/main/cpp/CMakeLists.txt +++ b/manager/app/src/main/cpp/CMakeLists.txt @@ -6,10 +6,11 @@ cmake_minimum_required(VERSION 3.18.1) project("kernelsu") -add_library(zako +add_library(kernelsu SHARED jni.c ksu.c + legacy.c ) find_library(log-lib log) @@ -21,7 +22,7 @@ elseif(ANDROID_ABI STREQUAL "armeabi-v7a") endif() if(ANDROID_ABI STREQUAL "arm64-v8a" OR ANDROID_ABI STREQUAL "armeabi-v7a") - target_link_libraries(zako ${log-lib} ${zakosign-lib}) + target_link_libraries(kernelsu ${log-lib} ${zakosign-lib}) else() - target_link_libraries(zako ${log-lib}) + target_link_libraries(kernelsu ${log-lib}) endif() diff --git a/manager/app/src/main/cpp/jni.c b/manager/app/src/main/cpp/jni.c index 2862763..5715300 100644 --- a/manager/app/src/main/cpp/jni.c +++ b/manager/app/src/main/cpp/jni.c @@ -5,430 +5,440 @@ #include #include #include - - -NativeBridge(becomeManager, jboolean, jstring pkg) { - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, JNI_FALSE); - bool result = become_manager(cpkg); - - GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); - return result; -} +#include +#include NativeBridgeNP(getVersion, jint) { - return get_version(); + uint32_t version = get_version(); + if (version > 0) { + return (jint)version; + } + // try legacy method as fallback + return legacy_get_info().version; } // get VERSION FULL NativeBridgeNP(getFullVersion, jstring) { - char buff[255] = { 0 }; - get_full_version((char *) &buff); - return GetEnvironment()->NewStringUTF(env, buff); + char buff[255] = { 0 }; + get_full_version((char *) &buff); + return GetEnvironment()->NewStringUTF(env, buff); } NativeBridgeNP(getAllowList, jintArray) { - int uids[1024]; - int size = 0; - bool result = get_allow_list(uids, &size); + struct ksu_get_allow_list_cmd cmd = {}; + bool result = get_allow_list(&cmd); - LogDebug("getAllowList: %d, size: %d", result, size); + if (result) { + jsize array_size = (jsize)cmd.count; + if (array_size < 0 || (unsigned int)array_size != cmd.count) { + LogDebug("Invalid array size: %u", cmd.count); + return GetEnvironment()->NewIntArray(env, 0); + } - if (result) { - jintArray array = GetEnvironment()->NewIntArray(env, size); - GetEnvironment()->SetIntArrayRegion(env, array, 0, size, uids); + jintArray array = GetEnvironment()->NewIntArray(env, array_size); + GetEnvironment()->SetIntArrayRegion(env, array, 0, array_size, (const jint *)(cmd.uids)); - return array; - } + return array; + } - return GetEnvironment()->NewIntArray(env, 0); + return GetEnvironment()->NewIntArray(env, 0); } NativeBridgeNP(isSafeMode, jboolean) { - return is_safe_mode(); + return is_safe_mode(); } NativeBridgeNP(isLkmMode, jboolean) { - return is_lkm_mode(); + return is_lkm_mode(); +} + +NativeBridgeNP(isManager, jboolean) { + return is_manager(); } static void fillIntArray(JNIEnv *env, jobject list, int *data, int count) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); - for (int i = 0; i < count; ++i) { - jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]); - GetEnvironment()->CallBooleanMethod(env, list, add, integer); - } + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); + for (int i = 0; i < count; ++i) { + jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, data[i]); + GetEnvironment()->CallBooleanMethod(env, list, add, integer); + } } static void addIntToList(JNIEnv *env, jobject list, int ele) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); - jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele); - GetEnvironment()->CallBooleanMethod(env, list, add, integer); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID add = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, integerCls, "", "(I)V"); + jobject integer = GetEnvironment()->NewObject(env, integerCls, constructor, ele); + GetEnvironment()->CallBooleanMethod(env, list, add, integer); } static uint64_t capListToBits(JNIEnv *env, jobject list) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); - jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); - jint listSize = GetEnvironment()->CallIntMethod(env, list, size); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); - uint64_t result = 0; - for (int i = 0; i < listSize; ++i) { - jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); - int data = GetEnvironment()->CallIntMethod(env, integer, intValue); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); + jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); + jint listSize = GetEnvironment()->CallIntMethod(env, list, size); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); + uint64_t result = 0; + for (int i = 0; i < listSize; ++i) { + jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); + int data = GetEnvironment()->CallIntMethod(env, integer, intValue); - if (cap_valid(data)) { - result |= (1ULL << data); - } - } + if (cap_valid(data)) { + result |= (1ULL << data); + } + } - return result; + return result; } static int getListSize(JNIEnv *env, jobject list) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); - return GetEnvironment()->CallIntMethod(env, list, size); + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID size = GetEnvironment()->GetMethodID(env, cls, "size", "()I"); + return GetEnvironment()->CallIntMethod(env, list, size); } static void fillArrayWithList(JNIEnv *env, jobject list, int *data, int count) { - jclass cls = GetEnvironment()->GetObjectClass(env, list); - jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); - jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); - jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); - for (int i = 0; i < count; ++i) { - jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); - data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue); - } + jclass cls = GetEnvironment()->GetObjectClass(env, list); + jmethodID get = GetEnvironment()->GetMethodID(env, cls, "get", "(I)Ljava/lang/Object;"); + jclass integerCls = GetEnvironment()->FindClass(env, "java/lang/Integer"); + jmethodID intValue = GetEnvironment()->GetMethodID(env, integerCls, "intValue", "()I"); + for (int i = 0; i < count; ++i) { + jobject integer = GetEnvironment()->CallObjectMethod(env, list, get, i); + data[i] = GetEnvironment()->CallIntMethod(env, integer, intValue); + } } NativeBridge(getAppProfile, jobject, jstring pkg, jint uid) { - if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) { - return NULL; - } + if (GetEnvironment()->GetStringLength(env, pkg) > KSU_MAX_PACKAGE_NAME) { + return NULL; + } - char key[KSU_MAX_PACKAGE_NAME] = { 0 }; - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr); - strcpy(key, cpkg); - GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); + char key[KSU_MAX_PACKAGE_NAME] = { 0 }; + const char* cpkg = GetEnvironment()->GetStringUTFChars(env, pkg, nullptr); + strcpy(key, cpkg); + GetEnvironment()->ReleaseStringUTFChars(env, pkg, cpkg); - struct app_profile profile = { 0 }; - profile.version = KSU_APP_PROFILE_VER; + struct app_profile profile = { 0 }; + profile.version = KSU_APP_PROFILE_VER; - strcpy(profile.key, key); - profile.current_uid = uid; + strcpy(profile.key, key); + profile.current_uid = uid; - bool useDefaultProfile = !get_app_profile(key, &profile); + bool useDefaultProfile = get_app_profile(&profile) != 0; - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); - jobject obj = GetEnvironment()->NewObject(env, cls, constructor); - jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); - jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); - jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); + jobject obj = GetEnvironment()->NewObject(env, cls, constructor); + jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); + jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); + jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); - jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); - jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); + jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); + jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); - jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); - jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); - jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); - jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); - jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); - jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); + jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); + jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); + jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); + jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); + jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); + jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); - jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); - jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); + jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); + jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); - GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key)); - GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid); + GetEnvironment()->SetObjectField(env, obj, keyField, GetEnvironment()->NewStringUTF(env, profile.key)); + GetEnvironment()->SetIntField(env, obj, currentUidField, profile.current_uid); - if (useDefaultProfile) { - // no profile found, so just use default profile: - // don't allow root and use default profile! - LogDebug("use default profile for: %s, %d", key, uid); + if (useDefaultProfile) { + // no profile found, so just use default profile: + // don't allow root and use default profile! + LogDebug("use default profile for: %s, %d", key, uid); - // allow_su = false - // non root use default = true - GetEnvironment()->SetBooleanField(env, obj, allowSuField, false); - GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true); + // allow_su = false + // non root use default = true + GetEnvironment()->SetBooleanField(env, obj, allowSuField, false); + GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, true); - return obj; - } + return obj; + } - bool allowSu = profile.allow_su; + bool allowSu = profile.allow_su; - if (allowSu) { - GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); - if (strlen(profile.rp_config.template_name) > 0) { - GetEnvironment()->SetObjectField(env, obj, rootTemplateField, - GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name)); - } + if (allowSu) { + GetEnvironment()->SetBooleanField(env, obj, rootUseDefaultField, (jboolean) profile.rp_config.use_default); + if (strlen(profile.rp_config.template_name) > 0) { + GetEnvironment()->SetObjectField(env, obj, rootTemplateField, + GetEnvironment()->NewStringUTF(env, profile.rp_config.template_name)); + } - GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid); - GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid); + GetEnvironment()->SetIntField(env, obj, uidField, profile.rp_config.profile.uid); + GetEnvironment()->SetIntField(env, obj, gidField, profile.rp_config.profile.gid); - jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField); - int groupCount = profile.rp_config.profile.groups_count; - if (groupCount > KSU_MAX_GROUPS) { - LogDebug("kernel group count too large: %d???", groupCount); - groupCount = KSU_MAX_GROUPS; - } - fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); + jobject groupList = GetEnvironment()->GetObjectField(env, obj, groupsField); + int groupCount = profile.rp_config.profile.groups_count; + if (groupCount > KSU_MAX_GROUPS) { + LogDebug("kernel group count too large: %d???", groupCount); + groupCount = KSU_MAX_GROUPS; + } + fillIntArray(env, groupList, profile.rp_config.profile.groups, groupCount); - jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField); - for (int i = 0; i <= CAP_LAST_CAP; i++) { - if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { - addIntToList(env, capList, i); - } - } + jobject capList = GetEnvironment()->GetObjectField(env, obj, capabilitiesField); + for (int i = 0; i <= CAP_LAST_CAP; i++) { + if (profile.rp_config.profile.capabilities.effective & (1ULL << i)) { + addIntToList(env, capList, i); + } + } - GetEnvironment()->SetObjectField(env, obj, domainField, - GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain)); - GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces); - GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su); - } else { - GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default); - GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules); - } + GetEnvironment()->SetObjectField(env, obj, domainField, + GetEnvironment()->NewStringUTF(env, profile.rp_config.profile.selinux_domain)); + GetEnvironment()->SetIntField(env, obj, namespacesField, profile.rp_config.profile.namespaces); + GetEnvironment()->SetBooleanField(env, obj, allowSuField, profile.allow_su); + } else { + GetEnvironment()->SetBooleanField(env, obj, nonRootUseDefaultField, profile.nrp_config.use_default); + GetEnvironment()->SetBooleanField(env, obj, umountModulesField, profile.nrp_config.profile.umount_modules); + } - return obj; + return obj; } NativeBridge(setAppProfile, jboolean, jobject profile) { - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$Profile"); - jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); - jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); - jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); + jfieldID keyField = GetEnvironment()->GetFieldID(env, cls, "name", "Ljava/lang/String;"); + jfieldID currentUidField = GetEnvironment()->GetFieldID(env, cls, "currentUid", "I"); + jfieldID allowSuField = GetEnvironment()->GetFieldID(env, cls, "allowSu", "Z"); - jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); - jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); + jfieldID rootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "rootUseDefault", "Z"); + jfieldID rootTemplateField = GetEnvironment()->GetFieldID(env, cls, "rootTemplate", "Ljava/lang/String;"); - jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); - jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); - jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); - jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); - jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); - jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); + jfieldID uidField = GetEnvironment()->GetFieldID(env, cls, "uid", "I"); + jfieldID gidField = GetEnvironment()->GetFieldID(env, cls, "gid", "I"); + jfieldID groupsField = GetEnvironment()->GetFieldID(env, cls, "groups", "Ljava/util/List;"); + jfieldID capabilitiesField = GetEnvironment()->GetFieldID(env, cls, "capabilities", "Ljava/util/List;"); + jfieldID domainField = GetEnvironment()->GetFieldID(env, cls, "context", "Ljava/lang/String;"); + jfieldID namespacesField = GetEnvironment()->GetFieldID(env, cls, "namespace", "I"); - jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); - jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); + jfieldID nonRootUseDefaultField = GetEnvironment()->GetFieldID(env, cls, "nonRootUseDefault", "Z"); + jfieldID umountModulesField = GetEnvironment()->GetFieldID(env, cls, "umountModules", "Z"); - jobject key = GetEnvironment()->GetObjectField(env, profile, keyField); - if (!key) { - return false; - } - if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) { - return false; - } + jobject key = GetEnvironment()->GetObjectField(env, profile, keyField); + if (!key) { + return false; + } + if (GetEnvironment()->GetStringLength(env, (jstring) key) > KSU_MAX_PACKAGE_NAME) { + return false; + } - const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr); - char p_key[KSU_MAX_PACKAGE_NAME] = { 0 }; - strcpy(p_key, cpkg); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg); + const char* cpkg = GetEnvironment()->GetStringUTFChars(env, (jstring) key, nullptr); + char p_key[KSU_MAX_PACKAGE_NAME] = { 0 }; + strcpy(p_key, cpkg); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) key, cpkg); - jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField); + jint currentUid = GetEnvironment()->GetIntField(env, profile, currentUidField); - jint uid = GetEnvironment()->GetIntField(env, profile, uidField); - jint gid = GetEnvironment()->GetIntField(env, profile, gidField); - jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField); - jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField); - jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField); - jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField); - jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField); + jint uid = GetEnvironment()->GetIntField(env, profile, uidField); + jint gid = GetEnvironment()->GetIntField(env, profile, gidField); + jobject groups = GetEnvironment()->GetObjectField(env, profile, groupsField); + jobject capabilities = GetEnvironment()->GetObjectField(env, profile, capabilitiesField); + jobject domain = GetEnvironment()->GetObjectField(env, profile, domainField); + jboolean allowSu = GetEnvironment()->GetBooleanField(env, profile, allowSuField); + jboolean umountModules = GetEnvironment()->GetBooleanField(env, profile, umountModulesField); - struct app_profile p = { 0 }; - p.version = KSU_APP_PROFILE_VER; + struct app_profile p = { 0 }; + p.version = KSU_APP_PROFILE_VER; - strcpy(p.key, p_key); - p.allow_su = allowSu; - p.current_uid = currentUid; + strcpy(p.key, p_key); + p.allow_su = allowSu; + p.current_uid = currentUid; - if (allowSu) { - p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField); - jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField); - if (templateName) { - const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr); - strcpy(p.rp_config.template_name, ctemplateName); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName); - } + if (allowSu) { + p.rp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, rootUseDefaultField); + jobject templateName = GetEnvironment()->GetObjectField(env, profile, rootTemplateField); + if (templateName) { + const char* ctemplateName = GetEnvironment()->GetStringUTFChars(env, (jstring) templateName, nullptr); + strcpy(p.rp_config.template_name, ctemplateName); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) templateName, ctemplateName); + } - p.rp_config.profile.uid = uid; - p.rp_config.profile.gid = gid; + p.rp_config.profile.uid = uid; + p.rp_config.profile.gid = gid; - int groups_count = getListSize(env, groups); - if (groups_count > KSU_MAX_GROUPS) { - LogDebug("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); + int groups_count = getListSize(env, groups); + if (groups_count > KSU_MAX_GROUPS) { + LogDebug("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); + p.rp_config.profile.capabilities.effective = capListToBits(env, capabilities); - const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr); - strcpy(p.rp_config.profile.selinux_domain, cdomain); - GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain); + const char* cdomain = GetEnvironment()->GetStringUTFChars(env, (jstring) domain, nullptr); + strcpy(p.rp_config.profile.selinux_domain, cdomain); + GetEnvironment()->ReleaseStringUTFChars(env, (jstring) domain, cdomain); - p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField); - } else { - p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField); - p.nrp_config.profile.umount_modules = umountModules; - } + p.rp_config.profile.namespaces = GetEnvironment()->GetIntField(env, profile, namespacesField); + } else { + p.nrp_config.use_default = GetEnvironment()->GetBooleanField(env, profile, nonRootUseDefaultField); + p.nrp_config.profile.umount_modules = umountModules; + } - return set_app_profile(&p); + return set_app_profile(&p); } NativeBridge(uidShouldUmount, jboolean, jint uid) { - return uid_should_umount(uid); + return uid_should_umount(uid); } NativeBridgeNP(isSuEnabled, jboolean) { - return is_su_enabled(); + return is_su_enabled(); } NativeBridge(setSuEnabled, jboolean, jboolean enabled) { - return set_su_enabled(enabled); + return set_su_enabled(enabled); +} + +NativeBridgeNP(isKernelUmountEnabled, jboolean) { + return is_kernel_umount_enabled(); +} + +NativeBridge(setKernelUmountEnabled, jboolean, jboolean enabled) { + return set_kernel_umount_enabled(enabled); +} + +NativeBridgeNP(isEnhancedSecurityEnabled, jboolean) { + return is_enhanced_security_enabled(); +} + +NativeBridge(setEnhancedSecurityEnabled, jboolean, jboolean enabled) { + return set_enhanced_security_enabled(enabled); +} + +NativeBridge(getUserName, jstring, jint uid) { + struct passwd *pw = getpwuid((uid_t) uid); + if (pw && pw->pw_name && pw->pw_name[0] != '\0') { + return GetEnvironment()->NewStringUTF(env, pw->pw_name); + } + return NULL; } // Check if KPM is enabled NativeBridgeNP(isKPMEnabled, jboolean) { - return is_KPM_enable(); + return is_KPM_enable(); } // Get HOOK type NativeBridgeNP(getHookType, jstring) { - char hook_type[16]; - get_hook_type(hook_type, sizeof(hook_type)); - return GetEnvironment()->NewStringUTF(env, hook_type); -} - -// SuSFS Related Function Status -NativeBridgeNP(getSusfsFeatureStatus, jobject) { - struct susfs_feature_status status; - bool result = get_susfs_feature_status(&status); - - if (!result) { - return NULL; - } - - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$SusfsFeatureStatus"); - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); - jobject obj = GetEnvironment()->NewObject(env, cls, constructor); - - SET_BOOLEAN_FIELD(obj, cls, statusSusPath, status.status_sus_path); - SET_BOOLEAN_FIELD(obj, cls, statusSusMount, status.status_sus_mount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoDefaultMount, status.status_auto_default_mount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoBindMount, status.status_auto_bind_mount); - SET_BOOLEAN_FIELD(obj, cls, statusSusKstat, status.status_sus_kstat); - SET_BOOLEAN_FIELD(obj, cls, statusTryUmount, status.status_try_umount); - SET_BOOLEAN_FIELD(obj, cls, statusAutoTryUmountBind, status.status_auto_try_umount_bind); - SET_BOOLEAN_FIELD(obj, cls, statusSpoofUname, status.status_spoof_uname); - SET_BOOLEAN_FIELD(obj, cls, statusEnableLog, status.status_enable_log); - SET_BOOLEAN_FIELD(obj, cls, statusHideSymbols, status.status_hide_symbols); - SET_BOOLEAN_FIELD(obj, cls, statusSpoofCmdline, status.status_spoof_cmdline); - SET_BOOLEAN_FIELD(obj, cls, statusOpenRedirect, status.status_open_redirect); - SET_BOOLEAN_FIELD(obj, cls, statusMagicMount, status.status_magic_mount); - SET_BOOLEAN_FIELD(obj, cls, statusSusSu, status.status_sus_su); - - return obj; + char hook_type[32] = { 0 }; + get_hook_type((char *) &hook_type); + return GetEnvironment()->NewStringUTF(env, hook_type); } // dynamic manager NativeBridge(setDynamicManager, jboolean, jint size, jstring hash) { - if (!hash) { - LogDebug("setDynamicManager: hash is null"); - return false; - } + if (!hash) { + LogDebug("setDynamicManager: hash is null"); + return false; + } - const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr); - bool result = set_dynamic_manager((unsigned int)size, chash); - GetEnvironment()->ReleaseStringUTFChars(env, hash, chash); + const char* chash = GetEnvironment()->GetStringUTFChars(env, hash, nullptr); + bool result = set_dynamic_manager((unsigned int)size, chash); + GetEnvironment()->ReleaseStringUTFChars(env, hash, chash); - LogDebug("setDynamicManager: size=0x%x, result=%d", size, result); - return result; + LogDebug("setDynamicManager: size=0x%x, result=%d", size, result); + return result; } NativeBridgeNP(getDynamicManager, jobject) { - struct dynamic_manager_user_config config; - bool result = get_dynamic_manager(&config); + struct dynamic_manager_user_config config; + bool result = get_dynamic_manager(&config); - if (!result) { - LogDebug("getDynamicManager: failed to get dynamic manager config"); - return NULL; - } + if (!result) { + LogDebug("getDynamicManager: failed to get dynamic manager config"); + return NULL; + } - jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig"); - jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig"); + jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$DynamicManagerConfig"); + jclass cls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$DynamicManagerConfig"); - SET_INT_FIELD(obj, cls, size, (jint)config.size); - SET_STRING_FIELD(obj, cls, hash, config.hash); + SET_INT_FIELD(obj, cls, size, (jint)config.size); + SET_STRING_FIELD(obj, cls, hash, config.hash); - LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash); - return obj; + LogDebug("getDynamicManager: size=0x%x, hash=%.16s...", config.size, config.hash); + return obj; } NativeBridgeNP(clearDynamicManager, jboolean) { - bool result = clear_dynamic_manager(); - LogDebug("clearDynamicManager: result=%d", result); - return result; + bool result = clear_dynamic_manager(); + LogDebug("clearDynamicManager: result=%d", result); + return result; } // Get a list of active managers NativeBridgeNP(getManagersList, jobject) { - struct manager_list_info managerListInfo; - bool result = get_managers_list(&managerListInfo); + struct manager_list_info managerListInfo; + bool result = get_managers_list(&managerListInfo); - if (!result) { - LogDebug("getManagersList: failed to get active managers list"); - return NULL; - } + if (!result) { + LogDebug("getManagersList: failed to get active managers list"); + return NULL; + } - jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList"); - jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList"); + jobject obj = CREATE_JAVA_OBJECT("com/sukisu/ultra/Natives$ManagersList"); + jclass managerListCls = GetEnvironment()->FindClass(env, "com/sukisu/ultra/Natives$ManagersList"); - SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count); + SET_INT_FIELD(obj, managerListCls, count, (jint)managerListInfo.count); - jobject managersList = CREATE_ARRAYLIST(); + jobject managersList = CREATE_ARRAYLIST(); - for (int i = 0; i < managerListInfo.count; i++) { - jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS( - "com/sukisu/ultra/Natives$ManagerInfo", - "(II)V", - (jint)managerListInfo.managers[i].uid, - (jint)managerListInfo.managers[i].signature_index - ); - ADD_TO_LIST(managersList, managerInfo); - } + for (int i = 0; i < managerListInfo.count; i++) { + jobject managerInfo = CREATE_JAVA_OBJECT_WITH_PARAMS( + "com/sukisu/ultra/Natives$ManagerInfo", + "(II)V", + (jint)managerListInfo.managers[i].uid, + (jint)managerListInfo.managers[i].signature_index + ); + ADD_TO_LIST(managersList, managerInfo); + } - SET_OBJECT_FIELD(obj, managerListCls, managers, managersList); + SET_OBJECT_FIELD(obj, managerListCls, managers, managersList); - LogDebug("getManagersList: count=%d", managerListInfo.count); - return obj; + LogDebug("getManagersList: count=%d", managerListInfo.count); + return obj; } NativeBridge(verifyModuleSignature, jboolean, jstring modulePath) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) - if (!modulePath) { - LogDebug("verifyModuleSignature: modulePath is null"); - return false; - } + if (!modulePath) { + LogDebug("verifyModuleSignature: modulePath is null"); + return false; + } - const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr); - bool result = verify_module_signature(cModulePath); - GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath); + const char* cModulePath = GetEnvironment()->GetStringUTFChars(env, modulePath, nullptr); + bool result = verify_module_signature(cModulePath); + GetEnvironment()->ReleaseStringUTFChars(env, modulePath, cModulePath); - LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result); - return result; + LogDebug("verifyModuleSignature: path=%s, result=%d", cModulePath, result); + return result; #else - LogDebug("verifyModuleSignature: not supported on non-ARM architecture"); - return false; + LogDebug("verifyModuleSignature: not supported on non-ARM architecture"); + return false; #endif +} + +NativeBridgeNP(isUidScannerEnabled, jboolean) { + return is_uid_scanner_enabled(); +} + +NativeBridge(setUidScannerEnabled, jboolean, jboolean enabled) { + return set_uid_scanner_enabled(enabled); +} + +NativeBridgeNP(clearUidScannerEnvironment, jboolean) { + return clear_uid_scanner_environment(); } \ No newline at end of file diff --git a/manager/app/src/main/cpp/ksu.c b/manager/app/src/main/cpp/ksu.c index 7c5a047..f3d1c13 100644 --- a/manager/app/src/main/cpp/ksu.c +++ b/manager/app/src/main/cpp/ksu.c @@ -2,11 +2,14 @@ // Created by weishu on 2022/12/9. // -#include #include #include #include #include +#include +#include +#include +#include #include "prelude.h" #include "ksu.h" @@ -21,232 +24,367 @@ extern const char* zako_file_verrcidx2str(uint8_t index); #endif // __aarch64__ || _M_ARM64 || __arm__ || _M_ARM -#define KERNEL_SU_OPTION 0xDEADBEEF +static int fd = -1; -#define CMD_GRANT_ROOT 0 +static inline int scan_driver_fd() { + const char *kName = "[ksu_driver]"; + DIR *fd_dir = opendir("/proc/self/fd"); + if (!fd_dir) { + return -1; + } -#define CMD_BECOME_MANAGER 1 -#define CMD_GET_VERSION 2 -#define CMD_ALLOW_SU 3 -#define CMD_DENY_SU 4 -#define CMD_GET_SU_LIST 5 -#define CMD_GET_DENY_LIST 6 -#define CMD_CHECK_SAFEMODE 9 + int found = -1; + struct dirent *de; + char path[64]; + char target[PATH_MAX]; -#define CMD_GET_APP_PROFILE 10 -#define CMD_SET_APP_PROFILE 11 + while ((de = readdir(fd_dir)) != NULL) { + if (de->d_name[0] == '.') { + continue; + } -#define CMD_IS_UID_GRANTED_ROOT 12 -#define CMD_IS_UID_SHOULD_UMOUNT 13 -#define CMD_IS_SU_ENABLED 14 -#define CMD_ENABLE_SU 15 + char *endptr = nullptr; + long fd_long = strtol(de->d_name, &endptr, 10); + if (!de->d_name[0] || *endptr != '\0' || fd_long < 0 || fd_long > INT_MAX) { + continue; + } -#define CMD_GET_VERSION_FULL 0xC0FFEE1A + 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'; -#define CMD_ENABLE_KPM 100 -#define CMD_HOOK_TYPE 101 -#define CMD_GET_SUSFS_FEATURE_STATUS 102 -#define CMD_DYNAMIC_MANAGER 103 -#define CMD_GET_MANAGERS 104 + const char *base = strrchr(target, '/'); + base = base ? base + 1 : target; -#define DYNAMIC_MANAGER_OP_SET 0 -#define DYNAMIC_MANAGER_OP_GET 1 -#define DYNAMIC_MANAGER_OP_CLEAR 2 + if (strstr(base, kName)) { + found = (int)fd_long; + break; + } + } -static bool ksuctl(int cmd, void* arg1, void* arg2) { - int32_t result = 0; - int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); - - return result == KERNEL_SU_OPTION && rtn == -1; + closedir(fd_dir); + return found; } -bool become_manager(const char* pkg) { - char param[128]; - uid_t uid = getuid(); - uint32_t userId = uid / 100000; - if (userId == 0) { - sprintf(param, "/data/data/%s", pkg); - } else { - snprintf(param, sizeof(param), "/data/user/%d/%s", userId, pkg); +static int ksuctl(unsigned long op, void* arg) { + if (fd < 0) { + fd = scan_driver_fd(); + } + return ioctl(fd, op, arg); +} + +static struct ksu_get_info_cmd g_version = {0}; + +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_get_allow_list_cmd *cmd) { + if (ksuctl(KSU_IOCTL_GET_ALLOW_LIST, cmd) == 0) { + return true; } - return ksuctl(CMD_BECOME_MANAGER, param, NULL); -} - -// cache the result to avoid unnecessary syscall -static bool is_lkm; -int get_version() { - int32_t version = -1; - int32_t flags = 0; - ksuctl(CMD_GET_VERSION, &version, &flags); - if (!is_lkm && (flags & 0x1)) { - is_lkm = true; + // fallback to legacy + int size = 0; + int uids[1024]; + if (legacy_get_allow_list(uids, &size)) { + cmd->count = size; + memcpy(cmd->uids, uids, sizeof(int) * size); + return true; } - return version; -} -void get_full_version(char* buff) { - ksuctl(CMD_GET_VERSION_FULL, buff, NULL); -} - -bool get_allow_list(int *uids, int *size) { - return ksuctl(CMD_GET_SU_LIST, uids, size); + return false; } bool is_safe_mode() { - return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL); + struct ksu_check_safemode_cmd cmd = {}; + if (ksuctl(KSU_IOCTL_CHECK_SAFEMODE, &cmd) == 0) { + return cmd.in_safe_mode; + } + // fallback + return legacy_is_safe_mode(); } bool is_lkm_mode() { - // you should call get_version first! - return is_lkm; + auto info = get_info(); + if (info.version > 0) { + return (info.flags & 0x1) != 0; + } + // Legacy Compatible + return (legacy_get_info().flags & 0x1) != 0; +} + +bool is_manager() { + auto info = get_info(); + if (info.version > 0) { + return (info.flags & 0x2) != 0; + } + // Legacy Compatible + return legacy_get_info().version > 0; } bool uid_should_umount(int uid) { - int should; - return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should; + struct ksu_uid_should_umount_cmd cmd = {}; + cmd.uid = uid; + if (ksuctl(KSU_IOCTL_UID_SHOULD_UMOUNT, &cmd) == 0) { + return cmd.should_umount; + } + return legacy_uid_should_umount(uid); } -bool set_app_profile(const struct app_profile* profile) { - return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL); +bool set_app_profile(const struct app_profile *profile) { + struct ksu_set_app_profile_cmd cmd = {}; + cmd.profile = *profile; + if (ksuctl(KSU_IOCTL_SET_APP_PROFILE, &cmd) == 0) { + return true; + } + return legacy_set_app_profile(profile); } -bool get_app_profile(char* key, struct app_profile* profile) { - return ksuctl(CMD_GET_APP_PROFILE, profile, NULL); +int get_app_profile(struct app_profile *profile) { + struct ksu_get_app_profile_cmd cmd = {.profile = *profile}; + int ret = ksuctl(KSU_IOCTL_GET_APP_PROFILE, &cmd); + if (ret == 0) { + *profile = cmd.profile; + return 0; + } + return legacy_get_app_profile(profile->key, profile) ? 0 : -1; } bool set_su_enabled(bool enabled) { - return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL); + struct ksu_set_feature_cmd cmd = {}; + cmd.feature_id = KSU_FEATURE_SU_COMPAT; + cmd.value = enabled ? 1 : 0; + if (ksuctl(KSU_IOCTL_SET_FEATURE, &cmd) == 0) { + return true; + } + return legacy_set_su_enabled(enabled); } bool is_su_enabled() { - int enabled = true; - // if ksuctl failed, we assume su is enabled, and it cannot be disabled. - ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL); - return enabled; + struct ksu_get_feature_cmd cmd = {}; + cmd.feature_id = KSU_FEATURE_SU_COMPAT; + if (ksuctl(KSU_IOCTL_GET_FEATURE, &cmd) == 0 && cmd.supported) { + return cmd.value != 0; + } + return legacy_is_su_enabled(); } -bool is_KPM_enable() { - int enabled = false; - ksuctl(CMD_ENABLE_KPM, &enabled, NULL); - return enabled; -} - -bool get_hook_type(char* hook_type, size_t size) { - if (hook_type == NULL || size == 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; } - - static char cached_hook_type[16] = {0}; - if (cached_hook_type[0] == '\0') { - if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) { - strcpy(cached_hook_type, "Unknown"); - } - } - - strncpy(hook_type, cached_hook_type, size); - hook_type[size - 1] = '\0'; + if (out_value) *out_value = cmd.value; + if (out_supported) *out_supported = cmd.supported; return true; } -bool get_susfs_feature_status(struct susfs_feature_status* status) { - if (status == NULL) { - return false; - } - - return ksuctl(CMD_GET_SUSFS_FEATURE_STATUS, status, NULL); +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_dynamic_manager(unsigned int size, const char* hash) { - if (hash == NULL) { - return false; - } - - struct dynamic_manager_user_config config; - config.operation = DYNAMIC_MANAGER_OP_SET; - config.size = size; - strncpy(config.hash, hash, sizeof(config.hash) - 1); - config.hash[sizeof(config.hash) - 1] = '\0'; - - return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +bool set_kernel_umount_enabled(bool enabled) { + return set_feature(KSU_FEATURE_KERNEL_UMOUNT, enabled ? 1 : 0); } -bool get_dynamic_manager(struct dynamic_manager_user_config* config) { - if (config == NULL) { +bool is_kernel_umount_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_KERNEL_UMOUNT, &value, &supported)) { return false; } - - config->operation = DYNAMIC_MANAGER_OP_GET; - return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL); -} - -bool clear_dynamic_manager() { - struct dynamic_manager_user_config config; - config.operation = DYNAMIC_MANAGER_OP_CLEAR; - return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); -} - -bool get_managers_list(struct manager_list_info* info) { - if (info == NULL) { + if (!supported) { return false; } + return value != 0; +} - return ksuctl(CMD_GET_MANAGERS, info, NULL); +bool set_enhanced_security_enabled(bool enabled) { + return set_feature(KSU_FEATURE_ENHANCED_SECURITY, enabled ? 1 : 0); +} + +bool is_enhanced_security_enabled() { + uint64_t value = 0; + bool supported = false; + if (!get_feature(KSU_FEATURE_ENHANCED_SECURITY, &value, &supported)) { + return false; + } + if (!supported) { + return false; + } + return value != 0; +} + +void get_full_version(char* buff) { + struct ksu_get_full_version_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_GET_FULL_VERSION, &cmd) == 0) { + strncpy(buff, cmd.version_full, KSU_FULL_VERSION_STRING - 1); + buff[KSU_FULL_VERSION_STRING - 1] = '\0'; + } else { + return legacy_get_full_version(buff); + } +} + +bool is_KPM_enable(void) { + struct ksu_enable_kpm_cmd cmd = {}; + if (ksuctl(KSU_IOCTL_ENABLE_KPM, &cmd) == 0 && cmd.enabled) { + return true; + } + return legacy_is_KPM_enable(); +} + +void get_hook_type(char *buff) { + struct ksu_hook_type_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_HOOK_TYPE, &cmd) == 0) { + strncpy(buff, cmd.hook_type, 32 - 1); + buff[32 - 1] = '\0'; + } else { + legacy_get_hook_type(buff, 32); + } +} + +bool set_dynamic_manager(unsigned int size, const char *hash) +{ + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_SET; + cmd.config.size = size; + strlcpy(cmd.config.hash, hash, sizeof(cmd.config.hash)); + + return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0; +} + +bool get_dynamic_manager(struct dynamic_manager_user_config *cfg) +{ + if (!cfg) + return false; + + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_GET; + + if (ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) != 0) + return false; + + *cfg = cmd.config; + return true; +} + +bool clear_dynamic_manager(void) +{ + struct ksu_dynamic_manager_cmd cmd = {0}; + cmd.config.operation = DYNAMIC_MANAGER_OP_CLEAR; + return ksuctl(KSU_IOCTL_DYNAMIC_MANAGER, &cmd) == 0; +} + +bool get_managers_list(struct manager_list_info *info) +{ + if (!info) + return false; + struct ksu_get_managers_cmd cmd = {0}; + if (ksuctl(KSU_IOCTL_GET_MANAGERS, &cmd) != 0) + return false; + + *info = cmd.manager_info; + return true; +} + +bool is_uid_scanner_enabled(void) +{ + bool status = false; + + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_GET_STATUS, + .status_ptr = (__u64)(uintptr_t)&status + }; + + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd) == 0 != 0 && status; +} + +bool set_uid_scanner_enabled(bool enabled) +{ + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_TOGGLE, + .enabled = enabled + }; + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); +} + +bool clear_uid_scanner_environment(void) +{ + struct ksu_enable_uid_scanner_cmd cmd = { + .operation = UID_SCANNER_OP_CLEAR_ENV + }; + return ksuctl(KSU_IOCTL_ENABLE_UID_SCANNER, &cmd); } bool verify_module_signature(const char* input) { #if defined(__aarch64__) || defined(_M_ARM64) || defined(__arm__) || defined(_M_ARM) - if (input == NULL) { - LogDebug("verify_module_signature: input path is null"); - return false; - } + if (input == NULL) { + LogDebug("verify_module_signature: input path is null"); + return false; + } - int fd = zako_sys_file_open(input); - if (fd < 0) { - LogDebug("verify_module_signature: failed to open file: %s", input); - return false; - } + int file_fd = zako_sys_file_open(input); + if (file_fd < 0) { + LogDebug("verify_module_signature: failed to open file: %s", input); + return false; + } - uint32_t results = zako_file_verify_esig(fd, 0); + uint32_t results = zako_file_verify_esig(file_fd, 0); - if (results != 0) { - /* If important error occured, verification process should - be considered as failed due to unexpected modification - potentially happened. */ - if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) { - LogDebug("verify_module_signature: Verification failed! (important error)"); - } else { - /* This is for manager that doesn't want to do certificate checks */ - LogDebug("verify_module_signature: Verification partially passed"); - } - } else { - LogDebug("verify_module_signature: Verification passed!"); - goto exit; - } + if (results != 0) { + /* If important error occured, verification process should + be considered as failed due to unexpected modification + potentially happened. */ + if ((results & ZAKO_ESV_IMPORTANT_ERROR) != 0) { + LogDebug("verify_module_signature: Verification failed! (important error)"); + } else { + /* This is for manager that doesn't want to do certificate checks */ + LogDebug("verify_module_signature: Verification partially passed"); + } + } else { + LogDebug("verify_module_signature: Verification passed!"); + goto exit; + } - /* Go through all bit fields */ - for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) { - if ((results & (1 << i)) == 0) { - continue; - } + /* Go through all bit fields */ + for (size_t i = 0; i < sizeof(uint32_t) * 8; i++) { + if ((results & (1 << i)) == 0) { + continue; + } - /* Convert error bit field index into human readable string */ - const char* message = zako_file_verrcidx2str((uint8_t)i); - // Error message: message - if (message != NULL) { - LogDebug("verify_module_signature: Error bit %zu: %s", i, message); - } else { - LogDebug("verify_module_signature: Error bit %zu: Unknown error", i); - } - } + /* Convert error bit field index into human readable string */ + const char* message = zako_file_verrcidx2str((uint8_t)i); + // Error message: message + if (message != NULL) { + LogDebug("verify_module_signature: Error bit %zu: %s", i, message); + } else { + LogDebug("verify_module_signature: Error bit %zu: Unknown error", i); + } + } - exit: - close(fd); - LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s", - input, results, (results == 0) ? "true" : "false"); - return results == 0; + exit: + close(file_fd); + LogDebug("verify_module_signature: path=%s, results=0x%x, success=%s", + input, results, (results == 0) ? "true" : "false"); + return results == 0; #else - LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null"); - return false; + LogDebug("verify_module_signature: not supported on non-ARM architecture, path=%s", input ? input : "null"); + return false; #endif -} \ No newline at end of file +} diff --git a/manager/app/src/main/cpp/ksu.h b/manager/app/src/main/cpp/ksu.h index 8df709d..dd46e04 100644 --- a/manager/app/src/main/cpp/ksu.h +++ b/manager/app/src/main/cpp/ksu.h @@ -6,16 +6,15 @@ #define KERNELSU_KSU_H #include "prelude.h" -#include +#include #include +#include +#include +#include -bool become_manager(const char *); +#define KSU_FULL_VERSION_STRING 255 -void get_full_version(char* buff); - -int get_version(); - -bool get_allow_list(int *uids, int *size); +uint32_t get_version(); bool uid_should_umount(int uid); @@ -23,6 +22,10 @@ bool is_safe_mode(); bool is_lkm_mode(); +bool is_manager(); + +void get_full_version(char* buff); + #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. @@ -33,99 +36,80 @@ bool is_lkm_mode(); #define DYNAMIC_MANAGER_OP_GET 1 #define DYNAMIC_MANAGER_OP_CLEAR 2 +#define UID_SCANNER_OP_GET_STATUS 0 +#define UID_SCANNER_OP_TOGGLE 1 +#define UID_SCANNER_OP_CLEAR_ENV 2 + struct dynamic_manager_user_config { - unsigned int operation; - unsigned int size; - char hash[65]; + unsigned int operation; + unsigned int size; + char hash[65]; }; -// SUSFS Functional State Structures -struct susfs_feature_status { - bool status_sus_path; - bool status_sus_mount; - bool status_auto_default_mount; - bool status_auto_bind_mount; - bool status_sus_kstat; - bool status_try_umount; - bool status_auto_try_umount_bind; - bool status_spoof_uname; - bool status_enable_log; - bool status_hide_symbols; - bool status_spoof_cmdline; - bool status_open_redirect; - bool status_magic_mount; - bool status_sus_su; -}; struct root_profile { - int32_t uid; - int32_t gid; + int32_t uid; + int32_t gid; - int32_t groups_count; - int32_t groups[KSU_MAX_GROUPS]; + 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; + // 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]; + char selinux_domain[KSU_SELINUX_DOMAIN]; - int32_t namespaces; + int32_t namespaces; }; struct non_root_profile { - bool umount_modules; + 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; + // 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; + // 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]; + union { + struct { + bool use_default; + char template_name[KSU_MAX_PACKAGE_NAME]; - struct root_profile profile; - } rp_config; + struct root_profile profile; + } rp_config; - struct { - bool use_default; + struct { + bool use_default; - struct non_root_profile profile; - } nrp_config; - }; + struct non_root_profile profile; + } nrp_config; + }; }; struct manager_list_info { - int count; - struct { - uid_t uid; - int signature_index; - } managers[2]; + int count; + struct { + uid_t uid; + int signature_index; + } managers[2]; }; bool set_app_profile(const struct app_profile* profile); -bool get_app_profile(char* key, struct app_profile* profile); - -bool set_su_enabled(bool enabled); - -bool is_su_enabled(); +int get_app_profile(struct app_profile* profile); bool is_KPM_enable(); -bool get_hook_type(char* hook_type, size_t size); - -bool get_susfs_feature_status(struct susfs_feature_status* status); +void get_hook_type(char* hook_type); bool set_dynamic_manager(unsigned int size, const char* hash); @@ -137,4 +121,176 @@ bool get_managers_list(struct manager_list_info* info); bool verify_module_signature(const char* input); +bool is_uid_scanner_enabled(); + +bool set_uid_scanner_enabled(bool enabled); + +bool clear_uid_scanner_environment(); + +// Feature IDs +enum ksu_feature_id { + KSU_FEATURE_SU_COMPAT = 0, + KSU_FEATURE_KERNEL_UMOUNT = 1, + KSU_FEATURE_ENHANCED_SECURITY = 2, +}; + +// 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) +}; + +struct ksu_get_info_cmd { + uint32_t version; // Output: KERNEL_SU_VERSION + uint32_t flags; // Output: flags (bit 0: MODULE mode) + 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_get_allow_list_cmd { + uint32_t uids[128]; // Output: array of allowed/denied UIDs + uint32_t count; // Output: number of UIDs in array + uint8_t allow; // Input: true for allow list, false for deny list +}; + +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_uid_cmd { + uint32_t uid; // Output: manager UID +}; + +struct ksu_set_manager_uid_cmd { + uint32_t uid; // Input: new manager UID +}; + +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(); + +// Enhanced security +bool set_enhanced_security_enabled(bool enabled); + +bool is_enhanced_security_enabled(); + +// Other command structures +struct ksu_get_full_version_cmd { + char version_full[KSU_FULL_VERSION_STRING]; // Output: full version string +}; + +struct ksu_hook_type_cmd { + char hook_type[32]; // Output: hook type string +}; + +struct ksu_enable_kpm_cmd { + uint8_t enabled; // Output: true if KPM is enabled +}; + +struct ksu_dynamic_manager_cmd { + struct dynamic_manager_user_config config; // Input/Output: dynamic manager config +}; + +struct ksu_get_managers_cmd { + struct manager_list_info manager_info; // Output: manager list information +}; + +struct ksu_enable_uid_scanner_cmd { + uint32_t operation; // Input: operation type (UID_SCANNER_OP_GET_STATUS, UID_SCANNER_OP_TOGGLE, UID_SCANNER_OP_CLEAR_ENV) + uint32_t enabled; // Input: enable or disable (for UID_SCANNER_OP_TOGGLE) + uint64_t status_ptr; // Input: pointer to store status (for UID_SCANNER_OP_GET_STATUS) +}; + +// 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_GET_ALLOW_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 6, 0) +#define KSU_IOCTL_GET_DENY_LIST _IOC(_IOC_READ|_IOC_WRITE, 'K', 7, 0) +#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_UID _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) + +// Other IOCTL command definitions +#define KSU_IOCTL_GET_FULL_VERSION _IOC(_IOC_READ, 'K', 100, 0) +#define KSU_IOCTL_HOOK_TYPE _IOC(_IOC_READ, 'K', 101, 0) +#define KSU_IOCTL_ENABLE_KPM _IOC(_IOC_READ, 'K', 102, 0) +#define KSU_IOCTL_DYNAMIC_MANAGER _IOC(_IOC_READ|_IOC_WRITE, 'K', 103, 0) +#define KSU_IOCTL_GET_MANAGERS _IOC(_IOC_READ|_IOC_WRITE, 'K', 104, 0) +#define KSU_IOCTL_ENABLE_UID_SCANNER _IOC(_IOC_READ|_IOC_WRITE, 'K', 105, 0) + +bool get_allow_list(struct ksu_get_allow_list_cmd *); + +// Legacy Compatible +struct ksu_version_info legacy_get_info(); + +struct ksu_version_info { + int32_t version; + int32_t flags; +}; + +bool legacy_get_allow_list(int *uids, int *size); +bool legacy_is_safe_mode(); +bool legacy_uid_should_umount(int uid); +bool legacy_set_app_profile(const struct app_profile* profile); +bool legacy_get_app_profile(char* key, struct app_profile* profile); +bool legacy_set_su_enabled(bool enabled); +bool legacy_is_su_enabled(); +bool legacy_is_KPM_enable(); +bool legacy_get_hook_type(char* hook_type, size_t size); +void legacy_get_full_version(char* buff); +bool legacy_set_dynamic_manager(unsigned int size, const char* hash); +bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config); +bool legacy_clear_dynamic_manager(); +bool legacy_get_managers_list(struct manager_list_info* info); +bool legacy_is_uid_scanner_enabled(); +bool legacy_set_uid_scanner_enabled(bool enabled); +bool legacy_clear_uid_scanner_environment(); + #endif //KERNELSU_KSU_H \ No newline at end of file diff --git a/manager/app/src/main/cpp/legacy.c b/manager/app/src/main/cpp/legacy.c new file mode 100644 index 0000000..de72a32 --- /dev/null +++ b/manager/app/src/main/cpp/legacy.c @@ -0,0 +1,163 @@ +// +// Created by shirkneko on 2025/11/3. +// +// Legacy Compatible +#include +#include +#include +#include +#include +#include +#include +#include + +#include "prelude.h" +#include "ksu.h" + +#define KERNEL_SU_OPTION 0xDEADBEEF + +#define CMD_GRANT_ROOT 0 + +#define CMD_BECOME_MANAGER 1 +#define CMD_GET_VERSION 2 +#define CMD_ALLOW_SU 3 +#define CMD_DENY_SU 4 +#define CMD_GET_SU_LIST 5 +#define CMD_GET_DENY_LIST 6 +#define CMD_CHECK_SAFEMODE 9 + +#define CMD_GET_APP_PROFILE 10 +#define CMD_SET_APP_PROFILE 11 + +#define CMD_IS_UID_GRANTED_ROOT 12 +#define CMD_IS_UID_SHOULD_UMOUNT 13 +#define CMD_IS_SU_ENABLED 14 +#define CMD_ENABLE_SU 15 + +#define CMD_GET_VERSION_FULL 0xC0FFEE1A + +#define CMD_ENABLE_KPM 100 +#define CMD_HOOK_TYPE 101 +#define CMD_DYNAMIC_MANAGER 103 +#define CMD_GET_MANAGERS 104 +#define CMD_ENABLE_UID_SCANNER 105 + +static bool ksuctl(int cmd, void* arg1, void* arg2) { + int32_t result = 0; + int32_t rtn = prctl(KERNEL_SU_OPTION, cmd, arg1, arg2, &result); + return result == KERNEL_SU_OPTION && rtn == -1; +} + +struct ksu_version_info legacy_get_info() +{ + int32_t version = -1; + int32_t flags = 0; + ksuctl(CMD_GET_VERSION, &version, &flags); + return (struct ksu_version_info){version, flags}; +} + +bool legacy_get_allow_list(int *uids, int *size) { + return ksuctl(CMD_GET_SU_LIST, uids, size); +} + +bool legacy_is_safe_mode() { + return ksuctl(CMD_CHECK_SAFEMODE, NULL, NULL); +} + +bool legacy_uid_should_umount(int uid) { + int should; + return ksuctl(CMD_IS_UID_SHOULD_UMOUNT, (void*) ((size_t) uid), &should) && should; +} + +bool legacy_set_app_profile(const struct app_profile* profile) { + return ksuctl(CMD_SET_APP_PROFILE, (void*) profile, NULL); +} + +bool legacy_get_app_profile(char* key, struct app_profile* profile) { + return ksuctl(CMD_GET_APP_PROFILE, profile, NULL); +} + +bool legacy_set_su_enabled(bool enabled) { + return ksuctl(CMD_ENABLE_SU, (void*) enabled, NULL); +} + +bool legacy_is_su_enabled() { + int enabled = true; + // if ksuctl failed, we assume su is enabled, and it cannot be disabled. + ksuctl(CMD_IS_SU_ENABLED, &enabled, NULL); + return enabled; +} + +bool legacy_is_KPM_enable() { + int enabled = false; + ksuctl(CMD_ENABLE_KPM, &enabled, NULL); + return enabled; +} + +bool legacy_get_hook_type(char* hook_type, size_t size) { + if (hook_type == NULL || size == 0) { + return false; + } + + static char cached_hook_type[16] = {0}; + if (cached_hook_type[0] == '\0') { + if (!ksuctl(CMD_HOOK_TYPE, cached_hook_type, NULL)) { + strcpy(cached_hook_type, "Unknown"); + } + } + + strncpy(hook_type, cached_hook_type, size - 1); + hook_type[size - 1] = '\0'; + return true; +} + +void legacy_get_full_version(char* buff) { + ksuctl(CMD_GET_VERSION_FULL, buff, NULL); +} + +bool legacy_set_dynamic_manager(unsigned int size, const char* hash) { + if (hash == NULL) { + return false; + } + struct dynamic_manager_user_config config; + config.operation = DYNAMIC_MANAGER_OP_SET; + config.size = size; + strncpy(config.hash, hash, sizeof(config.hash) - 1); + config.hash[sizeof(config.hash) - 1] = '\0'; + return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +} + +bool legacy_get_dynamic_manager(struct dynamic_manager_user_config* config) { + if (config == NULL) { + return false; + } + config->operation = DYNAMIC_MANAGER_OP_GET; + return ksuctl(CMD_DYNAMIC_MANAGER, config, NULL); +} + +bool legacy_clear_dynamic_manager() { + struct dynamic_manager_user_config config; + config.operation = DYNAMIC_MANAGER_OP_CLEAR; + return ksuctl(CMD_DYNAMIC_MANAGER, &config, NULL); +} + +bool legacy_get_managers_list(struct manager_list_info* info) { + if (info == NULL) { + return false; + } + return ksuctl(CMD_GET_MANAGERS, info, NULL); +} + +bool legacy_is_uid_scanner_enabled() { + bool status = false; + ksuctl(CMD_ENABLE_UID_SCANNER, (void*)0, &status); + return status; +} + +bool legacy_set_uid_scanner_enabled(bool enabled) { + return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)1, (void*)enabled); +} + +bool legacy_clear_uid_scanner_environment() { + return ksuctl(CMD_ENABLE_UID_SCANNER, (void*)2, NULL); +} \ No newline at end of file diff --git a/manager/app/src/main/cpp/prelude.h b/manager/app/src/main/cpp/prelude.h index 780d841..18e19fa 100644 --- a/manager/app/src/main/cpp/prelude.h +++ b/manager/app/src/main/cpp/prelude.h @@ -14,51 +14,51 @@ // Macros to simplify field setup #define SET_BOOLEAN_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \ - GetEnvironment()->SetBooleanField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Z"); \ + GetEnvironment()->SetBooleanField(env, obj, field, value); \ } while(0) #define SET_INT_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \ - GetEnvironment()->SetIntField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "I"); \ + GetEnvironment()->SetIntField(env, obj, field, value); \ } while(0) #define SET_STRING_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \ - GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/lang/String;"); \ + GetEnvironment()->SetObjectField(env, obj, field, GetEnvironment()->NewStringUTF(env, value)); \ } while(0) #define SET_OBJECT_FIELD(obj, cls, fieldName, value) do { \ - jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \ - GetEnvironment()->SetObjectField(env, obj, field, value); \ + jfieldID field = GetEnvironment()->GetFieldID(env, cls, #fieldName, "Ljava/util/List;"); \ + GetEnvironment()->SetObjectField(env, obj, field, value); \ } while(0) // Macros for creating Java objects #define CREATE_JAVA_OBJECT(className) ({ \ - jclass cls = GetEnvironment()->FindClass(env, className); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); \ - GetEnvironment()->NewObject(env, cls, constructor); \ + jclass cls = GetEnvironment()->FindClass(env, className); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", "()V"); \ + GetEnvironment()->NewObject(env, cls, constructor); \ }) // Macros for creating ArrayList #define CREATE_ARRAYLIST() ({ \ - jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "", "()V"); \ - GetEnvironment()->NewObject(env, arrayListCls, constructor); \ + jclass arrayListCls = GetEnvironment()->FindClass(env, "java/util/ArrayList"); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, arrayListCls, "", "()V"); \ + GetEnvironment()->NewObject(env, arrayListCls, constructor); \ }) // Macros for adding elements to an ArrayList #define ADD_TO_LIST(list, item) do { \ - jclass cls = GetEnvironment()->GetObjectClass(env, list); \ - jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \ - GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \ + jclass cls = GetEnvironment()->GetObjectClass(env, list); \ + jmethodID addMethod = GetEnvironment()->GetMethodID(env, cls, "add", "(Ljava/lang/Object;)Z"); \ + GetEnvironment()->CallBooleanMethod(env, list, addMethod, item); \ } while(0) // Macros for creating Java objects with parameter constructors #define CREATE_JAVA_OBJECT_WITH_PARAMS(className, signature, ...) ({ \ - jclass cls = GetEnvironment()->FindClass(env, className); \ - jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", signature); \ - GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \ + jclass cls = GetEnvironment()->FindClass(env, className); \ + jmethodID constructor = GetEnvironment()->GetMethodID(env, cls, "", signature); \ + GetEnvironment()->NewObject(env, cls, constructor, __VA_ARGS__); \ }) #ifdef NDEBUG diff --git a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt index 73de13d..2f587d1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/KernelSUApplication.kt @@ -1,94 +1,40 @@ package com.sukisu.ultra -import android.annotation.SuppressLint -import android.app.Activity -import android.app.ActivityOptions import android.app.Application -import android.content.Context -import android.content.res.Configuration -import android.content.res.Resources -import android.os.Build -import android.os.Bundle +import android.system.Os +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore +import androidx.lifecycle.ViewModelStoreOwner +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import coil.Coil import coil.ImageLoader import com.dergoogler.mmrl.platform.Platform import me.zhanghai.android.appiconloader.coil.AppIconFetcher import me.zhanghai.android.appiconloader.coil.AppIconKeyer +import okhttp3.Cache +import okhttp3.OkHttpClient import java.io.File -import java.util.* +import java.util.Locale -@SuppressLint("StaticFieldLeak") lateinit var ksuApp: KernelSUApplication -class KernelSUApplication : Application() { - private var currentActivity: Activity? = null +class KernelSUApplication : Application(), ViewModelStoreOwner { - private val activityLifecycleCallbacks = object : ActivityLifecycleCallbacks { - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - currentActivity = activity - } - override fun onActivityStarted(activity: Activity) { - currentActivity = activity - } - override fun onActivityResumed(activity: Activity) { - currentActivity = activity - } - override fun onActivityPaused(activity: Activity) {} - override fun onActivityStopped(activity: Activity) {} - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityDestroyed(activity: Activity) { - if (currentActivity == activity) { - currentActivity = null - } - } - } - - override fun attachBaseContext(base: Context) { - val prefs = base.getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - var context = base - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val config = Configuration(base.resources.configuration) - config.setLocale(locale) - - context = base.createConfigurationContext(config) - } - - super.attachBaseContext(context) - } - - @SuppressLint("ObsoleteSdkInt") - override fun getResources(): Resources { - val resources = super.getResources() - val prefs = getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - val config = Configuration(resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - return createConfigurationContext(config).resources - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - - return resources - } + lateinit var okhttpClient: OkHttpClient + private val appViewModelStore by lazy { ViewModelStore() } override fun onCreate() { super.onCreate() ksuApp = this - // 注册Activity生命周期回调 - registerActivityLifecycleCallbacks(activityLifecycleCallbacks) + // For faster response when first entering superuser or webui activity + val superUserViewModel = ViewModelProvider(this)[SuperUserViewModel::class.java] + CoroutineScope(Dispatchers.Main).launch { + superUserViewModel.fetchAppList() + } Platform.setHiddenApiExemptions() @@ -107,45 +53,20 @@ class KernelSUApplication : Application() { 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", "SukiSU/${BuildConfig.VERSION_CODE}") + .header("Accept-Language", Locale.getDefault().toLanguageTag()).build() + ) + }.build() } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - applyLanguageSetting() - } - - @SuppressLint("ObsoleteSdkInt") - private fun applyLanguageSetting() { - val prefs = getSharedPreferences("settings", MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val resources = resources - val config = Configuration(resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - } - - // 添加刷新当前Activity的方法 - fun refreshCurrentActivity() { - currentActivity?.let { activity -> - val intent = activity.intent - activity.finish() - - val options = ActivityOptions.makeCustomAnimation( - activity, android.R.anim.fade_in, android.R.anim.fade_out - ) - activity.startActivity(intent, options.toBundle()) - } - } -} + override val viewModelStore: ViewModelStore + get() = appViewModelStore +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt index 222df27..2f8aa33 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/Natives.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/Natives.kt @@ -16,23 +16,22 @@ object Natives { // 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. - const val MINIMAL_SUPPORTED_KERNEL = 11071 - const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.5" - - // 11640: Support query working mode, LKM or GKI - // when MINIMAL_SUPPORTED_KERNEL > 11640, we can remove this constant. - const val MINIMAL_SUPPORTED_KERNEL_LKM = 11648 + // 12143: breaking: new supercall impl + const val MINIMAL_SUPPORTED_KERNEL = 12143 // 12040: Support disable sucompat mode - const val MINIMAL_SUPPORTED_SU_COMPAT = 12040 const val KERNEL_SU_DOMAIN = "u:r:su:s0" + const val MINIMAL_SUPPORTED_KERNEL_FULL = "v3.1.8" + const val MINIMAL_SUPPORTED_KPM = 12800 const val MINIMAL_SUPPORTED_DYNAMIC_MANAGER = 13215 const val MINIMAL_SUPPORTED_UID_SCANNER = 13347 + const val MINIMAL_NEW_IOCTL_KERNEL = 13490 + const val ROOT_UID = 0 const val ROOT_GID = 0 @@ -63,11 +62,9 @@ object Natives { init { System.loadLibrary("zakosign") - System.loadLibrary("zako") + System.loadLibrary("kernelsu") } - // become root manager, return true if success. - external fun becomeManager(pkg: String?): Boolean val version: Int external get @@ -81,6 +78,9 @@ object Natives { val isLkmMode: Boolean external get + val isManager: Boolean + external get + external fun uidShouldUmount(uid: Int): Boolean /** @@ -99,6 +99,25 @@ object Natives { */ 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 + + /** + * Enhanced security can be enabled/disabled. + * 0: disabled + * 1: enabled + * negative : error + */ + external fun isEnhancedSecurityEnabled(): Boolean + external fun setEnhancedSecurityEnabled(enabled: Boolean): Boolean + external fun isKPMEnabled(): Boolean external fun getHookType(): String @@ -106,7 +125,6 @@ object Natives { * Get SUSFS feature status from kernel * @return SusfsFeatureStatus object containing all feature states, or null if failed */ - external fun getSusfsFeatureStatus(): SusfsFeatureStatus? /** * Set dynamic managerature configuration @@ -138,6 +156,28 @@ object Natives { // 模块签名验证 external fun verifyModuleSignature(modulePath: String): Boolean + /** + * Check if UID scanner is currently enabled + * @return true if UID scanner is enabled, false otherwise + */ + external fun isUidScannerEnabled(): Boolean + + /** + * Enable or disable UID scanner + * @param enabled true to enable, false to disable + * @return true if operation was successful, false otherwise + */ + external fun setUidScannerEnabled(enabled: Boolean): Boolean + + /** + * Clear UID scanner environment (force exit) + * This will forcefully stop all UID scanner operations and clear the environment + * @return true if operation was successful, false otherwise + */ + external fun clearUidScannerEnvironment(): Boolean + + external fun getUserName(uid: Int): String? + private const val NON_ROOT_DEFAULT_PROFILE_KEY = "$" private const val NOBODY_UID = 9999 @@ -159,31 +199,10 @@ object Natives { } fun requireNewKernel(): Boolean { - if (version < MINIMAL_SUPPORTED_KERNEL) return true + if (version != -1 && version < MINIMAL_SUPPORTED_KERNEL) return true return isVersionLessThan(getFullVersion(), MINIMAL_SUPPORTED_KERNEL_FULL) } - @Immutable - @Parcelize - @Keep - data class SusfsFeatureStatus( - val statusSusPath: Boolean = false, - val statusSusMount: Boolean = false, - val statusAutoDefaultMount: Boolean = false, - val statusAutoBindMount: Boolean = false, - val statusSusKstat: Boolean = false, - val statusTryUmount: Boolean = false, - val statusAutoTryUmountBind: Boolean = false, - val statusSpoofUname: Boolean = false, - val statusEnableLog: Boolean = false, - val statusHideSymbols: Boolean = false, - val statusSpoofCmdline: Boolean = false, - val statusOpenRedirect: Boolean = false, - val statusMagicMount: Boolean = false, - val statusOverlayfsAutoKstat: Boolean = false, - val statusSusSu: Boolean = false - ) : Parcelable - @Immutable @Parcelize @Keep diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt index 41d0f43..39201e5 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/KsuService.kt @@ -1,137 +1,75 @@ package com.sukisu.ultra.ui +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageInfo import android.os.* import android.util.Log import com.topjohnwu.superuser.ipc.RootService -import rikka.parcelablelist.ParcelableListSlice -import java.lang.reflect.Method +import com.sukisu.zako.IKsuInterface /** * @author ShirkNeko - * @date 2025/7/2. + * @date 2025/10/17. */ class KsuService : RootService() { - companion object { - private const val TAG = "KsuService" - private const val DESCRIPTOR = "com.sukisu.ultra.IKsuInterface" - private const val TRANSACTION_GET_PACKAGES = IBinder.FIRST_CALL_TRANSACTION + 0 + private val TAG = "KsuService" + + private val cacheLock = Object() + private var _all: List? = null + private val allPackages: List + get() = synchronized(cacheLock) { + _all ?: loadAllPackages().also { _all = it } + } + + private fun loadAllPackages(): List { + val tmp = arrayListOf() + for (user in (getSystemService(USER_SERVICE) as UserManager).userProfiles) { + val userId = user.getUserIdCompat() + tmp += getInstalledPackagesAsUser(userId) + } + return tmp } - interface IKsuInterface : IInterface { - fun getPackages(flags: Int): ParcelableListSlice - } + internal inner class Stub : IKsuInterface.Stub() { + override fun getPackageCount(): Int = allPackages.size - abstract class Stub : Binder(), IKsuInterface { - init { - attachInterface(this, DESCRIPTOR) - } - - companion object { - fun asInterface(obj: IBinder?): IKsuInterface? { - if (obj == null) return null - val iin = obj.queryLocalInterface(DESCRIPTOR) - return if (iin != null && iin is IKsuInterface) { - iin - } else { - Proxy(obj) - } - } - } - - override fun asBinder(): IBinder = this - - override fun onTransact(code: Int, data: Parcel, reply: Parcel?, flags: Int): Boolean { - val descriptor = DESCRIPTOR - when (code) { - INTERFACE_TRANSACTION -> { - reply?.writeString(descriptor) - return true - } - TRANSACTION_GET_PACKAGES -> { - data.enforceInterface(descriptor) - val flagsArg = data.readInt() - val result = getPackages(flagsArg) - reply?.writeNoException() - reply?.writeInt(1) - result.writeToParcel(reply!!, Parcelable.PARCELABLE_WRITE_RETURN_VALUE) - return true - } - } - return super.onTransact(code, data, reply, flags) - } - - private class Proxy(private val mRemote: IBinder) : IKsuInterface { - override fun getPackages(flags: Int): ParcelableListSlice { - val data = Parcel.obtain() - val reply = Parcel.obtain() - return try { - data.writeInterfaceToken(DESCRIPTOR) - data.writeInt(flags) - mRemote.transact(TRANSACTION_GET_PACKAGES, data, reply, 0) - reply.readException() - if (reply.readInt() != 0) { - @Suppress("UNCHECKED_CAST") - ParcelableListSlice.CREATOR.createFromParcel(reply) as ParcelableListSlice - } else { - ParcelableListSlice(emptyList()) - } - } finally { - reply.recycle() - data.recycle() - } - } - - override fun asBinder(): IBinder = mRemote + override fun getPackages(start: Int, maxCount: Int): List { + val list = allPackages + val end = (start + maxCount).coerceAtMost(list.size) + return if (start >= list.size) emptyList() + else list.subList(start, end) } } - inner class KsuInterfaceImpl : Stub() { - override fun getPackages(flags: Int): ParcelableListSlice { - val list = getInstalledPackagesAll(flags) - Log.i(TAG, "getPackages: ${list.size}") - return ParcelableListSlice(list) - } - } + override fun onBind(intent: Intent): IBinder = Stub() - override fun onBind(intent: Intent): IBinder { - return KsuInterfaceImpl() - } - - private fun getUserIds(): List { - val result = mutableListOf() - val um = getSystemService(USER_SERVICE) as UserManager - val userProfiles = um.userProfiles - for (userProfile in userProfiles) { - result.add(userProfile.hashCode()) - } - return result - } - - private fun getInstalledPackagesAll(flags: Int): ArrayList { - val packages = ArrayList() - for (userId in getUserIds()) { - Log.i(TAG, "getInstalledPackagesAll: $userId") - packages.addAll(getInstalledPackagesAsUser(flags, userId)) - } - return packages - } - - private fun getInstalledPackagesAsUser(flags: Int, userId: Int): List { + @SuppressLint("PrivateApi") + private fun getInstalledPackagesAsUser(userId: Int): List { return try { val pm = packageManager - val getInstalledPackagesAsUser: Method = pm.javaClass.getDeclaredMethod( + val m = pm.javaClass.getDeclaredMethod( "getInstalledPackagesAsUser", Int::class.java, Int::class.java ) @Suppress("UNCHECKED_CAST") - getInstalledPackagesAsUser.invoke(pm, flags, userId) as List + m.invoke(pm, 0, userId) as List } catch (e: Throwable) { - Log.e(TAG, "err", e) - ArrayList() + Log.e(TAG, "getInstalledPackagesAsUser", e) + emptyList() + } + } + + private fun UserHandle.getUserIdCompat(): Int { + return try { + javaClass.getDeclaredField("identifier").apply { isAccessible = true }.getInt(this) + } catch (_: NoSuchFieldException) { + javaClass.getDeclaredMethod("getIdentifier").invoke(this) as Int + } catch (e: Throwable) { + Log.e("KsuService", "getUserIdCompat", e) + 0 } } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt index ca0b995..033ee44 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/MainActivity.kt @@ -1,7 +1,8 @@ package com.sukisu.ultra.ui import android.content.Context -import android.content.res.Configuration +import android.content.Intent +import android.net.Uri import android.os.Build import android.os.Bundle import androidx.activity.ComponentActivity @@ -9,13 +10,10 @@ import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.* import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.Scaffold -import androidx.compose.material3.SnackbarHostState -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember +import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.lifecycle.lifecycleScope import androidx.navigation.NavBackStackEntry @@ -26,6 +24,8 @@ import com.ramcosta.composedestinations.animations.NavHostAnimatedDestinationSty import com.ramcosta.composedestinations.generated.NavGraphs import com.ramcosta.composedestinations.generated.destinations.ExecuteModuleActionScreenDestination import com.ramcosta.composedestinations.spec.NavHostGraphSpec +import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper import com.sukisu.ultra.Natives import com.sukisu.ultra.ui.screen.BottomBarDestination import com.sukisu.ultra.ui.theme.KernelSUTheme @@ -34,11 +34,11 @@ import com.sukisu.ultra.ui.util.install import com.sukisu.ultra.ui.viewmodel.HomeViewModel import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.webui.initPlatform -import io.sukisu.ultra.UltraToolInstall +import com.sukisu.ultra.ui.component.* import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch -import zako.zako.zako.zakoui.activity.component.BottomBar -import zako.zako.zako.zakoui.activity.util.* +import com.sukisu.ultra.ui.activity.component.BottomBar +import com.sukisu.ultra.ui.activity.util.* class MainActivity : ComponentActivity() { private lateinit var superUserViewModel: SuperUserViewModel @@ -50,21 +50,18 @@ class MainActivity : ComponentActivity() { val showKpmInfo: Boolean = false ) - private lateinit var themeChangeObserver: ThemeChangeContentObserver + private var showConfirmationDialog = mutableStateOf(false) + private var pendingZipFiles = mutableStateOf>(emptyList()) - // 添加标记避免重复初始化 + private lateinit var themeChangeObserver: ThemeChangeContentObserver private var isInitialized = false - override fun attachBaseContext(newBase: Context) { - val context = LocaleUtils.applyLocale(newBase) - super.attachBaseContext(context) + override fun attachBaseContext(newBase: Context?) { + super.attachBaseContext(newBase?.let { LocaleHelper.applyLanguage(it) }) } override fun onCreate(savedInstanceState: Bundle?) { try { - // 确保应用正确的语言设置 - LocaleUtils.applyLanguageSetting(this) - // 应用自定义 DPI DisplayUtils.applyCustomDpi(this) @@ -77,6 +74,11 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) + val isManager = Natives.isManager + if (isManager && !Natives.requireNewKernel()) { + install() + } + // 使用标记控制初始化流程 if (!isInitialized) { initializeViewModels() @@ -84,6 +86,39 @@ class MainActivity : ComponentActivity() { isInitialized = true } + // Check if launched with a ZIP file + val zipUri: ArrayList? = when (intent?.action) { + Intent.ACTION_SEND -> { + val uri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableExtra(Intent.EXTRA_STREAM) + } + uri?.let { arrayListOf(it) } + } + + Intent.ACTION_SEND_MULTIPLE -> { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM, Uri::class.java) + } else { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM) + } + } + + else -> when { + intent?.data != null -> arrayListOf(intent.data!!) + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { + intent.getParcelableArrayListExtra("uris", Uri::class.java) + } + else -> { + @Suppress("DEPRECATION") + intent.getParcelableArrayListExtra("uris") + } + } + } + setContent { KernelSUTheme { val navController = rememberNavController() @@ -94,6 +129,38 @@ class MainActivity : ComponentActivity() { BottomBarDestination.entries.map { it.direction.route }.toSet() } + val navigator = navController.rememberDestinationsNavigator() + + InstallConfirmationDialog( + show = showConfirmationDialog.value, + zipFiles = pendingZipFiles.value, + onConfirm = { confirmedFiles -> + showConfirmationDialog.value = false + UltraActivityUtils.navigateToFlashScreen(this, confirmedFiles, navigator) + }, + onDismiss = { + showConfirmationDialog.value = false + pendingZipFiles.value = emptyList() + finish() + } + ) + + LaunchedEffect(zipUri) { + if (!zipUri.isNullOrEmpty()) { + // 检测 ZIP 文件类型并显示确认对话框 + lifecycleScope.launch { + UltraActivityUtils.detectZipTypeAndShowConfirmation(this@MainActivity, zipUri) { infos -> + if (infos.isNotEmpty()) { + pendingZipFiles.value = infos + showConfirmationDialog.value = true + } else { + finish() + } + } + } + } + } + val showBottomBar = when (currentDestination?.route) { ExecuteModuleActionScreenDestination.route -> false else -> true @@ -187,32 +254,17 @@ class MainActivity : ComponentActivity() { } } - lifecycleScope.launch { - try { - homeViewModel.initializeData() - } catch (e: Exception) { - e.printStackTrace() - } - } - // 数据刷新协程 DataRefreshUtils.startDataRefreshCoroutine(lifecycleScope) DataRefreshUtils.startSettingsMonitorCoroutine(lifecycleScope, this, settingsStateFlow) // 初始化主题相关设置 ThemeUtils.initializeThemeSettings(this, settingsStateFlow) - - val isManager = Natives.becomeManager(packageName) - if (isManager) { - install() - UltraToolInstall.tryToInstall() - } } override fun onResume() { try { super.onResume() - LocaleUtils.applyLanguageSetting(this) ThemeUtils.onActivityResume() // 仅在需要时刷新数据 @@ -228,7 +280,6 @@ class MainActivity : ComponentActivity() { lifecycleScope.launch { try { superUserViewModel.fetchAppList() - homeViewModel.initializeData() DataRefreshUtils.refreshData(lifecycleScope) } catch (e: Exception) { e.printStackTrace() @@ -253,13 +304,4 @@ class MainActivity : ComponentActivity() { e.printStackTrace() } } - - override fun onConfigurationChanged(newConfig: Configuration) { - try { - super.onConfigurationChanged(newConfig) - LocaleUtils.applyLanguageSetting(this) - } catch (e: Exception) { - e.printStackTrace() - } - } -} +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt similarity index 94% rename from manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt index 14e0c11..7efffad 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/component/BottomBar.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/component/BottomBar.kt @@ -1,4 +1,4 @@ -package zako.zako.zako.zakoui.activity.component +package com.sukisu.ultra.ui.activity.component import android.annotation.SuppressLint import androidx.compose.foundation.layout.* @@ -15,21 +15,20 @@ import com.ramcosta.composedestinations.spec.RouteOrDirection import com.ramcosta.composedestinations.utils.isRouteOnBackStackAsState import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator import com.sukisu.ultra.Natives -import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.MainActivity +import com.sukisu.ultra.ui.activity.util.* +import com.sukisu.ultra.ui.activity.util.AppData.getKpmVersionUse import com.sukisu.ultra.ui.screen.BottomBarDestination import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import zako.zako.zako.zakoui.activity.util.AppData -import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager -import zako.zako.zako.zakoui.activity.util.AppData.getKpmVersionUse +import com.sukisu.ultra.ui.util.* @SuppressLint("ContextCastToActivity") @OptIn(ExperimentalMaterial3Api::class) @Composable fun BottomBar(navController: NavHostController) { val navigator = navController.rememberDestinationsNavigator() - val isFullFeatured = AppData.isFullFeatured(ksuApp.packageName) + val isFullFeatured = AppData.isFullFeatured() val kpmVersion = getKpmVersionUse() val cardColor = MaterialTheme.colorScheme.surfaceContainer val activity = LocalContext.current as MainActivity @@ -40,9 +39,9 @@ fun BottomBar(navController: NavHostController) { val showKpmInfo = settings.showKpmInfo // 收集计数数据 - val superuserCount by DataRefreshManager.superuserCount.collectAsState() - val moduleCount by DataRefreshManager.moduleCount.collectAsState() - val kpmModuleCount by DataRefreshManager.kpmModuleCount.collectAsState() + val superuserCount by AppData.DataRefreshManager.superuserCount.collectAsState() + val moduleCount by AppData.DataRefreshManager.moduleCount.collectAsState() + val kpmModuleCount by AppData.DataRefreshManager.kpmModuleCount.collectAsState() NavigationBar( diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt similarity index 95% rename from manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt index 75ce7da..6786917 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/ThemeUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/ThemeUtils.kt @@ -1,8 +1,9 @@ -package zako.zako.zako.zakoui.activity.util +package com.sukisu.ultra.ui.activity.util import android.content.Context import android.database.ContentObserver import android.os.Handler +import android.provider.Settings import androidx.core.content.edit import com.sukisu.ultra.ui.MainActivity import com.sukisu.ultra.ui.theme.CardConfig @@ -56,7 +57,7 @@ object ThemeUtils { } activity.contentResolver.registerContentObserver( - android.provider.Settings.System.getUriFor("ui_night_mode"), + Settings.System.getUriFor("ui_night_mode"), false, contentObserver ) diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt new file mode 100644 index 0000000..367e791 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/activity/util/UltraActivityUtils.kt @@ -0,0 +1,236 @@ +package com.sukisu.ultra.ui.activity.util + +import android.content.Context +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.runtime.Composable +import androidx.lifecycle.LifecycleCoroutineScope +import com.sukisu.ultra.Natives +import com.sukisu.ultra.ui.MainActivity +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.isActive +import kotlinx.coroutines.launch +import java.util.* +import android.net.Uri +import androidx.lifecycle.lifecycleScope +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.ui.component.ZipFileDetector +import com.sukisu.ultra.ui.component.ZipFileInfo +import com.sukisu.ultra.ui.component.ZipType +import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.InstallScreenDestination +import com.sukisu.ultra.ui.screen.FlashIt +import kotlinx.coroutines.withContext +import androidx.core.content.edit + +object AnimatedBottomBar { + @Composable + fun AnimatedBottomBarWrapper( + showBottomBar: Boolean, + content: @Composable () -> Unit + ) { + AnimatedVisibility( + visible = showBottomBar, + enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), + exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() + ) { + content() + } + } +} + +object UltraActivityUtils { + + suspend fun detectZipTypeAndShowConfirmation( + activity: MainActivity, + zipUris: ArrayList, + onResult: (List) -> Unit + ) { + val infos = ZipFileDetector.detectAndParseZipFiles(activity, zipUris) + withContext(Dispatchers.Main) { onResult(infos) } + } + + fun navigateToFlashScreen( + activity: MainActivity, + zipFiles: List, + navigator: DestinationsNavigator + ) { + activity.lifecycleScope.launch { + val moduleUris = zipFiles.filter { it.type == ZipType.MODULE }.map { it.uri } + val kernelUris = zipFiles.filter { it.type == ZipType.KERNEL }.map { it.uri } + + when { + kernelUris.isNotEmpty() && moduleUris.isEmpty() -> { + if (kernelUris.size == 1 && rootAvailable()) { + navigator.navigate( + InstallScreenDestination( + preselectedKernelUri = kernelUris.first().toString() + ) + ) + } + setAutoExitAfterFlash(activity) + } + + moduleUris.isNotEmpty() -> { + navigator.navigate( + FlashScreenDestination( + FlashIt.FlashModules(ArrayList(moduleUris)) + ) + ) + setAutoExitAfterFlash(activity) + } + } + } + } + + private fun setAutoExitAfterFlash(activity: Context) { + activity.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + .edit { + putBoolean("auto_exit_after_flash", true) + } + } +} + +object AppData { + object DataRefreshManager { + // 私有状态流 + private val _superuserCount = MutableStateFlow(0) + private val _moduleCount = MutableStateFlow(0) + private val _kpmModuleCount = MutableStateFlow(0) + + // 公开的只读状态流 + val superuserCount: StateFlow = _superuserCount.asStateFlow() + val moduleCount: StateFlow = _moduleCount.asStateFlow() + val kpmModuleCount: StateFlow = _kpmModuleCount.asStateFlow() + + /** + * 刷新所有数据计数 + */ + fun refreshData() { + _superuserCount.value = getSuperuserCountUse() + _moduleCount.value = getModuleCountUse() + _kpmModuleCount.value = getKpmModuleCountUse() + } + } + + /** + * 获取超级用户应用计数 + */ + fun getSuperuserCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + getSuperuserCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取模块计数 + */ + fun getModuleCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + getModuleCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取KPM模块计数 + */ + fun getKpmModuleCountUse(): Int { + return try { + if (!rootAvailable()) return 0 + val kpmVersion = getKpmVersionUse() + if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0 + getKpmModuleCount() + } catch (_: Exception) { + 0 + } + } + + /** + * 获取KPM版本 + */ + fun getKpmVersionUse(): String { + return try { + if (!rootAvailable()) return "" + val version = getKpmVersion() + version.ifEmpty { "" } + } catch (e: Exception) { + "Error: ${e.message}" + } + } + + /** + * 检查是否是完整功能模式 + */ + fun isFullFeatured(): Boolean { + val isManager = Natives.isManager + return isManager && !Natives.requireNewKernel() && rootAvailable() + } +} + +object DataRefreshUtils { + fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) { + scope.launch(Dispatchers.IO) { + while (isActive) { + AppData.DataRefreshManager.refreshData() + delay(5000) + } + } + } + + fun startSettingsMonitorCoroutine( + scope: LifecycleCoroutineScope, + activity: MainActivity, + settingsStateFlow: MutableStateFlow + ) { + scope.launch(Dispatchers.IO) { + while (isActive) { + val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) + settingsStateFlow.value = MainActivity.SettingsState( + isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), + showKpmInfo = prefs.getBoolean("show_kpm_info", false) + ) + delay(1000) + } + } + } + + fun refreshData(scope: LifecycleCoroutineScope) { + scope.launch { + AppData.DataRefreshManager.refreshData() + } + } +} + +object DisplayUtils { + fun applyCustomDpi(context: Context) { + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val customDpi = prefs.getInt("app_dpi", 0) + + if (customDpi > 0) { + try { + val resources = context.resources + val metrics = resources.displayMetrics + metrics.density = customDpi / 160f + @Suppress("DEPRECATION") + metrics.scaledDensity = customDpi / 160f + metrics.densityDpi = customDpi + } catch (e: Exception) { + e.printStackTrace() + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt index f398c1b..10c0477 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/Dialog.kt @@ -8,11 +8,16 @@ import android.text.method.LinkMovementMethod import android.util.Log import android.view.ViewGroup import android.widget.TextView +import androidx.compose.foundation.gestures.ScrollableDefaults import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.Saver @@ -428,27 +433,36 @@ private fun ConfirmDialog(visuals: ConfirmDialogVisuals, confirm: () -> Unit, di @Composable private fun MarkdownContent(content: String) { val contentColor = LocalContentColor.current + val scrollState = rememberScrollState() - AndroidView( - factory = { context -> - TextView(context).apply { - movementMethod = LinkMovementMethod.getInstance() - setSpannableFactory(NoCopySpannableFactory.getInstance()) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE - } - hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - }, + Column( modifier = Modifier .fillMaxWidth() - .wrapContentHeight(), - update = { - Markwon.create(it.context).setMarkdown(it, content) - it.setTextColor(contentColor.toArgb()) - } - ) -} \ No newline at end of file + .verticalScroll( + state = scrollState, + flingBehavior = ScrollableDefaults.flingBehavior() + ) + .padding(12.dp) + ) { + AndroidView( + factory = { context -> + TextView(context).apply { + movementMethod = LinkMovementMethod.getInstance() + setSpannableFactory(NoCopySpannableFactory.getInstance()) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + breakStrategy = LineBreaker.BREAK_STRATEGY_SIMPLE + } + hyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + }, + update = { + Markwon.create(it.context).setMarkdown(it, content) + it.setTextColor(contentColor.toArgb()) + } + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt deleted file mode 100644 index 4dd8b2e..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/ImageEditorDialog.kt +++ /dev/null @@ -1,223 +0,0 @@ -package com.sukisu.ultra.ui.component - -import android.net.Uri -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.background -import androidx.compose.foundation.gestures.detectTransformGestures -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Fullscreen -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.geometry.Size -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.input.pointer.pointerInput -import androidx.compose.ui.layout.ContentScale -import androidx.compose.ui.layout.onSizeChanged -import androidx.compose.ui.platform.LocalContext -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 coil.compose.AsyncImage -import coil.request.ImageRequest -import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground -import kotlinx.coroutines.launch -import kotlin.math.max - -@Composable -fun ImageEditorDialog( - imageUri: Uri, - onDismiss: () -> Unit, - onConfirm: (Uri) -> Unit -) { - var scale by remember { mutableFloatStateOf(1f) } - var offsetX by remember { mutableFloatStateOf(0f) } - var offsetY by remember { mutableFloatStateOf(0f) } - val context = LocalContext.current - val scope = rememberCoroutineScope() - var lastScale by remember { mutableFloatStateOf(1f) } - var lastOffsetX by remember { mutableFloatStateOf(0f) } - var lastOffsetY by remember { mutableFloatStateOf(0f) } - var imageSize by remember { mutableStateOf(Size.Zero) } - var screenSize by remember { mutableStateOf(Size.Zero) } - val animatedScale by animateFloatAsState( - targetValue = scale, - label = "ScaleAnimation" - ) - val animatedOffsetX by animateFloatAsState( - targetValue = offsetX, - label = "OffsetXAnimation" - ) - val animatedOffsetY by animateFloatAsState( - targetValue = offsetY, - label = "OffsetYAnimation" - ) - val updateTransformation = remember { - { newScale: Float, newOffsetX: Float, newOffsetY: Float -> - val scaleDiff = kotlin.math.abs(newScale - lastScale) - val offsetXDiff = kotlin.math.abs(newOffsetX - lastOffsetX) - val offsetYDiff = kotlin.math.abs(newOffsetY - lastOffsetY) - if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { - scale = newScale - offsetX = newOffsetX - offsetY = newOffsetY - lastScale = newScale - lastOffsetX = newOffsetX - lastOffsetY = newOffsetY - } - } - } - val scaleToFullScreen = remember { - { - if (imageSize.height > 0 && screenSize.height > 0) { - val newScale = screenSize.height / imageSize.height - updateTransformation(newScale, 0f, 0f) - } - } - } - - Dialog( - onDismissRequest = onDismiss, - properties = DialogProperties( - dismissOnBackPress = true, - dismissOnClickOutside = false, - usePlatformDefaultWidth = false - ) - ) { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Black.copy(alpha = 0.9f)) - .onSizeChanged { size -> - screenSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) { - AsyncImage( - model = ImageRequest.Builder(LocalContext.current) - .data(imageUri) - .crossfade(true) - .build(), - contentDescription = stringResource(R.string.settings_custom_background), - contentScale = ContentScale.Fit, - modifier = Modifier - .fillMaxSize() - .graphicsLayer( - scaleX = animatedScale, - scaleY = animatedScale, - translationX = animatedOffsetX, - translationY = animatedOffsetY - ) - .pointerInput(Unit) { - detectTransformGestures { _, pan, zoom, _ -> - scope.launch { - try { - val newScale = (scale * zoom).coerceIn(0.5f, 3f) - val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) - val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) - val newOffsetX = if (maxOffsetX > 0) { - (offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) - } else { - 0f - } - val newOffsetY = if (maxOffsetY > 0) { - (offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) - } else { - 0f - } - updateTransformation(newScale, newOffsetX, newOffsetY) - } catch (_: Exception) { - updateTransformation(lastScale, lastOffsetX, lastOffsetY) - } - } - } - } - .onSizeChanged { size -> - imageSize = Size(size.width.toFloat(), size.height.toFloat()) - } - ) - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .align(Alignment.TopCenter), - horizontalArrangement = Arrangement.SpaceBetween - ) { - IconButton( - onClick = onDismiss, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.cancel), - tint = Color.White - ) - } - IconButton( - onClick = { scaleToFullScreen() }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Fullscreen, - contentDescription = stringResource(R.string.reprovision), - tint = Color.White - ) - } - IconButton( - onClick = { - scope.launch { - try { - val transformation = BackgroundTransformation(scale, offsetX, offsetY) - val savedUri = context.saveTransformedBackground(imageUri, transformation) - savedUri?.let { onConfirm(it) } - } catch (_: Exception) { - "" - } - } - }, - modifier = Modifier - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = stringResource(R.string.confirm), - tint = Color.White - ) - } - } - - Box( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) - .clip(RoundedCornerShape(8.dp)) - .background(Color.Black.copy(alpha = 0.6f)) - .padding(16.dp) - .align(Alignment.BottomCenter) - ) { - Text( - text = stringResource(id = R.string.image_editor_hint), - color = Color.White, - style = MaterialTheme.typography.bodyMedium - ) - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt new file mode 100644 index 0000000..6ae6a47 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/InstallConfirmationDialog.kt @@ -0,0 +1,441 @@ +package com.sukisu.ultra.ui.component + +import android.content.Context +import android.net.Uri +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.filled.Extension +import androidx.compose.material.icons.filled.GetApp +import androidx.compose.material.icons.filled.Memory +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import com.sukisu.ultra.R +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.IOException +import java.io.InputStreamReader +import java.util.zip.ZipInputStream + +enum class ZipType { + MODULE, + KERNEL, + UNKNOWN +} + +data class ZipFileInfo( + val uri: Uri, + val type: ZipType, + val name: String = "", + val version: String = "", + val versionCode: String = "", + val author: String = "", + val description: String = "", + val kernelVersion: String = "", + val supported: String = "" +) + +object ZipFileDetector { + + fun detectZipType(context: Context, uri: Uri): ZipType { + return try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var hasModuleProp = false + var hasToolsFolder = false + var hasAnykernelSh = false + + var entry = zipStream.nextEntry + while (entry != null) { + val entryName = entry.name.lowercase() + + when { + entryName == "module.prop" || entryName.endsWith("/module.prop") -> { + hasModuleProp = true + } + entryName.startsWith("tools/") || entryName == "tools" -> { + hasToolsFolder = true + } + entryName == "anykernel.sh" || entryName.endsWith("/anykernel.sh") -> { + hasAnykernelSh = true + } + } + + zipStream.closeEntry() + entry = zipStream.nextEntry + } + + when { + hasModuleProp -> ZipType.MODULE + hasToolsFolder && hasAnykernelSh -> ZipType.KERNEL + else -> ZipType.UNKNOWN + } + } + } ?: ZipType.UNKNOWN + } catch (e: IOException) { + e.printStackTrace() + ZipType.UNKNOWN + } + } + + fun parseModuleInfo(context: Context, uri: Uri): ZipFileInfo { + var zipInfo = ZipFileInfo(uri = uri, type = ZipType.MODULE) + + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var entry = zipStream.nextEntry + while (entry != null) { + if (entry.name.lowercase() == "module.prop" || entry.name.endsWith("/module.prop")) { + val reader = BufferedReader(InputStreamReader(zipStream)) + val props = mutableMapOf() + + var line = reader.readLine() + while (line != null) { + if (line.contains("=") && !line.startsWith("#")) { + val parts = line.split("=", limit = 2) + if (parts.size == 2) { + props[parts[0].trim()] = parts[1].trim() + } + } + line = reader.readLine() + } + + zipInfo = zipInfo.copy( + name = props["name"] ?: context.getString(R.string.unknown_module), + version = props["version"] ?: "", + versionCode = props["versionCode"] ?: "", + author = props["author"] ?: "", + description = props["description"] ?: "" + ) + break + } + zipStream.closeEntry() + entry = zipStream.nextEntry + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return zipInfo + } + + fun parseKernelInfo(context: Context, uri: Uri): ZipFileInfo { + var zipInfo = ZipFileInfo(uri = uri, type = ZipType.KERNEL) + + try { + context.contentResolver.openInputStream(uri)?.use { inputStream -> + ZipInputStream(inputStream).use { zipStream -> + var entry = zipStream.nextEntry + while (entry != null) { + if (entry.name.lowercase() == "anykernel.sh" || entry.name.endsWith("/anykernel.sh")) { + val reader = BufferedReader(InputStreamReader(zipStream)) + val props = mutableMapOf() + + var inPropertiesBlock = false + var line = reader.readLine() + while (line != null) { + if (line.contains("properties()")) { + inPropertiesBlock = true + } else if (inPropertiesBlock && line.contains("'; }")) { + inPropertiesBlock = false + } else if (inPropertiesBlock) { + val propertyLine = line.trim() + if (propertyLine.contains("=") && !propertyLine.startsWith("#")) { + val parts = propertyLine.split("=", limit = 2) + if (parts.size == 2) { + val key = parts[0].trim() + val value = parts[1].trim().removeSurrounding("'").removeSurrounding("\"") + when (key) { + "kernel.string" -> props["name"] = value + "supported.versions" -> props["supported"] = value + } + } + } + } + + // 解析普通变量定义 + if (line.contains("kernel.string=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.string=").trim().removeSurrounding("\"") + props["name"] = value + } + if (line.contains("supported.versions=") && !inPropertiesBlock) { + val value = line.substringAfter("supported.versions=").trim().removeSurrounding("\"") + props["supported"] = value + } + if (line.contains("kernel.version=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.version=").trim().removeSurrounding("\"") + props["version"] = value + } + if (line.contains("kernel.author=") && !inPropertiesBlock) { + val value = line.substringAfter("kernel.author=").trim().removeSurrounding("\"") + props["author"] = value + } + + line = reader.readLine() + } + + zipInfo = zipInfo.copy( + name = props["name"] ?: context.getString(R.string.unknown_kernel), + version = props["version"] ?: "", + author = props["author"] ?: "", + supported = props["supported"] ?: "", + kernelVersion = props["version"] ?: "" + ) + break + } + zipStream.closeEntry() + entry = zipStream.nextEntry + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } + + return zipInfo + } + + suspend fun detectAndParseZipFiles(context: Context, zipUris: List): List { + return withContext(Dispatchers.IO) { + val zipFileInfos = mutableListOf() + + for (uri in zipUris) { + val zipType = detectZipType(context, uri) + val zipInfo = when (zipType) { + ZipType.MODULE -> parseModuleInfo(context, uri) + ZipType.KERNEL -> parseKernelInfo(context, uri) + ZipType.UNKNOWN -> ZipFileInfo( + uri = uri, + type = ZipType.UNKNOWN, + name = context.getString(R.string.unknown_file) + ) + } + zipFileInfos.add(zipInfo) + } + + zipFileInfos.filter { it.type != ZipType.UNKNOWN } + } + } +} + +@Composable +fun InstallConfirmationDialog( + show: Boolean, + zipFiles: List, + onConfirm: (List) -> Unit, + onDismiss: () -> Unit +) { + if (show && zipFiles.isNotEmpty()) { + val context = LocalContext.current + + AlertDialog( + onDismissRequest = onDismiss, + title = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = if (zipFiles.any { it.type == ZipType.KERNEL }) + Icons.Default.Memory else Icons.Default.Extension, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + Text( + text = if (zipFiles.size == 1) { + context.getString(R.string.confirm_installation) + } else { + context.getString(R.string.confirm_multiple_installation, zipFiles.size) + }, + style = MaterialTheme.typography.headlineSmall + ) + } + }, + text = { + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = 400.dp), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + items(zipFiles.size) { index -> + val zipFile = zipFiles[index] + InstallItemCard(zipFile = zipFile) + } + } + }, + confirmButton = { + Button( + onClick = { onConfirm(zipFiles) }, + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary + ) + ) { + Icon( + imageVector = Icons.Default.GetApp, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(context.getString(R.string.install_confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text( + context.getString(android.R.string.cancel), + color = MaterialTheme.colorScheme.onSurface + ) + } + }, + modifier = Modifier.widthIn(min = 320.dp, max = 560.dp) + ) + } +} + +@Composable +fun InstallItemCard(zipFile: ZipFileInfo) { + val context = LocalContext.current + + ElevatedCard( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.elevatedCardColors( + containerColor = when (zipFile.type) { + ZipType.MODULE -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ZipType.KERNEL -> MaterialTheme.colorScheme.tertiaryContainer.copy(alpha = 0.3f) + else -> MaterialTheme.colorScheme.surfaceVariant + } + ), + elevation = CardDefaults.elevatedCardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = when (zipFile.type) { + ZipType.MODULE -> Icons.Default.Extension + ZipType.KERNEL -> Icons.Default.Memory + else -> Icons.AutoMirrored.Filled.Help + }, + contentDescription = null, + tint = when (zipFile.type) { + ZipType.MODULE -> MaterialTheme.colorScheme.primary + ZipType.KERNEL -> MaterialTheme.colorScheme.tertiary + else -> MaterialTheme.colorScheme.onSurfaceVariant + }, + modifier = Modifier.size(20.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Column(modifier = Modifier.weight(1f)) { + Text( + text = zipFile.name.ifEmpty { + when (zipFile.type) { + ZipType.MODULE -> context.getString(R.string.unknown_module) + ZipType.KERNEL -> context.getString(R.string.unknown_kernel) + else -> context.getString(R.string.unknown_file) + } + }, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + Text( + text = when (zipFile.type) { + ZipType.MODULE -> context.getString(R.string.module_package) + ZipType.KERNEL -> context.getString(R.string.kernel_package) + else -> context.getString(R.string.unknown_package) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + + // 详细信息 + if (zipFile.version.isNotEmpty() || zipFile.author.isNotEmpty() || + zipFile.description.isNotEmpty() || zipFile.supported.isNotEmpty()) { + + Spacer(modifier = Modifier.height(12.dp)) + HorizontalDivider( + color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), + thickness = 0.5.dp + ) + Spacer(modifier = Modifier.height(8.dp)) + + // 版本信息 + if (zipFile.version.isNotEmpty()) { + InfoRow( + label = context.getString(R.string.version), + value = zipFile.version + if (zipFile.versionCode.isNotEmpty()) " (${zipFile.versionCode})" else "" + ) + } + + // 作者信息 + if (zipFile.author.isNotEmpty()) { + InfoRow( + label = context.getString(R.string.author), + value = zipFile.author + ) + } + + // 描述信息 (仅模块) + if (zipFile.description.isNotEmpty() && zipFile.type == ZipType.MODULE) { + InfoRow( + label = context.getString(R.string.description), + value = zipFile.description + ) + } + + // 支持设备 (仅内核) + if (zipFile.supported.isNotEmpty() && zipFile.type == ZipType.KERNEL) { + InfoRow( + label = context.getString(R.string.supported_devices), + value = zipFile.supported + ) + } + } + } + } +} + +@Composable +fun InfoRow(label: String, value: String) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 2.dp), + verticalAlignment = Alignment.Top + ) { + Text( + text = "$label:", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.widthIn(min = 60.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = value, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.weight(1f) + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt index 5adf608..eb3c5db 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/KsuIsValidCheck.kt @@ -8,7 +8,7 @@ import com.sukisu.ultra.ksuApp fun KsuIsValid( content: @Composable () -> Unit ) { - val isManager = Natives.becomeManager(ksuApp.packageName) + val isManager = Natives.isManager val ksuVersion = if (isManager) Natives.version else null if (ksuVersion != null) { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt new file mode 100644 index 0000000..9103e55 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuperDropdown.kt @@ -0,0 +1,250 @@ +package com.sukisu.ultra.ui.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* +import androidx.compose.runtime.* +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.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.dp + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SuperDropdown( + items: List, + selectedIndex: Int, + title: String, + summary: String? = null, + icon: ImageVector? = null, + enabled: Boolean = true, + showValue: Boolean = true, + maxHeight: Dp? = 400.dp, + colors: SuperDropdownColors = SuperDropdownDefaults.colors(), + leftAction: (@Composable () -> Unit)? = null, + onSelectedIndexChange: (Int) -> Unit +) { + var showDialog by remember { mutableStateOf(false) } + val selectedItemText = items.getOrNull(selectedIndex) ?: "" + val itemsNotEmpty = items.isNotEmpty() + val actualEnabled = enabled && itemsNotEmpty + + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(enabled = actualEnabled) { showDialog = true } + .padding(horizontal = 16.dp, vertical = 12.dp), + verticalAlignment = Alignment.Top + ) { + if (leftAction != null) { + leftAction() + } else if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (actualEnabled) colors.iconColor else colors.disabledIconColor, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + } + + Column(modifier = Modifier.weight(1f)) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + color = if (actualEnabled) colors.titleColor else colors.disabledTitleColor + ) + + if (summary != null) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.summaryColor else colors.disabledSummaryColor + ) + } + + if (showValue && itemsNotEmpty) { + Spacer(modifier = Modifier.height(3.dp)) + Text( + text = selectedItemText, + style = MaterialTheme.typography.bodyMedium, + color = if (actualEnabled) colors.valueColor else colors.disabledValueColor, + maxLines = 2, + overflow = TextOverflow.Ellipsis + ) + } + } + + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowForward, + contentDescription = null, + tint = if (actualEnabled) colors.arrowColor else colors.disabledArrowColor, + modifier = Modifier.size(24.dp) + ) + } + + if (showDialog && itemsNotEmpty) { + AlertDialog( + onDismissRequest = { showDialog = false }, + title = { + Text( + text = title, + style = MaterialTheme.typography.headlineSmall + ) + }, + text = { + val dialogMaxHeight = maxHeight ?: 400.dp + LazyColumn( + modifier = Modifier + .fillMaxWidth() + .heightIn(max = dialogMaxHeight), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(items.size) { index -> + DropdownItem( + text = items[index], + isSelected = selectedIndex == index, + colors = colors, + onClick = { + onSelectedIndexChange(index) + showDialog = false + } + ) + } + } + }, + confirmButton = { + TextButton(onClick = { showDialog = false }) { + Text(text = stringResource(id = android.R.string.cancel)) + } + }, + containerColor = colors.dialogBackgroundColor, + shape = MaterialTheme.shapes.extraLarge, + tonalElevation = 4.dp + ) + } +} + +@Composable +private fun DropdownItem( + text: String, + isSelected: Boolean, + colors: SuperDropdownColors, + onClick: () -> Unit +) { + val backgroundColor = if (isSelected) { + colors.selectedBackgroundColor + } else { + Color.Transparent + } + + val contentColor = if (isSelected) { + colors.selectedContentColor + } else { + colors.contentColor + } + + Row( + modifier = Modifier + .fillMaxWidth() + .clip(MaterialTheme.shapes.medium) + .background(backgroundColor) + .clickable(onClick = onClick) + .padding(vertical = 12.dp, horizontal = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = isSelected, + onClick = null, + colors = RadioButtonDefaults.colors( + selectedColor = colors.selectedContentColor, + unselectedColor = colors.contentColor + ) + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = text, + style = MaterialTheme.typography.bodyLarge, + color = contentColor, + modifier = Modifier.weight(1f) + ) + + if (isSelected) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = null, + tint = colors.selectedContentColor, + modifier = Modifier.size(20.dp) + ) + } + } +} + +@Immutable +data class SuperDropdownColors( + val titleColor: Color, + val summaryColor: Color, + val valueColor: Color, + val iconColor: Color, + val arrowColor: Color, + val disabledTitleColor: Color, + val disabledSummaryColor: Color, + val disabledValueColor: Color, + val disabledIconColor: Color, + val disabledArrowColor: Color, + val dialogBackgroundColor: Color, + val contentColor: Color, + val selectedContentColor: Color, + val selectedBackgroundColor: Color +) + +object SuperDropdownDefaults { + @Composable + fun colors( + titleColor: Color = MaterialTheme.colorScheme.onSurface, + summaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + valueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + iconColor: Color = MaterialTheme.colorScheme.primary, + arrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + disabledTitleColor: Color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.38f), + disabledSummaryColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledValueColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledIconColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + disabledArrowColor: Color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.38f), + dialogBackgroundColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + contentColor: Color = MaterialTheme.colorScheme.onSurface, + selectedContentColor: Color = MaterialTheme.colorScheme.primary, + selectedBackgroundColor: Color = MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) + ): SuperDropdownColors { + return SuperDropdownColors( + titleColor = titleColor, + summaryColor = summaryColor, + valueColor = valueColor, + iconColor = iconColor, + arrowColor = arrowColor, + disabledTitleColor = disabledTitleColor, + disabledSummaryColor = disabledSummaryColor, + disabledValueColor = disabledValueColor, + disabledIconColor = disabledIconColor, + disabledArrowColor = disabledArrowColor, + dialogBackgroundColor = dialogBackgroundColor, + contentColor = contentColor, + selectedContentColor = selectedContentColor, + selectedBackgroundColor = selectedBackgroundColor + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt index 914240c..e37cd81 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/VerticalExpandableFab.kt @@ -21,7 +21,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import com.sukisu.ultra.R -// 菜单项数据类 data class FabMenuItem( val icon: ImageVector, val labelRes: Int, @@ -29,7 +28,6 @@ data class FabMenuItem( val onClick: () -> Unit ) -// 动画配置 object FabAnimationConfig { const val ANIMATION_DURATION = 300 const val STAGGER_DELAY = 50 @@ -53,23 +51,15 @@ fun VerticalExpandableFab( ) { var isExpanded by remember { mutableStateOf(false) } - // 主按钮旋转动画 val rotationAngle by animateFloatAsState( targetValue = if (isExpanded) 45f else 0f, - animationSpec = tween( - durationMillis = animationDurationMs, - easing = FastOutSlowInEasing - ), + animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), label = "mainButtonRotation" ) - // 主按钮缩放动画 val mainButtonScale by animateFloatAsState( targetValue = if (isExpanded) 1.1f else 1f, - animationSpec = tween( - durationMillis = animationDurationMs, - easing = FastOutSlowInEasing - ), + animationSpec = tween(animationDurationMs, easing = FastOutSlowInEasing), label = "mainButtonScale" ) @@ -77,14 +67,9 @@ fun VerticalExpandableFab( modifier = modifier.wrapContentSize(), contentAlignment = Alignment.BottomEnd ) { - // 子菜单按钮 menuItems.forEachIndexed { index, menuItem -> val animatedOffsetY by animateFloatAsState( - targetValue = if (isExpanded) { - -(buttonSpacing.value * (index + 1)) - } else { - 0f - }, + targetValue = if (isExpanded) -(buttonSpacing.value * (index + 1)) else 0f, animationSpec = tween( durationMillis = animationDurationMs, delayMillis = if (isExpanded) { @@ -125,7 +110,6 @@ fun VerticalExpandableFab( label = "fabAlpha$index" ) - // 子按钮容器(包含标签) Row( modifier = Modifier .offset(y = animatedOffsetY.dp) @@ -134,7 +118,6 @@ fun VerticalExpandableFab( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.End ) { - // 标签 AnimatedVisibility( visible = isExpanded && animatedScale > 0.5f, enter = slideInHorizontally( @@ -161,7 +144,6 @@ fun VerticalExpandableFab( } } - // 子按钮 SmallFloatingActionButton( onClick = { menuItem.onClick() @@ -193,15 +175,12 @@ fun VerticalExpandableFab( } } - // 主按钮 FloatingActionButton( onClick = { onMainButtonClick?.invoke() isExpanded = !isExpanded }, - modifier = Modifier - .size(buttonSize) - .scale(mainButtonScale), + modifier = Modifier.size(buttonSize).scale(mainButtonScale), elevation = FloatingActionButtonDefaults.elevation( defaultElevation = 6.dp, pressedElevation = 8.dp, @@ -221,7 +200,6 @@ fun VerticalExpandableFab( } } -// 预设菜单项 object FabMenuPresets { fun getScrollMenuItems( onScrollToTop: () -> Unit, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt index 08c093a..7af311b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/component/profile/TemplateConfig.kt @@ -43,7 +43,7 @@ fun TemplateConfig( ) { OutlinedTextField( modifier = Modifier - .menuAnchor(MenuAnchorType.PrimaryNotEditable) + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable) .fillMaxWidth(), readOnly = true, label = { Text(stringResource(R.string.profile_template)) }, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt index bb8eca0..7764cb0 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/AppProfile.kt @@ -248,7 +248,12 @@ private fun AppProfileInner( ProfileBox(mode, true) { // template mode shouldn't change profile here! if (it == Mode.Default || it == Mode.Custom) { - onProfileChange(profile.copy(rootUseDefault = it == Mode.Default)) + onProfileChange( + profile.copy( + rootUseDefault = it == Mode.Default, + rootTemplate = null + ) + ) } mode = it } @@ -479,7 +484,10 @@ private fun ProfileBox( @SuppressLint("UnusedBoxWithConstraintsScope") @Composable -private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { +private fun AppMenuBox( + packageName: String, + content: @Composable () -> Unit +) { var expanded by remember { mutableStateOf(false) } var touchPoint: Offset by remember { mutableStateOf(Offset.Zero) } val density = LocalDensity.current @@ -499,15 +507,15 @@ private fun AppMenuBox(packageName: String, content: @Composable () -> Unit) { content() val (offsetX, offsetY) = with(density) { - (touchPoint.x.toDp()) to (touchPoint.y.toDp()) + (touchPoint.x.toDp()) to (-touchPoint.y.toDp()) } DropdownMenu( expanded = expanded, - offset = DpOffset(offsetX, -offsetY), + offset = DpOffset(offsetX, offsetY), onDismissRequest = { expanded = false - }, + } ) { AppMenuOption( text = stringResource(id = R.string.launch_app), diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt index d401806..e991839 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Flash.kt @@ -1,5 +1,7 @@ package com.sukisu.ultra.ui.screen +import android.content.Context +import android.content.Intent import android.net.Uri import android.os.Environment import android.os.Parcelable @@ -30,6 +32,7 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.activity.ComponentActivity import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination @@ -48,6 +51,9 @@ import kotlinx.parcelize.Parcelize import java.io.File import java.text.SimpleDateFormat import java.util.* +import androidx.core.content.edit +import com.sukisu.ultra.ui.util.module.ModuleOperationUtils +import com.sukisu.ultra.ui.util.module.ModuleUtils /** * @author ShirkNeko @@ -119,6 +125,29 @@ fun setModuleVerificationStatus(uri: Uri, isVerified: Boolean) { @Destination fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { val context = LocalContext.current + + val shouldAutoExit = remember { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.getBoolean("auto_exit_after_flash", false) + } + + // 是否通过从外部启动的模块安装 + val isExternalInstall = remember { + when (flashIt) { + is FlashIt.FlashModule -> { + (context as? ComponentActivity)?.intent?.let { intent -> + intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND + } ?: false + } + is FlashIt.FlashModules -> { + (context as? ComponentActivity)?.intent?.let { intent -> + intent.action == Intent.ACTION_VIEW || intent.action == Intent.ACTION_SEND + } ?: false + } + else -> false + } + } + var text by rememberSaveable { mutableStateOf("") } var tempText: String val logContent = rememberSaveable { StringBuilder() } @@ -203,8 +232,25 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { if (showReboot) { text += "\n\n\n" showFloatAction = true + + // 如果是内部安装,显示重启按钮后不自动返回 + if (isExternalInstall) { + return@flashModuleUpdate + } } hasUpdateCompleted = true + + // 如果是外部安装或需要自动退出的模块更新且不需要重启,延迟后自动返回 + if (isExternalInstall || shouldAutoExit) { + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } + } }, onStdout = { tempText = "$it\n" if (tempText.startsWith("")) { // clear command @@ -297,6 +343,26 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { kotlinx.coroutines.delay(500) navigator.navigate(FlashScreenDestination(nextFlashIt)) } + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModules && flashIt.currentIndex >= flashIt.uris.size - 1) { + // 如果是外部安装或需要自动退出且是最后一个模块,安装完成后自动返回 + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } + } else if ((isExternalInstall || shouldAutoExit) && flashIt is FlashIt.FlashModule) { + // 如果是外部安装或需要自动退出的单个模块,安装完成后自动返回 + scope.launch { + kotlinx.coroutines.delay(1000) + if (shouldAutoExit) { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + } + (context as? ComponentActivity)?.finish() + } } }, onStdout = { tempText = "$it\n" @@ -319,14 +385,18 @@ fun FlashScreen(navigator: DestinationsNavigator, flashIt: FlashIt) { } if (canGoBack) { - if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.navigate(ModuleScreenDestination) + if (isExternalInstall) { + (context as? ComponentActivity)?.finish() } else { - viewModel.markNeedRefresh() - viewModel.fetchModuleList() - navigator.popBackStack() + if (flashIt is FlashIt.FlashModules || flashIt is FlashIt.FlashModuleUpdate) { + viewModel.markNeedRefresh() + viewModel.fetchModuleList() + navigator.navigate(ModuleScreenDestination) + } else { + viewModel.markNeedRefresh() + viewModel.fetchModuleList() + navigator.popBackStack() + } } } } @@ -619,7 +689,7 @@ private fun TopBar( ) } -suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): String { +suspend fun getModuleNameFromUri(context: Context, uri: Uri): String { return withContext(Dispatchers.IO) { try { if (uri == Uri.EMPTY) { @@ -637,7 +707,7 @@ suspend fun getModuleNameFromUri(context: android.content.Context, uri: Uri): St @Parcelize sealed class FlashIt : Parcelable { - data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean) : FlashIt() + data class FlashBoot(val boot: Uri? = null, val lkm: LkmSelection, val ota: Boolean, val partition: String? = null) : FlashIt() data class FlashModule(val uri: Uri) : FlashIt() data class FlashModules(val uris: List, val currentIndex: Int = 0) : FlashIt() data class FlashModuleUpdate(val uri: Uri) : FlashIt() // 模块更新 @@ -666,6 +736,7 @@ fun flashIt( flashIt.boot, flashIt.lkm, flashIt.ota, + flashIt.partition, onFinish, onStdout, onStderr diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt index 954044e..b6ef712 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Home.kt @@ -50,20 +50,21 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager import com.sukisu.ultra.ui.util.checkNewVersion -import com.sukisu.ultra.ui.util.getSuSFS +import com.sukisu.ultra.ui.util.getSuSFSVersion import com.sukisu.ultra.ui.util.module.LatestVersionInfo import com.sukisu.ultra.ui.util.reboot import com.sukisu.ultra.ui.viewmodel.HomeViewModel import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import kotlin.random.Random /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class) @Destination(start = true) @@ -73,16 +74,35 @@ fun HomeScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() val coroutineScope = rememberCoroutineScope() + val pullRefreshState = rememberPullRefreshState( + refreshing = viewModel.isRefreshing, + onRefresh = { + viewModel.onPullRefresh(context) + } + ) + LaunchedEffect(key1 = navigator) { + viewModel.loadUserSettings(context) coroutineScope.launch { - viewModel.refreshAllData(context) + viewModel.loadCoreData() + delay(100) + viewModel.loadExtendedData(context) + } + + // 启动数据变化监听 + coroutineScope.launch { + while (true) { + delay(5000) // 每5秒检查一次 + viewModel.autoRefreshIfNeeded(context) + } } } - LaunchedEffect(Unit) { - viewModel.loadUserSettings(context) - viewModel.initializeData() - viewModel.checkForUpdates(context) + // 监听数据刷新状态流 + LaunchedEffect(viewModel.dataRefreshTrigger) { + viewModel.dataRefreshTrigger.collect { _ -> + // 数据刷新时的额外处理可以在这里添加 + } } val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -92,22 +112,14 @@ fun HomeScreen(navigator: DestinationsNavigator) { topBar = { TopBar( scrollBehavior = scrollBehavior, - navigator = navigator + navigator = navigator, + isDataLoaded = viewModel.isCoreDataLoaded ) }, contentWindowInsets = WindowInsets.safeDrawing.only( WindowInsetsSides.Top + WindowInsetsSides.Horizontal ) ) { innerPadding -> - val pullRefreshState = rememberPullRefreshState( - refreshing = false, - onRefresh = { - coroutineScope.launch { - viewModel.refreshAllData(context) - } - } - ) - Box( modifier = Modifier .padding(innerPadding) @@ -121,50 +133,78 @@ fun HomeScreen(navigator: DestinationsNavigator) { .padding(top = 12.dp, start = 16.dp, end = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - StatusCard( - systemStatus = viewModel.systemStatus, - onClickInstall = { - navigator.navigate(InstallScreenDestination) - } - ) + // 状态卡片 + if (viewModel.isCoreDataLoaded) { + StatusCard( + systemStatus = viewModel.systemStatus, + onClickInstall = { + navigator.navigate(InstallScreenDestination(preselectedKernelUri = null)) + } + ) - if (viewModel.systemStatus.requireNewKernel) { - WarningCard( - stringResource(id = R.string.require_kernel_version).format( - Natives.getSimpleVersionFull(), - Natives.MINIMAL_SUPPORTED_KERNEL_FULL + // 警告信息 + if (viewModel.systemStatus.requireNewKernel) { + WarningCard( + stringResource(id = R.string.require_kernel_version).format( + Natives.getSimpleVersionFull(), + Natives.MINIMAL_SUPPORTED_KERNEL_FULL + ) ) + } + + if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) { + WarningCard( + stringResource(id = R.string.grant_root_failed) + ) + } + + // 只有在没有其他警告信息时才显示不兼容内核警告 + val shouldShowWarnings = viewModel.systemStatus.requireNewKernel || + (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) + + if (Natives.version <= Natives.MINIMAL_NEW_IOCTL_KERNEL && !shouldShowWarnings && viewModel.systemStatus.ksuVersion != null) { + IncompatibleKernelCard() + Spacer(Modifier.height(12.dp)) + } + } + + // 更新检查 + if (viewModel.isExtendedDataLoaded) { + val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + .getBoolean("check_update", true) + if (checkUpdate) { + UpdateCard() + } + + // 信息卡片 + InfoCard( + systemInfo = viewModel.systemInfo, + isSimpleMode = viewModel.isSimpleMode, + isHideSusfsStatus = viewModel.isHideSusfsStatus, + isHideZygiskImplement = viewModel.isHideZygiskImplement, + showKpmInfo = viewModel.showKpmInfo, + lkmMode = viewModel.systemStatus.lkmMode, ) - } - if (viewModel.systemStatus.ksuVersion != null && !viewModel.systemStatus.isRootAvailable) { - WarningCard( - stringResource(id = R.string.grant_root_failed) - ) - } - - val checkUpdate = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - .getBoolean("check_update", true) - if (checkUpdate) { - UpdateCard() - } - - InfoCard( - systemInfo = viewModel.systemInfo, - isSimpleMode = viewModel.isSimpleMode, - isHideSusfsStatus = viewModel.isHideSusfsStatus, - isHideZygiskImplement = viewModel.isHideZygiskImplement, - showKpmInfo = viewModel.showKpmInfo, - lkmMode = viewModel.systemStatus.lkmMode, - ) - - if (!viewModel.isSimpleMode) { - if (!viewModel.isHideLinkCard) { + // 链接卡片 + if (!viewModel.isSimpleMode && !viewModel.isHideLinkCard) { ContributionCard() DonateCard() LearnMoreCard() } } + + if (!viewModel.isExtendedDataLoaded) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(24.dp), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } + Spacer(Modifier.height(16.dp)) } } @@ -231,7 +271,8 @@ fun RebootDropdownItem(@StringRes id: Int, reason: String = "") { @Composable private fun TopBar( scrollBehavior: TopAppBarScrollBehavior? = null, - navigator: DestinationsNavigator + navigator: DestinationsNavigator, + isDataLoaded: Boolean = false ) { val context = LocalContext.current val colorScheme = MaterialTheme.colorScheme @@ -253,44 +294,47 @@ private fun TopBar( scrolledContainerColor = cardColor.copy(alpha = cardAlpha) ), actions = { - // SuSFS 配置按钮 - if (getSuSFS() == "Supported" && SuSFSManager.isBinaryAvailable(context)) { - IconButton(onClick = { - navigator.navigate(SuSFSConfigScreenDestination) - }) { - Icon( - imageVector = Icons.Filled.Tune, - contentDescription = stringResource(R.string.susfs_config_setting_title) - ) - } - } - - // 重启按钮 - var showDropdown by remember { mutableStateOf(false) } - KsuIsValid { - IconButton(onClick = { - showDropdown = true - }) { - Icon( - imageVector = Icons.Filled.PowerSettingsNew, - contentDescription = stringResource(id = R.string.reboot) - ) - - DropdownMenu(expanded = showDropdown, onDismissRequest = { - showDropdown = false + if (isDataLoaded) { + // SuSFS 配置按钮 + val susfsVersion = getSuSFSVersion() + if (susfsVersion.isNotEmpty() && !susfsVersion.startsWith("[-]") && SuSFSManager.isBinaryAvailable(context)) { + IconButton(onClick = { + navigator.navigate(SuSFSConfigScreenDestination) }) { - RebootDropdownItem(id = R.string.reboot) + Icon( + imageVector = Icons.Filled.Tune, + contentDescription = stringResource(R.string.susfs_config_setting_title) + ) + } + } - val pm = - LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? - @Suppress("DEPRECATION") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { - RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + // 重启按钮 + var showDropdown by remember { mutableStateOf(false) } + KsuIsValid { + IconButton(onClick = { + showDropdown = true + }) { + Icon( + imageVector = Icons.Filled.PowerSettingsNew, + contentDescription = stringResource(id = R.string.reboot) + ) + + DropdownMenu(expanded = showDropdown, onDismissRequest = { + showDropdown = false + }) { + RebootDropdownItem(id = R.string.reboot) + + val pm = + LocalContext.current.getSystemService(Context.POWER_SERVICE) as PowerManager? + @Suppress("DEPRECATION") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm?.isRebootingUserspaceSupported == true) { + RebootDropdownItem(id = R.string.reboot_userspace, reason = "userspace") + } + RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") + RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") + RebootDropdownItem(id = R.string.reboot_download, reason = "download") + RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") } - RebootDropdownItem(id = R.string.reboot_recovery, reason = "recovery") - RebootDropdownItem(id = R.string.reboot_bootloader, reason = "bootloader") - RebootDropdownItem(id = R.string.reboot_download, reason = "download") - RebootDropdownItem(id = R.string.reboot_edl, reason = "edl") } } } @@ -731,7 +775,7 @@ private fun InfoCard( systemInfo.seLinuxStatus, icon = Icons.Default.Security, ) - + if (!isHideZygiskImplement && !isSimpleMode && systemInfo.zygiskImplement != "None") { InfoCardItem( stringResource(R.string.home_zygisk_implement), @@ -741,7 +785,6 @@ private fun InfoCard( } if (!isSimpleMode) { - // 根据showKpmInfo决定是否显示KPM信息 if (lkmMode != true && !showKpmInfo) { val displayVersion = if (systemInfo.kpmVersion.isEmpty() || systemInfo.kpmVersion.startsWith("Error")) { @@ -785,21 +828,15 @@ private fun InfoCard( private fun SuSFSInfoText(systemInfo: HomeViewModel.SystemInfo): String = buildString { append(systemInfo.suSFSVersion) - val isSUS_SU = systemInfo.suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" - val isKprobesHook = Natives.getHookType() == "Kprobes" - when { - isSUS_SU && isKprobesHook -> { - append(" (${systemInfo.suSFSVariant})") - if (systemInfo.susSUMode.isNotEmpty()) { - append(" ${stringResource(R.string.sus_su_mode)} ${systemInfo.susSUMode}") - } - } - Natives.getHookType() == "Manual" -> { append(" (${stringResource(R.string.manual_hook)})") } + Natives.getHookType() == "Inline" -> { + append(" (${stringResource(R.string.inline_hook)})") + } + else -> { append(" (${Natives.getHookType()})") } @@ -829,7 +866,7 @@ private fun StatusCardPreview() { StatusCard( HomeViewModel.SystemStatus( isManager = true, - ksuVersion = 20000, + ksuVersion = 40000, lkmMode = true, kernelVersion = KernelVersion(5, 10, 101), isRootAvailable = true @@ -858,6 +895,23 @@ private fun StatusCardPreview() { } } +@Composable +private fun IncompatibleKernelCard() { + val currentKver = remember { Natives.version } + val threshold = Natives.MINIMAL_NEW_IOCTL_KERNEL + + val msg = stringResource( + id = R.string.incompatible_kernel_msg, + currentKver, + threshold + ) + + WarningCard( + message = msg, + color = MaterialTheme.colorScheme.error + ) +} + @Preview @Composable private fun WarningCardPreview() { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt index 9f4fb7c..15aa078 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Install.kt @@ -1,8 +1,10 @@ package com.sukisu.ultra.ui.screen import android.app.Activity +import android.content.Context import android.content.Intent import android.net.Uri +import android.provider.OpenableColumns import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -17,7 +19,9 @@ import androidx.compose.foundation.selection.selectable 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.Input import androidx.compose.material.icons.filled.AutoFixHigh +import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.FileUpload import androidx.compose.material.icons.filled.Security import androidx.compose.material3.* @@ -33,6 +37,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import com.maxkeppeker.sheets.core.models.base.Header import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState import com.maxkeppeler.sheets.list.ListDialog @@ -47,7 +52,7 @@ import com.ramcosta.composedestinations.navigation.EmptyDestinationsNavigator import com.sukisu.ultra.R import com.sukisu.ultra.getKernelVersion import com.sukisu.ultra.ui.component.DialogHandle -import com.sukisu.ultra.ui.component.SlotSelectionDialog +import com.sukisu.ultra.ui.component.SuperDropdown import com.sukisu.ultra.ui.component.rememberConfirmDialog import com.sukisu.ultra.ui.component.rememberCustomDialog import com.sukisu.ultra.ui.theme.CardConfig @@ -56,6 +61,7 @@ import com.sukisu.ultra.ui.theme.CardConfig.cardElevation import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* +import zako.zako.zako.zakoui.screen.kernelFlash.component.SlotSelectionDialog /** * @author ShirkNeko @@ -71,19 +77,49 @@ enum class KpmPatchOption { @OptIn(ExperimentalMaterial3Api::class) @Destination @Composable -fun InstallScreen(navigator: DestinationsNavigator) { +fun InstallScreen( + navigator: DestinationsNavigator, + preselectedKernelUri: String? = null +) { + val context = LocalContext.current var installMethod by remember { mutableStateOf(null) } var lkmSelection by remember { mutableStateOf(LkmSelection.KmiNone) } var kpmPatchOption by remember { mutableStateOf(KpmPatchOption.FOLLOW_KERNEL) } - val context = LocalContext.current var showRebootDialog by remember { mutableStateOf(false) } var showSlotSelectionDialog by remember { mutableStateOf(false) } + var showKpmPatchDialog by remember { mutableStateOf(false) } var tempKernelUri by remember { mutableStateOf(null) } + val kernelVersion = getKernelVersion() val isGKI = kernelVersion.isGKI() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val summary = stringResource(R.string.horizon_kernel_summary) + // 处理预选的内核文件 + LaunchedEffect(preselectedKernelUri) { + preselectedKernelUri?.let { uriString -> + try { + val preselectedUri = uriString.toUri() + val horizonMethod = InstallMethod.HorizonKernel( + uri = preselectedUri, + summary = summary + ) + installMethod = horizonMethod + tempKernelUri = preselectedUri + + if (isAbDevice) { + showSlotSelectionDialog = true + } else { + showKpmPatchDialog = true + } + } catch (e: Exception) { + e.printStackTrace() + } + } + } + if (showRebootDialog) { RebootDialog( show = true, @@ -103,6 +139,10 @@ fun InstallScreen(navigator: DestinationsNavigator) { ) } + var partitionSelectionIndex by remember { mutableIntStateOf(0) } + var partitionsState by remember { mutableStateOf>(emptyList()) } + var hasCustomSelected by remember { mutableStateOf(false) } + val onInstall = { installMethod?.let { method -> when (method) { @@ -119,10 +159,13 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } else -> { + val isOta = method is InstallMethod.DirectInstallToInactiveSlot + val partitionSelection = partitionsState.getOrNull(partitionSelectionIndex) val flashIt = FlashIt.FlashBoot( boot = if (method is InstallMethod.SelectFile) method.uri else null, lkm = lkmSelection, - ota = method is InstallMethod.DirectInstallToInactiveSlot + ota = isOta, + partition = partitionSelection ) navigator.navigate(FlashScreenDestination(flashIt)) } @@ -143,6 +186,20 @@ fun InstallScreen(navigator: DestinationsNavigator) { summary = summary ) installMethod = horizonMethod + + if (preselectedKernelUri != null) { + showKpmPatchDialog = true + } + } + ) + + KpmPatchSelectionDialog( + show = showKpmPatchDialog, + currentOption = kpmPatchOption, + onDismiss = { showKpmPatchDialog = false }, + onOptionSelected = { option -> + kpmPatchOption = option + showKpmPatchDialog = false } ) @@ -165,6 +222,32 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } + val selectLkmLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { + if (it.resultCode == Activity.RESULT_OK) { + it.data?.data?.let { uri -> + val isKo = isKoFile(context, uri) + if (isKo) { + lkmSelection = LkmSelection.LkmUri(uri) + } else { + lkmSelection = LkmSelection.KmiNone + Toast.makeText( + context, + context.getString(R.string.install_only_support_ko_file), + Toast.LENGTH_SHORT + ).show() + } + } + } + } + + val onLkmUpload = { + selectLkmLauncher.launch(Intent(Intent.ACTION_GET_CONTENT).apply { + type = "application/octet-stream" + }) + } + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) Scaffold( @@ -194,6 +277,7 @@ fun InstallScreen(navigator: DestinationsNavigator) { showSlotSelectionDialog = true } else { installMethod = method + showKpmPatchDialog = true } } else { installMethod = method @@ -204,35 +288,104 @@ fun InstallScreen(navigator: DestinationsNavigator) { selectedMethod = installMethod ) - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp) + // 选择LKM直接安装分区 + AnimatedVisibility( + visible = installMethod is InstallMethod.DirectInstall || installMethod is InstallMethod.DirectInstallToInactiveSlot, + enter = fadeIn() + expandVertically(), + exit = shrinkVertically() + fadeOut() ) { - (lkmSelection as? LkmSelection.LkmUri)?.let { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { ElevatedCard( colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), elevation = getCardElevation(), modifier = Modifier .fillMaxWidth() - .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) + .padding(bottom = 12.dp), ) { - Text( - text = stringResource( - id = R.string.selected_lkm, - it.uri.lastPathSegment ?: "(file)" - ), - style = MaterialTheme.typography.bodyMedium, - modifier = Modifier.padding(16.dp) + val isOta = installMethod is InstallMethod.DirectInstallToInactiveSlot + val suffix = produceState(initialValue = "", isOta) { + value = getSlotSuffix(isOta) + }.value + + val partitions = produceState(initialValue = emptyList()) { + value = getAvailablePartitions() + }.value + + val defaultPartition = produceState(initialValue = "") { + value = getDefaultPartition() + }.value + + partitionsState = partitions + val displayPartitions = partitions.map { name -> + if (defaultPartition == name) "$name (default)" else name + } + + val defaultIndex = partitions.indexOf(defaultPartition).takeIf { it >= 0 } ?: 0 + if (!hasCustomSelected) partitionSelectionIndex = defaultIndex + + SuperDropdown( + items = displayPartitions, + selectedIndex = partitionSelectionIndex, + title = "${stringResource(R.string.install_select_partition)} (${suffix})", + onSelectedIndexChange = { index -> + hasCustomSelected = true + partitionSelectionIndex = index + }, + leftAction = { + Icon( + Icons.Default.Edit, + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(end = 16.dp), + contentDescription = null + ) + } ) } } + } + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) { + // 使用本地的LKM文件 + ElevatedCard( + colors = getCardColors(MaterialTheme.colorScheme.surfaceVariant), + elevation = getCardElevation(), + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 12.dp), + ) { + ListItem( + headlineContent = { + Text(stringResource(id = R.string.install_upload_lkm_file)) + }, + supportingContent = { + (lkmSelection as? LkmSelection.LkmUri)?.let { + Text( + stringResource( + id = R.string.selected_lkm, + it.uri.lastPathSegment ?: "(file)" + ) + ) + } + }, + leadingContent = { + Icon( + Icons.AutoMirrored.Filled.Input, + contentDescription = null + ) + }, + modifier = Modifier + .fillMaxWidth() + .clickable { onLkmUpload() } + ) + } (installMethod as? InstallMethod.HorizonKernel)?.let { method -> if (method.slot != null) { @@ -242,12 +395,6 @@ fun InstallScreen(navigator: DestinationsNavigator) { modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = stringResource( @@ -269,12 +416,6 @@ fun InstallScreen(navigator: DestinationsNavigator) { modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.medium) - .shadow( - elevation = cardElevation, - shape = MaterialTheme.shapes.medium, - spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f) - ) ) { Text( text = when (kpmPatchOption) { @@ -316,6 +457,47 @@ fun InstallScreen(navigator: DestinationsNavigator) { } } +@Composable +private fun KpmPatchSelectionDialog( + show: Boolean, + currentOption: KpmPatchOption, + onDismiss: () -> Unit, + onOptionSelected: (KpmPatchOption) -> Unit +) { + if (show) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.kpm_patch_options)) }, + text = { + Column { + Text( + text = stringResource(R.string.kpm_patch_description), + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.padding(bottom = 16.dp) + ) + + KpmPatchOptionGroup( + selectedOption = currentOption, + onOptionChanged = onOptionSelected + ) + } + }, + confirmButton = { + TextButton( + onClick = { onOptionSelected(currentOption) } + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } +} + @Composable private fun RebootDialog( show: Boolean, @@ -378,15 +560,15 @@ private fun SelectInstallMethod( selectedMethod: InstallMethod? = null ) { val rootAvailable = rootAvailable() - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value + val defaultPartitionName = produceState(initialValue = "boot") { + value = getDefaultPartition() + }.value val horizonKernelSummary = stringResource(R.string.horizon_kernel_summary) val selectFileTip = stringResource( - id = R.string.select_file_tip, - if (isInitBoot()) { - "init_boot / vendor_boot ${stringResource(R.string.select_file_tip_vendor)}" - } else { - "boot" - } + id = R.string.select_file_tip, defaultPartitionName ) val radioOptions = mutableListOf( @@ -404,6 +586,10 @@ private fun SelectInstallMethod( var selectedOption by remember { mutableStateOf(null) } var currentSelectingMethod by remember { mutableStateOf(null) } + LaunchedEffect(selectedMethod) { + selectedOption = selectedMethod + } + val selectImageLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { @@ -479,7 +665,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 16.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( @@ -518,7 +703,7 @@ private fun SelectInstallMethod( bottom = 16.dp ) ) { - radioOptions.take(3).forEach { option -> + radioOptions.filter { it !is InstallMethod.HorizonKernel }.forEach { option -> val interactionSource = remember { MutableInteractionSource() } Surface( color = if (option.javaClass == selectedOption?.javaClass) @@ -586,7 +771,6 @@ private fun SelectInstallMethod( modifier = Modifier .fillMaxWidth() .padding(bottom = 12.dp) - .clip(MaterialTheme.shapes.large) ) { MaterialTheme( colorScheme = MaterialTheme.colorScheme.copy( @@ -884,6 +1068,31 @@ private fun TopBar( ) } +private 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 + } +} + @Preview @Composable fun SelectInstallPreview() { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt new file mode 100644 index 0000000..929a8b2 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/LogViewerScreen.kt @@ -0,0 +1,941 @@ +package com.sukisu.ultra.ui.screen + +import android.content.Context +import androidx.compose.animation.* +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.* +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha +import com.sukisu.ultra.ui.theme.getCardColors +import com.sukisu.ultra.ui.theme.getCardElevation +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.time.* +import java.time.format.DateTimeFormatter +import android.os.Process.myUid +import androidx.core.content.edit + +private val SPACING_SMALL = 4.dp +private val SPACING_MEDIUM = 8.dp +private val SPACING_LARGE = 16.dp + +private const val PAGE_SIZE = 10000 +private const val MAX_TOTAL_LOGS = 100000 + +private const val LOGS_PATCH = "/data/adb/ksu/log/sulog.log" + +data class LogEntry( + val timestamp: String, + val type: LogType, + val uid: String, + val comm: String, + val details: String, + val pid: String, + val rawLine: String +) + +data class LogPageInfo( + val currentPage: Int = 0, + val totalPages: Int = 0, + val totalLogs: Int = 0, + val hasMore: Boolean = false +) + +enum class LogType(val displayName: String, val color: Color) { + SU_GRANT("SU_GRANT", Color(0xFF4CAF50)), + SU_EXEC("SU_EXEC", Color(0xFF2196F3)), + PERM_CHECK("PERM_CHECK", Color(0xFFFF9800)), + SYSCALL("SYSCALL", Color(0xFF00BCD4)), + MANAGER_OP("MANAGER_OP", Color(0xFF9C27B0)), + UNKNOWN("UNKNOWN", Color(0xFF757575)) +} + +enum class LogExclType(val displayName: String, val color: Color) { + CURRENT_APP("Current app", Color(0xFF9E9E9E)), + PRCTL_STAR("prctl_*", Color(0xFF00BCD4)), + PRCTL_UNKNOWN("prctl_unknown", Color(0xFF00BCD4)), + SETUID("setuid", Color(0xFF00BCD4)) +} + +private val utcFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") +private val localFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss") + +private fun saveExcludedSubTypes(context: Context, types: Set) { + val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) + val nameSet = types.map { it.name }.toSet() + prefs.edit { putStringSet("excluded_subtypes", nameSet) } +} + +private fun loadExcludedSubTypes(context: Context): Set { + val prefs = context.getSharedPreferences("sulog", Context.MODE_PRIVATE) + val nameSet = prefs.getStringSet("excluded_subtypes", emptySet()) ?: emptySet() + return nameSet.mapNotNull { name -> + LogExclType.entries.firstOrNull { it.name == name } + }.toSet() +} + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun LogViewerScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + + var logEntries by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var filterType by rememberSaveable { mutableStateOf(null) } + var searchQuery by rememberSaveable { mutableStateOf("") } + var showSearchBar by rememberSaveable { mutableStateOf(false) } + var pageInfo by remember { mutableStateOf(LogPageInfo()) } + var lastLogFileHash by remember { mutableStateOf("") } + val currentUid = remember { myUid().toString() } + + val initialExcluded = remember { + loadExcludedSubTypes(context) + } + + var excludedSubTypes by rememberSaveable { mutableStateOf(initialExcluded) } + + LaunchedEffect(excludedSubTypes) { + saveExcludedSubTypes(context, excludedSubTypes) + } + + val filteredEntries = remember( + logEntries, filterType, searchQuery, excludedSubTypes + ) { + logEntries.filter { entry -> + val matchesSearch = searchQuery.isEmpty() || + entry.comm.contains(searchQuery, ignoreCase = true) || + entry.details.contains(searchQuery, ignoreCase = true) || + entry.uid.contains(searchQuery, ignoreCase = true) + + // 排除本应用 + if (LogExclType.CURRENT_APP in excludedSubTypes && entry.uid == currentUid) return@filter false + + // 排除 SYSCALL 子类型 + if (entry.type == LogType.SYSCALL) { + val detail = entry.details + if (LogExclType.PRCTL_STAR in excludedSubTypes && detail.startsWith("Syscall: prctl") && !detail.startsWith("Syscall: prctl_unknown")) return@filter false + if (LogExclType.PRCTL_UNKNOWN in excludedSubTypes && detail.startsWith("Syscall: prctl_unknown")) return@filter false + if (LogExclType.SETUID in excludedSubTypes && detail.startsWith("Syscall: setuid")) return@filter false + } + + // 普通类型筛选 + val matchesFilter = filterType == null || entry.type == filterType + matchesFilter && matchesSearch + } + } + + val loadingDialog = rememberLoadingDialog() + val confirmDialog = rememberConfirmDialog() + + val loadPage: (Int, Boolean) -> Unit = { page, forceRefresh -> + scope.launch { + if (isLoading) return@launch + + isLoading = true + try { + loadLogsWithPagination( + page, + forceRefresh, + lastLogFileHash + ) { entries, newPageInfo, newHash -> + logEntries = if (page == 0 || forceRefresh) { + entries + } else { + logEntries + entries + } + pageInfo = newPageInfo + lastLogFileHash = newHash + } + } finally { + isLoading = false + } + } + } + + val onManualRefresh: () -> Unit = { + loadPage(0, true) + } + + val loadNextPage: () -> Unit = { + if (pageInfo.hasMore && !isLoading) { + loadPage(pageInfo.currentPage + 1, false) + } + } + + LaunchedEffect(Unit) { + while (true) { + delay(5_000) + if (!isLoading) { + scope.launch { + val hasNewLogs = checkForNewLogs(lastLogFileHash) + if (hasNewLogs) { + loadPage(0, true) + } + } + } + } + } + + LaunchedEffect(Unit) { + loadPage(0, true) + } + + Scaffold( + topBar = { + LogViewerTopBar( + scrollBehavior = scrollBehavior, + onBackClick = { navigator.navigateUp() }, + showSearchBar = showSearchBar, + searchQuery = searchQuery, + onSearchQueryChange = { searchQuery = it }, + onSearchToggle = { showSearchBar = !showSearchBar }, + onRefresh = onManualRefresh, + onClearLogs = { + scope.launch { + val result = confirmDialog.awaitConfirm( + title = context.getString(R.string.log_viewer_clear_logs), + content = context.getString(R.string.log_viewer_clear_logs_confirm) + ) + if (result == ConfirmResult.Confirmed) { + loadingDialog.withLoading { + clearLogs() + loadPage(0, true) + } + snackBarHost.showSnackbar(context.getString(R.string.log_viewer_logs_cleared)) + } + } + } + ) + }, + snackbarHost = { SnackbarHost(snackBarHost) }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + LogControlPanel( + filterType = filterType, + onFilterTypeSelected = { filterType = it }, + logCount = filteredEntries.size, + totalCount = logEntries.size, + pageInfo = pageInfo, + excludedSubTypes = excludedSubTypes, + onExcludeToggle = { excl -> + excludedSubTypes = if (excl in excludedSubTypes) + excludedSubTypes - excl + else + excludedSubTypes + excl + } + ) + + // 日志列表 + if (isLoading && logEntries.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else if (filteredEntries.isEmpty()) { + EmptyLogState( + hasLogs = logEntries.isNotEmpty(), + onRefresh = onManualRefresh + ) + } else { + LogList( + entries = filteredEntries, + pageInfo = pageInfo, + isLoading = isLoading, + onLoadMore = loadNextPage, + modifier = Modifier.fillMaxSize() + ) + } + } + } +} + +@Composable +private fun LogControlPanel( + filterType: LogType?, + onFilterTypeSelected: (LogType?) -> Unit, + logCount: Int, + totalCount: Int, + pageInfo: LogPageInfo, + excludedSubTypes: Set, + onExcludeToggle: (LogExclType) -> Unit +) { + var isExpanded by rememberSaveable { mutableStateOf(true) } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = getCardElevation() + ) { + Column { + // 标题栏(点击展开/收起) + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { isExpanded = !isExpanded } + .padding(SPACING_LARGE), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = stringResource(R.string.settings), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (isExpanded) Icons.Filled.ExpandLess else Icons.Filled.ExpandMore, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility( + visible = isExpanded, + enter = expandVertically() + fadeIn(), + exit = shrinkVertically() + fadeOut() + ) { + Column( + modifier = Modifier.padding(horizontal = SPACING_LARGE) + ) { + // 类型过滤 + Text( + text = stringResource(R.string.log_viewer_filter_type), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { + item { + FilterChip( + onClick = { onFilterTypeSelected(null) }, + label = { Text(stringResource(R.string.log_viewer_all_types)) }, + selected = filterType == null + ) + } + items(LogType.entries.toTypedArray()) { type -> + FilterChip( + onClick = { onFilterTypeSelected(if (filterType == type) null else type) }, + label = { Text(type.displayName) }, + selected = filterType == type, + leadingIcon = { + Box( + modifier = Modifier + .size(8.dp) + .background(type.color, RoundedCornerShape(4.dp)) + ) + } + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + // 排除子类型 + Text( + text = stringResource(R.string.log_viewer_exclude_subtypes), + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + LazyRow(horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM)) { + items(LogExclType.entries.toTypedArray()) { excl -> + val label = if (excl == LogExclType.CURRENT_APP) + stringResource(R.string.log_viewer_exclude_current_app) + else excl.displayName + + FilterChip( + onClick = { onExcludeToggle(excl) }, + label = { Text(label) }, + selected = excl in excludedSubTypes, + leadingIcon = { + Box( + modifier = Modifier + .size(8.dp) + .background(excl.color, RoundedCornerShape(4.dp)) + ) + } + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + // 统计信息 + Column(verticalArrangement = Arrangement.spacedBy(SPACING_SMALL)) { + Text( + text = stringResource(R.string.log_viewer_showing_entries, logCount, totalCount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + if (pageInfo.totalPages > 0) { + Text( + text = stringResource( + R.string.log_viewer_page_info, + pageInfo.currentPage + 1, + pageInfo.totalPages, + pageInfo.totalLogs + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + if (pageInfo.totalLogs >= MAX_TOTAL_LOGS) { + Text( + text = stringResource(R.string.log_viewer_too_many_logs, MAX_TOTAL_LOGS), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.error + ) + } + } + + Spacer(modifier = Modifier.height(SPACING_LARGE)) + } + } + } + } +} + +@Composable +private fun LogList( + entries: List, + pageInfo: LogPageInfo, + isLoading: Boolean, + onLoadMore: () -> Unit, + modifier: Modifier = Modifier +) { + val listState = rememberLazyListState() + + LazyColumn( + state = listState, + modifier = modifier, + contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + verticalArrangement = Arrangement.spacedBy(SPACING_SMALL) + ) { + items(entries) { entry -> + LogEntryCard(entry = entry) + } + + // 加载更多按钮或加载指示器 + if (pageInfo.hasMore) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + contentAlignment = Alignment.Center + ) { + if (isLoading) { + CircularProgressIndicator( + modifier = Modifier.size(24.dp) + ) + } else { + Button( + onClick = onLoadMore, + modifier = Modifier.fillMaxWidth() + ) { + Icon( + imageVector = Icons.Filled.ExpandMore, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.log_viewer_load_more)) + } + } + } + } + } else if (entries.isNotEmpty()) { + item { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + contentAlignment = Alignment.Center + ) { + Text( + text = stringResource(R.string.log_viewer_all_logs_loaded), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun LogEntryCard(entry: LogEntry) { + var expanded by remember { mutableStateOf(false) } + + Card( + modifier = Modifier + .fillMaxWidth() + .clickable { expanded = !expanded }, + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = CardDefaults.cardElevation(defaultElevation = 0.dp) + ) { + Column( + modifier = Modifier.padding(SPACING_MEDIUM) + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + Box( + modifier = Modifier + .size(12.dp) + .background(entry.type.color, RoundedCornerShape(6.dp)) + ) + Text( + text = entry.type.displayName, + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold + ) + } + Text( + text = entry.timestamp, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Spacer(modifier = Modifier.height(SPACING_SMALL)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "UID: ${entry.uid}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = "PID: ${entry.pid}", + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + Text( + text = entry.comm, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + maxLines = if (expanded) Int.MAX_VALUE else 1, + overflow = TextOverflow.Ellipsis + ) + + if (entry.details.isNotEmpty()) { + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = entry.details, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = if (expanded) Int.MAX_VALUE else 2, + overflow = TextOverflow.Ellipsis + ) + } + + AnimatedVisibility( + visible = expanded, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + Column { + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + Text( + text = stringResource(R.string.log_viewer_raw_log), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = entry.rawLine, + style = MaterialTheme.typography.bodySmall, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } +} + +@Composable +private fun EmptyLogState( + hasLogs: Boolean, + onRefresh: () -> Unit +) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(SPACING_LARGE) + ) { + Icon( + imageVector = if (hasLogs) Icons.Filled.FilterList else Icons.Filled.Description, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource( + if (hasLogs) R.string.log_viewer_no_matching_logs + else R.string.log_viewer_no_logs + ), + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Button(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = null, + modifier = Modifier.size(18.dp) + ) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.log_viewer_refresh)) + } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun LogViewerTopBar( + scrollBehavior: TopAppBarScrollBehavior? = null, + onBackClick: () -> Unit, + showSearchBar: Boolean, + searchQuery: String, + onSearchQueryChange: (String) -> Unit, + onSearchToggle: () -> Unit, + onRefresh: () -> Unit, + onClearLogs: () -> Unit +) { + val colorScheme = MaterialTheme.colorScheme + val cardColor = if (CardConfig.isCustomBackgroundEnabled) { + colorScheme.surfaceContainerLow + } else { + colorScheme.background + } + + Column { + TopAppBar( + title = { + Text( + text = stringResource(R.string.log_viewer_title), + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = onBackClick) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.log_viewer_back) + ) + } + }, + actions = { + IconButton(onClick = onSearchToggle) { + Icon( + imageVector = if (showSearchBar) Icons.Filled.SearchOff else Icons.Filled.Search, + contentDescription = stringResource(R.string.log_viewer_search) + ) + } + IconButton(onClick = onRefresh) { + Icon( + imageVector = Icons.Filled.Refresh, + contentDescription = stringResource(R.string.log_viewer_refresh) + ) + } + IconButton(onClick = onClearLogs) { + Icon( + imageVector = Icons.Filled.DeleteSweep, + contentDescription = stringResource(R.string.log_viewer_clear_logs) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = cardColor.copy(alpha = cardAlpha), + scrolledContainerColor = cardColor.copy(alpha = cardAlpha) + ), + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) + + AnimatedVisibility( + visible = showSearchBar, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = onSearchQueryChange, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + placeholder = { Text(stringResource(R.string.log_viewer_search_placeholder)) }, + leadingIcon = { + Icon( + imageVector = Icons.Filled.Search, + contentDescription = null + ) + }, + trailingIcon = { + if (searchQuery.isNotEmpty()) { + IconButton(onClick = { onSearchQueryChange("") }) { + Icon( + imageVector = Icons.Filled.Clear, + contentDescription = stringResource(R.string.log_viewer_clear_search) + ) + } + } + }, + singleLine = true + ) + } + } +} + +private suspend fun checkForNewLogs( + lastHash: String +): Boolean { + return withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + val logPath = "/data/adb/ksu/log/sulog.log" + + val result = runCmd(shell, "stat -c '%Y %s' $logPath 2>/dev/null || echo '0 0'") + val currentHash = result.trim() + + currentHash != lastHash && currentHash != "0 0" + } catch (_: Exception) { + false + } + } +} + +private suspend fun loadLogsWithPagination( + page: Int, + forceRefresh: Boolean, + lastHash: String, + onLoaded: (List, LogPageInfo, String) -> Unit +) { + withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + + // 获取文件信息 + val statResult = runCmd(shell, "stat -c '%Y %s' $LOGS_PATCH 2>/dev/null || echo '0 0'") + val currentHash = statResult.trim() + + if (!forceRefresh && currentHash == lastHash && currentHash != "0 0") { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), currentHash) + } + return@withContext + } + + // 获取总行数 + val totalLinesResult = runCmd(shell, "wc -l < $LOGS_PATCH 2>/dev/null || echo '0'") + val totalLines = totalLinesResult.trim().toIntOrNull() ?: 0 + + if (totalLines == 0) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), currentHash) + } + return@withContext + } + + // 限制最大日志数量 + val effectiveTotal = minOf(totalLines, MAX_TOTAL_LOGS) + val totalPages = (effectiveTotal + PAGE_SIZE - 1) / PAGE_SIZE + + // 计算要读取的行数范围 + val startLine = if (page == 0) { + maxOf(1, totalLines - effectiveTotal + 1) + } else { + val skipLines = page * PAGE_SIZE + maxOf(1, totalLines - effectiveTotal + 1 + skipLines) + } + + val endLine = minOf(startLine + PAGE_SIZE - 1, totalLines) + + if (startLine > totalLines) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(page, totalPages, effectiveTotal, false), currentHash) + } + return@withContext + } + + val result = runCmd(shell, "sed -n '${startLine},${endLine}p' $LOGS_PATCH 2>/dev/null || echo ''") + val entries = parseLogEntries(result) + + val hasMore = endLine < totalLines + val pageInfo = LogPageInfo(page, totalPages, effectiveTotal, hasMore) + + withContext(Dispatchers.Main) { + onLoaded(entries, pageInfo, currentHash) + } + } catch (_: Exception) { + withContext(Dispatchers.Main) { + onLoaded(emptyList(), LogPageInfo(), lastHash) + } + } + } +} + +private suspend fun clearLogs() { + withContext(Dispatchers.IO) { + try { + val shell = getRootShell() + runCmd(shell, "echo '' > $LOGS_PATCH") + } catch (_: Exception) { + } + } +} + +private fun parseLogEntries(logContent: String): List { + if (logContent.isBlank()) return emptyList() + + val entries = logContent.lines() + .filter { it.isNotBlank() && it.startsWith("[") } + .mapNotNull { line -> + try { + parseLogLine(line) + } catch (_: Exception) { + null + } + } + + return entries.reversed() +} +private fun utcToLocal(utc: String): String { + return try { + val instant = LocalDateTime.parse(utc, utcFormatter).atOffset(ZoneOffset.UTC).toInstant() + val local = instant.atZone(ZoneId.systemDefault()) + local.format(localFormatter) + } catch (_: Exception) { + utc + } +} + +private fun parseLogLine(line: String): LogEntry? { + // 解析格式: [timestamp] TYPE: UID=xxx COMM=xxx ... + val timestampRegex = """\[(.*?)]""".toRegex() + val timestampMatch = timestampRegex.find(line) ?: return null + val timestamp = utcToLocal(timestampMatch.groupValues[1]) + + val afterTimestamp = line.substring(timestampMatch.range.last + 1).trim() + val parts = afterTimestamp.split(":") + if (parts.size < 2) return null + + val typeStr = parts[0].trim() + val type = when (typeStr) { + "SU_GRANT" -> LogType.SU_GRANT + "SU_EXEC" -> LogType.SU_EXEC + "PERM_CHECK" -> LogType.PERM_CHECK + "SYSCALL" -> LogType.SYSCALL + "MANAGER_OP" -> LogType.MANAGER_OP + else -> LogType.UNKNOWN + } + + val details = parts[1].trim() + val uid: String = extractValue(details, "UID") ?: "" + val comm: String = extractValue(details, "COMM") ?: "" + val pid: String = extractValue(details, "PID") ?: "" + + // 构建详细信息字符串 + val detailsStr = when (type) { + LogType.SU_GRANT -> { + val method: String = extractValue(details, "METHOD") ?: "" + "Method: $method" + } + LogType.SU_EXEC -> { + val target: String = extractValue(details, "TARGET") ?: "" + val result: String = extractValue(details, "RESULT") ?: "" + "Target: $target, Result: $result" + } + LogType.PERM_CHECK -> { + val result: String = extractValue(details, "RESULT") ?: "" + "Result: $result" + } + LogType.SYSCALL -> { + val syscall = extractValue(details, "SYSCALL") ?: "" + val args = extractValue(details, "ARGS") ?: "" + "Syscall: $syscall, Args: $args" + } + LogType.MANAGER_OP -> { + val op: String = extractValue(details, "OP") ?: "" + val managerUid: String = extractValue(details, "MANAGER_UID") ?: "" + val targetUid: String = extractValue(details, "TARGET_UID") ?: "" + "Operation: $op, Manager UID: $managerUid, Target UID: $targetUid" + } + else -> details + } + + return LogEntry( + timestamp = timestamp, + type = type, + uid = uid, + comm = comm, + details = detailsStr, + pid = pid, + rawLine = line + ) +} + +private fun extractValue(text: String, key: String): String? { + val regex = """$key=(\S+)""".toRegex() + return regex.find(text)?.groupValues?.get(1) +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt index 3f4cead..0c203cb 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Module.kt @@ -75,6 +75,10 @@ import com.sukisu.ultra.ui.component.* import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation import com.sukisu.ultra.ui.util.* +import com.sukisu.ultra.ui.util.module.ModuleModify +import com.sukisu.ultra.ui.util.module.ModuleOperationUtils +import com.sukisu.ultra.ui.util.module.ModuleUtils +import com.sukisu.ultra.ui.util.module.verifyModuleSignature import com.sukisu.ultra.ui.viewmodel.ModuleViewModel import com.sukisu.ultra.ui.webui.WebUIActivity import com.sukisu.ultra.ui.webui.WebUIXActivity @@ -84,7 +88,6 @@ import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import java.util.concurrent.TimeUnit -// 菜单项数据类 data class ModuleBottomSheetMenuItem( val icon: ImageVector, val titleRes: Int, @@ -93,7 +96,7 @@ data class ModuleBottomSheetMenuItem( /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ @SuppressLint("ResourceType", "AutoboxingStateCreation") @OptIn(ExperimentalMaterial3Api::class) @@ -102,24 +105,21 @@ data class ModuleBottomSheetMenuItem( fun ModuleScreen(navigator: DestinationsNavigator) { val viewModel = viewModel() val context = LocalContext.current - val prefs = context.getSharedPreferences("settings",MODE_PRIVATE) + val prefs = context.getSharedPreferences("settings", MODE_PRIVATE) val snackBarHost = LocalSnackbarHost.current val scope = rememberCoroutineScope() val confirmDialog = rememberConfirmDialog() var lastClickTime by remember { mutableStateOf(0L) } - // 签名验证弹窗状态 var showSignatureDialog by remember { mutableStateOf(false) } var signatureDialogMessage by remember { mutableStateOf("") } var isForceVerificationFailed by remember { mutableStateOf(false) } var pendingInstallAction by remember { mutableStateOf<(() -> Unit)?>(null) } - // 初始化缓存系统 LaunchedEffect(Unit) { viewModel.initializeCache(context) } - // BottomSheet状态 val bottomSheetState = rememberModalBottomSheetState( skipPartiallyExpanded = true ) @@ -280,7 +280,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val backupLauncher = ModuleModify.rememberModuleBackupLauncher(context, snackBarHost) val restoreLauncher = ModuleModify.rememberModuleRestoreLauncher(context, snackBarHost) - LaunchedEffect(Unit) { if (viewModel.moduleList.isEmpty() || viewModel.isNeedRefresh) { viewModel.sortEnabledFirst = prefs.getBoolean("module_sort_enabled_first", false) @@ -291,7 +290,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { val isSafeMode = Natives.isSafeMode val hasMagisk = hasMagisk() - val hideInstallButton = isSafeMode || hasMagisk val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) @@ -300,7 +298,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { contract = ActivityResultContracts.StartActivityForResult() ) { viewModel.fetchModuleList() } - // BottomSheet菜单项 val bottomSheetMenuItems = remember { listOf( ModuleBottomSheetMenuItem( @@ -478,7 +475,6 @@ fun ModuleScreen(navigator: DestinationsNavigator) { } } - // BottomSheet if (showBottomSheet) { ModalBottomSheet( onDismissRequest = { @@ -620,7 +616,6 @@ private fun ModuleBottomSheetContent( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) - // 排序选项 Column( modifier = Modifier.padding(horizontal = 24.dp), verticalArrangement = Arrangement.spacedBy(8.dp) @@ -682,7 +677,6 @@ private fun ModuleBottomSheetContent( @Composable private fun ModuleBottomSheetMenuItemView(menuItem: ModuleBottomSheetMenuItem) { - // 添加交互状态 val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -810,7 +804,6 @@ private fun ModuleList( return } - // changelog is not empty, show it and wait for confirm val confirmResult = confirmDialog.awaitConfirm( changelogText, content = changelog, @@ -901,6 +894,7 @@ private fun ModuleList( reboot() } } + PullToRefreshBox( modifier = boxModifier, onRefresh = { @@ -1003,7 +997,6 @@ private fun ModuleList( } ) - // fix last item shadow incomplete in LazyColumn Spacer(Modifier.height(1.dp)) } } @@ -1011,7 +1004,6 @@ private fun ModuleList( } DownloadListener(context, onInstallModule) - } } @@ -1044,7 +1036,6 @@ fun ModuleItem( val indication = LocalIndication.current val viewModel = viewModel() - // 使用缓存系统获取模块大小 val sizeStr = remember(module.dirId) { viewModel.getModuleSize(module.dirId) } @@ -1152,10 +1143,8 @@ fun ModuleItem( modifier = Modifier .fillMaxWidth() .combinedClickable( - onClick = { - }, + onClick = { }, onLongClick = { - // 长按复制updateJson地址 val clipData = ClipData.newPlainText( "Update JSON URL", module.updateJson @@ -1163,7 +1152,6 @@ fun ModuleItem( clipboardManager.setPrimaryClip(clipData) hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - // 显示复制成功的提示 Toast.makeText( context, context.getString(R.string.module_update_json_copied), @@ -1202,8 +1190,8 @@ fun ModuleItem( maxLines = 4, textDecoration = textDecoration, ) - if (!isHideTagRow) { + if (!isHideTagRow) { Spacer(modifier = Modifier.height(12.dp)) // 文件夹名称和大小标签 Row( @@ -1276,8 +1264,7 @@ fun ModuleItem( onClick = { onClick(module) }, interactionSource = interactionSource, contentPadding = ButtonDefaults.TextButtonContentPadding, - - ) { + ) { Icon( modifier = Modifier.size(20.dp), imageVector = Icons.AutoMirrored.Outlined.Wysiwyg, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt index 1b3c7a0..2828e3a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Settings.kt @@ -2,6 +2,7 @@ package com.sukisu.ultra.ui.screen import android.content.Context import android.content.Intent +import android.content.SharedPreferences import android.net.Uri import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult @@ -10,12 +11,18 @@ import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowForward import androidx.compose.material.icons.automirrored.filled.Undo import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.rounded.EnhancedEncryption +import androidx.compose.material.icons.rounded.FolderDelete +import androidx.compose.material.icons.rounded.RemoveCircle +import androidx.compose.material.icons.rounded.RemoveModerator import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable @@ -36,6 +43,8 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.generated.destinations.AppProfileTemplateScreenDestination import com.ramcosta.composedestinations.generated.destinations.FlashScreenDestination +import com.ramcosta.composedestinations.generated.destinations.LogViewerScreenDestination +import com.ramcosta.composedestinations.generated.destinations.UmountManagerScreenDestination import com.ramcosta.composedestinations.generated.destinations.MoreSettingsScreenDestination import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.BuildConfig @@ -46,12 +55,8 @@ import com.sukisu.ultra.ui.theme.CardConfig import com.sukisu.ultra.ui.theme.CardConfig.cardAlpha import com.sukisu.ultra.ui.theme.getCardColors import com.sukisu.ultra.ui.theme.getCardElevation -import com.sukisu.ultra.ui.util.LocalSnackbarHost -import com.sukisu.ultra.ui.util.getBugreportFile -import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.setUidAutoScan -import com.sukisu.ultra.ui.util.setUidMultiUserScan -import com.topjohnwu.superuser.ShellUtils +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -60,7 +65,7 @@ import java.time.format.DateTimeFormatter /** * @author ShirkNeko - * @date 2025/5/31. + * @date 2025/9/29. */ private val SPACING_SMALL = 3.dp private val SPACING_MEDIUM = 8.dp @@ -81,7 +86,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } Scaffold( - // containerColor = MaterialTheme.colorScheme.surfaceBright, topBar = { TopBar(scrollBehavior = scrollBehavior) }, @@ -132,41 +136,155 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) - // 卸载模块开关 - var umountChecked by rememberSaveable { - mutableStateOf(Natives.isDefaultUmountModules()) + val modeItems = listOf( + stringResource(id = R.string.settings_mode_default), + stringResource(id = R.string.settings_mode_temp_enable), + stringResource(id = R.string.settings_mode_always_enable), + ) + var enhancedSecurityMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isEnhancedSecurityEnabled() + val savedPersist = prefs.getInt("enhanced_security_mode", 0) + if (savedPersist == 2) 2 else if (currentEnabled) 1 else 0 + } + ) } + SuperDropdown( + icon = Icons.Rounded.EnhancedEncryption, + title = stringResource(id = R.string.settings_enable_enhanced_security), + summary = stringResource(id = R.string.settings_enable_enhanced_security_summary), + items = modeItems, + selectedIndex = enhancedSecurityMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: disable and save to persist + 0 -> if (Natives.setEnhancedSecurityEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("enhanced_security_mode", 0) } + enhancedSecurityMode = 0 + } - SwitchItem( - icon = Icons.Filled.FolderDelete, - title = stringResource(R.string.settings_umount_modules_default), - summary = stringResource(R.string.settings_umount_modules_default_summary), - checked = umountChecked, - onCheckedChange = { enabled -> - if (Natives.setDefaultUmountModules(enabled)) { - umountChecked = enabled + // Temporarily enable: save disabled state first, then enable + 1 -> if (Natives.setEnhancedSecurityEnabled(false)) { + execKsud("feature save", true) + if (Natives.setEnhancedSecurityEnabled(true)) { + prefs.edit { putInt("enhanced_security_mode", 0) } + enhancedSecurityMode = 1 + } + } + + // Permanently enable: enable and save + 2 -> if (Natives.setEnhancedSecurityEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("enhanced_security_mode", 2) } + enhancedSecurityMode = 2 + } } } ) - // SU 禁用开关 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_SU_COMPAT) { - var isSuDisabled by rememberSaveable { - mutableStateOf(!Natives.isSuEnabled()) - } - SwitchItem( - icon = Icons.Filled.RemoveModerator, - title = stringResource(R.string.settings_disable_su), - summary = stringResource(R.string.settings_disable_su_summary), - checked = isSuDisabled, - onCheckedChange = { enabled -> - val shouldEnable = !enabled - if (Natives.setSuEnabled(shouldEnable)) { - isSuDisabled = enabled - } + var suCompatMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isSuEnabled() + val savedPersist = prefs.getInt("su_compat_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 } ) } + SuperDropdown( + icon = Icons.Rounded.RemoveModerator, + title = stringResource(id = R.string.settings_disable_su), + summary = stringResource(id = R.string.settings_disable_su_summary), + items = modeItems, + selectedIndex = suCompatMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setSuEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("su_compat_mode", 0) } + suCompatMode = 0 + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setSuEnabled(true)) { + execKsud("feature save", true) + if (Natives.setSuEnabled(false)) { + prefs.edit { putInt("su_compat_mode", 0) } + suCompatMode = 1 + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setSuEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("su_compat_mode", 2) } + suCompatMode = 2 + } + } + } + ) + + var kernelUmountMode by rememberSaveable { + mutableIntStateOf( + run { + val currentEnabled = Natives.isKernelUmountEnabled() + val savedPersist = prefs.getInt("kernel_umount_mode", 0) + if (savedPersist == 2) 2 else if (!currentEnabled) 1 else 0 + } + ) + } + SuperDropdown( + icon = Icons.Rounded.RemoveCircle, + title = stringResource(id = R.string.settings_disable_kernel_umount), + summary = stringResource(id = R.string.settings_disable_kernel_umount_summary), + items = modeItems, + selectedIndex = kernelUmountMode, + onSelectedIndexChange = { index -> + when (index) { + // Default: enable and save to persist + 0 -> if (Natives.setKernelUmountEnabled(true)) { + execKsud("feature save", true) + prefs.edit { putInt("kernel_umount_mode", 0) } + kernelUmountMode = 0 + } + + // Temporarily disable: save enabled state first, then disable + 1 -> if (Natives.setKernelUmountEnabled(true)) { + execKsud("feature save", true) + if (Natives.setKernelUmountEnabled(false)) { + prefs.edit { putInt("kernel_umount_mode", 0) } + kernelUmountMode = 1 + } + } + + // Permanently disable: disable and save + 2 -> if (Natives.setKernelUmountEnabled(false)) { + execKsud("feature save", true) + prefs.edit { putInt("kernel_umount_mode", 2) } + kernelUmountMode = 2 + } + } + } + ) + + // 卸载模块开关 + var umountChecked by rememberSaveable { mutableStateOf(Natives.isDefaultUmountModules()) } + SwitchItem( + icon = Icons.Rounded.FolderDelete, + title = stringResource(id = R.string.settings_umount_modules_default), + summary = stringResource(id = R.string.settings_umount_modules_default_summary), + checked = umountChecked, + onCheckedChange = { + if (Natives.setDefaultUmountModules(it)) { + umountChecked = it + } + } + ) + + // 强制签名验证开关 var forceSignatureVerification by rememberSaveable { mutableStateOf(prefs.getBoolean("force_signature_verification", false)) @@ -181,115 +299,9 @@ fun SettingScreen(navigator: DestinationsNavigator) { forceSignatureVerification = enabled } ) - if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER) { - var uidAutoScanEnabled by rememberSaveable { - mutableStateOf(prefs.getBoolean("uid_auto_scan", false)) - } - - var uidMultiUserScanEnabled by rememberSaveable { - mutableStateOf(prefs.getBoolean("uid_multi_user_scan", false)) - } - // 用户态扫描应用列表开关 - SwitchItem( - icon = Icons.Filled.Scanner, - title = stringResource(R.string.uid_auto_scan_title), - summary = stringResource(R.string.uid_auto_scan_summary), - checked = uidAutoScanEnabled, - onCheckedChange = { enabled -> - scope.launch { - try { - if (setUidAutoScan(enabled)) { - uidAutoScanEnabled = enabled - prefs.edit { putBoolean("uid_auto_scan", enabled) } - - if (!enabled) { - uidMultiUserScanEnabled = false - prefs.edit { putBoolean("uid_multi_user_scan", false) } - } - } else { - snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed)) - } - } catch (e: Exception) { - snackBarHost.showSnackbar( - context.getString( - R.string.uid_scanner_setting_error, - e.message ?: "" - ) - ) - } - } - } - ) - - // 多用户应用扫描开关 - 仅在启用用户态扫描时显示 - AnimatedVisibility( - visible = uidAutoScanEnabled, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SwitchItem( - icon = Icons.Filled.Groups, - title = stringResource(R.string.uid_multi_user_scan_title), - summary = stringResource(R.string.uid_multi_user_scan_summary), - checked = uidMultiUserScanEnabled, - onCheckedChange = { enabled -> - scope.launch { - try { - if (setUidMultiUserScan(enabled)) { - uidMultiUserScanEnabled = enabled - prefs.edit { putBoolean("uid_multi_user_scan", enabled) } - } else { - snackBarHost.showSnackbar(context.getString(R.string.uid_scanner_setting_failed)) - } - } catch (e: Exception) { - snackBarHost.showSnackbar( - context.getString( - R.string.uid_scanner_setting_error, - e.message ?: "" - ) - ) - } - } - } - ) - } - // 清理运行环境 - AnimatedVisibility( - visible = uidAutoScanEnabled, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - val confirmDialog = rememberConfirmDialog() - val scope = rememberCoroutineScope() - - SettingItem( - icon = Icons.Filled.CleaningServices, - title = stringResource(R.string.clean_runtime_environment), - summary = stringResource(R.string.clean_runtime_environment_summary), - onClick = { - scope.launch { - val result = confirmDialog.awaitConfirm( - title = context.getString(R.string.clean_runtime_environment), - content = context.getString(R.string.clean_runtime_environment_confirm) - ) - if (result == ConfirmResult.Confirmed) { - val cleanResult = cleanRuntimeEnvironment() - if (cleanResult) { - uidAutoScanEnabled = false - prefs.edit { putBoolean("uid_auto_scan", false) } - - uidMultiUserScanEnabled = false - prefs.edit { putBoolean("uid_multi_user_scan", false) } - - snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_success)) - } else { - snackBarHost.showSnackbar(context.getString(R.string.clean_runtime_environment_failed)) - } - } - } - } - ) - } + // UID 扫描开关 + if (Natives.version >= Natives.MINIMAL_SUPPORTED_UID_SCANNER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { + UidScannerSection(prefs, snackBarHost, scope, context) } } ) @@ -389,6 +401,31 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) + // 查看使用日志 + KsuIsValid { + SettingItem( + icon = Icons.Filled.Visibility, + title = stringResource(R.string.log_viewer_view_logs), + summary = stringResource(R.string.log_viewer_view_logs_summary), + onClick = { + navigator.navigate(LogViewerScreenDestination) + } + ) + } + val lkmMode = Natives.isLkmMode + KsuIsValid { + if (lkmMode) { + SettingItem( + icon = Icons.Filled.FolderOff, + title = stringResource(R.string.umount_path_manager), + summary = stringResource(R.string.umount_path_manager_summary), + onClick = { + navigator.navigate(UmountManagerScreenDestination) + } + ) + } + } + if (showBottomsheet) { LogBottomSheet( onDismiss = { showBottomsheet = false }, @@ -430,8 +467,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } ) } - - val lkmMode = Natives.version >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && Natives.isLkmMode if (lkmMode) { UninstallItem(navigator) { loadingDialog.withLoading(it) @@ -459,23 +494,6 @@ fun SettingScreen(navigator: DestinationsNavigator) { } } -fun cleanRuntimeEnvironment(): Boolean { - val shell = getRootShell() - return try { - try { - ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop") - } catch (_: Exception) { - } - ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid") - ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh") - true - } catch (_: Exception) { - false - } -} - @Composable private fun SettingsGroupCard( title: String, @@ -781,7 +799,6 @@ enum class UninstallType(val title: Int, val message: Int, val icon: ImageVector fun rememberUninstallDialog(onSelected: (UninstallType) -> Unit): DialogHandle { return rememberCustomDialog { dismiss -> val options = listOf( - // UninstallType.TEMPORARY, UninstallType.PERMANENT, UninstallType.RESTORE_STOCK_IMAGE ) @@ -938,4 +955,125 @@ private fun TopBar( windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) -} \ No newline at end of file +} + +@Composable +private fun UidScannerSection( + prefs: SharedPreferences, + snackBarHost: SnackbarHostState, + scope: CoroutineScope, + context: Context +) { + if (Natives.version < Natives.MINIMAL_SUPPORTED_UID_SCANNER) return + + val realAuto = Natives.isUidScannerEnabled() + val realMulti = getUidMultiUserScan() + + var autoOn by remember { mutableStateOf(realAuto) } + var multiOn by remember { mutableStateOf(realMulti) } + + LaunchedEffect(Unit) { + autoOn = realAuto + multiOn = realMulti + prefs.edit { + putBoolean("uid_auto_scan", autoOn) + putBoolean("uid_multi_user_scan", multiOn) + } + } + + SwitchItem( + icon = Icons.Filled.Scanner, + title = stringResource(R.string.uid_auto_scan_title), + summary = stringResource(R.string.uid_auto_scan_summary), + checked = autoOn, + onCheckedChange = { target -> + autoOn = target + if (!target) multiOn = false + + scope.launch(Dispatchers.IO) { + setUidAutoScan(target) + val actual = Natives.isUidScannerEnabled() || readUidScannerFile() + withContext(Dispatchers.Main) { + autoOn = actual + if (!actual) multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", actual) + putBoolean("uid_multi_user_scan", multiOn) + } + if (actual != target) { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + SwitchItem( + icon = Icons.Filled.Groups, + title = stringResource(R.string.uid_multi_user_scan_title), + summary = stringResource(R.string.uid_multi_user_scan_summary), + checked = multiOn, + onCheckedChange = { target -> + scope.launch(Dispatchers.IO) { + val ok = setUidMultiUserScan(target) + withContext(Dispatchers.Main) { + if (ok) { + multiOn = target + prefs.edit { putBoolean("uid_multi_user_scan", target) } + } else { + snackBarHost.showSnackbar( + context.getString(R.string.uid_scanner_setting_failed) + ) + } + } + } + } + ) + } + + AnimatedVisibility( + visible = autoOn, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + val confirmDialog = rememberConfirmDialog() + SettingItem( + icon = Icons.Filled.CleaningServices, + title = stringResource(R.string.clean_runtime_environment), + summary = stringResource(R.string.clean_runtime_environment_summary), + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.clean_runtime_environment), + content = context.getString(R.string.clean_runtime_environment_confirm) + ) == ConfirmResult.Confirmed + ) { + if (cleanRuntimeEnvironment()) { + autoOn = false + multiOn = false + prefs.edit { + putBoolean("uid_auto_scan", false) + putBoolean("uid_multi_user_scan", false) + } + Natives.setUidScannerEnabled(false) + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_success) + ) + } else { + snackBarHost.showSnackbar( + context.getString(R.string.clean_runtime_environment_failed) + ) + } + } + } + } + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt index e2434b7..489cb54 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuperUser.kt @@ -1,9 +1,11 @@ package com.sukisu.ultra.ui.screen import android.annotation.SuppressLint -import androidx.compose.animation.* +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.* -import androidx.compose.foundation.background +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.* import androidx.compose.foundation.clickable import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.interaction.MutableInteractionSource @@ -22,8 +24,18 @@ import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior import androidx.compose.material3.pulltorefresh.PullToRefreshBox -import androidx.compose.runtime.* +import androidx.compose.material3.rememberModalBottomSheetState +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.rememberCoroutineScope +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -33,12 +45,10 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.TextStyle 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.dp -import androidx.compose.ui.unit.sp import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage import coil.request.ImageRequest @@ -53,64 +63,23 @@ import com.sukisu.ultra.R import com.sukisu.ultra.ui.component.FabMenuPresets import com.sukisu.ultra.ui.component.SearchAppBar import com.sukisu.ultra.ui.component.VerticalExpandableFab -import com.sukisu.ultra.ui.util.ModuleModify +import com.sukisu.ultra.ui.util.module.ModuleModify import com.sukisu.ultra.ui.viewmodel.AppCategory import com.sukisu.ultra.ui.viewmodel.SortType import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import java.io.File -// 应用优先级枚举 enum class AppPriority(val value: Int) { - ROOT(1), // root权限应用 - CUSTOM(2), // 自定义应用 - DEFAULT(3) // 默认应用 + ROOT(1), CUSTOM(2), DEFAULT(3) } -// 菜单项数据类 data class BottomSheetMenuItem( val icon: ImageVector, val titleRes: Int, val onClick: () -> Unit ) -/** - * 获取应用的优先级 - */ -private fun getAppPriority(app: SuperUserViewModel.AppInfo): AppPriority { - return when { - app.allowSu -> AppPriority.ROOT - app.hasCustomProfile -> AppPriority.CUSTOM - else -> AppPriority.DEFAULT - } -} - -/** - * 获取多选模式的主按钮图标 - */ -private fun getMultiSelectMainIcon(isExpanded: Boolean): ImageVector { - return if (isExpanded) { - Icons.Filled.Close - } else { - Icons.Filled.GridView - } -} - -/** - * 获取单选模式的主按钮图标 - */ -private fun getSingleSelectMainIcon(isExpanded: Boolean): ImageVector { - return if (isExpanded) { - Icons.Filled.Close - } else { - Icons.Filled.Add - } -} - -/** - * @author ShirkNeko - * @date 2025/6/8 - */ @OptIn(ExperimentalMaterialApi::class, ExperimentalMaterial3Api::class) @Destination @Composable @@ -122,231 +91,90 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { val context = LocalContext.current val snackBarHostState = remember { SnackbarHostState() } - // 使用ViewModel中的状态,这些状态现在都会从SharedPreferences中加载并自动保存 - val selectedCategory = viewModel.selectedCategory - val currentSortType = viewModel.currentSortType - - // BottomSheet状态 - val bottomSheetState = rememberModalBottomSheetState( - skipPartiallyExpanded = true - ) + val bottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var showBottomSheet by remember { mutableStateOf(false) } - // 添加备份和还原启动器 val backupLauncher = ModuleModify.rememberAllowlistBackupLauncher(context, snackBarHostState) val restoreLauncher = ModuleModify.rememberAllowlistRestoreLauncher(context, snackBarHostState) - LaunchedEffect(key1 = navigator) { + LaunchedEffect(navigator) { viewModel.search = "" - if (viewModel.appList.isEmpty()) { - // viewModel.fetchAppList() - } } - LaunchedEffect(viewModel.search) { - if (viewModel.search.isEmpty()) { - // 取消自动滚动到顶部的行为 - // listState.scrollToItem(0) - } - } - - // 监听选中应用的变化,如果在多选模式下没有选中任何应用,则自动退出多选模式 LaunchedEffect(viewModel.selectedApps, viewModel.showBatchActions) { if (viewModel.showBatchActions && viewModel.selectedApps.isEmpty()) { viewModel.showBatchActions = false } } - // 应用分类和排序逻辑 - val filteredAndSortedApps = remember( - viewModel.appList, - selectedCategory, - currentSortType, + val filteredAndSortedAppGroups = remember( + viewModel.appGroupList, + viewModel.selectedCategory, + viewModel.currentSortType, viewModel.search, viewModel.showSystemApps ) { - var apps = viewModel.appList + var groups = viewModel.appGroupList // 按分类筛选 - apps = when (selectedCategory) { - AppCategory.ALL -> apps - AppCategory.ROOT -> apps.filter { it.allowSu } - AppCategory.CUSTOM -> apps.filter { !it.allowSu && it.hasCustomProfile } - AppCategory.DEFAULT -> apps.filter { !it.allowSu && !it.hasCustomProfile } + groups = when (viewModel.selectedCategory) { + AppCategory.ALL -> groups + AppCategory.ROOT -> groups.filter { it.allowSu } + AppCategory.CUSTOM -> groups.filter { !it.allowSu && it.hasCustomProfile } + AppCategory.DEFAULT -> groups.filter { !it.allowSu && !it.hasCustomProfile } } - // 优先级排序 + 二次排序 - apps = apps.sortedWith { app1, app2 -> - val priority1 = getAppPriority(app1) - val priority2 = getAppPriority(app2) + // 排序 + groups.sortedWith { group1, group2 -> + val priority1 = when { + group1.allowSu -> AppPriority.ROOT + group1.hasCustomProfile -> AppPriority.CUSTOM + else -> AppPriority.DEFAULT + } + val priority2 = when { + group2.allowSu -> AppPriority.ROOT + group2.hasCustomProfile -> AppPriority.CUSTOM + else -> AppPriority.DEFAULT + } - // 首先按优先级排序 val priorityComparison = priority1.value.compareTo(priority2.value) - if (priorityComparison != 0) { priorityComparison } else { - // 在相同优先级内按指定排序方式排序 - when (currentSortType) { - SortType.NAME_ASC -> app1.label.lowercase().compareTo(app2.label.lowercase()) - SortType.NAME_DESC -> app2.label.lowercase().compareTo(app1.label.lowercase()) - SortType.INSTALL_TIME_NEW -> app2.packageInfo.firstInstallTime.compareTo(app1.packageInfo.firstInstallTime) - SortType.INSTALL_TIME_OLD -> app1.packageInfo.firstInstallTime.compareTo(app2.packageInfo.firstInstallTime) - SortType.SIZE_DESC -> { - val size1: Long = app1.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - val size2: Long = app2.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - size2.compareTo(size1) - } - SortType.SIZE_ASC -> { - val size1: Long = app1.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - val size2: Long = app2.packageInfo.applicationInfo?.let { - try { - File(context.packageManager.getApplicationInfo(it.packageName, 0).sourceDir).length() - } catch (_: Exception) { - 0L - } - } ?: 0L - size1.compareTo(size2) - } - SortType.USAGE_FREQ -> app1.label.lowercase().compareTo(app2.label.lowercase()) // 默认按名称排序 + when (viewModel.currentSortType) { + SortType.NAME_ASC -> group1.mainApp.label.lowercase() + .compareTo(group2.mainApp.label.lowercase()) + SortType.NAME_DESC -> group2.mainApp.label.lowercase() + .compareTo(group1.mainApp.label.lowercase()) + SortType.INSTALL_TIME_NEW -> group2.mainApp.packageInfo.firstInstallTime + .compareTo(group1.mainApp.packageInfo.firstInstallTime) + SortType.INSTALL_TIME_OLD -> group1.mainApp.packageInfo.firstInstallTime + .compareTo(group2.mainApp.packageInfo.firstInstallTime) + else -> group1.mainApp.label.lowercase() + .compareTo(group2.mainApp.label.lowercase()) } } } - - apps } - // 计算应用数量 - val appCounts = remember(viewModel.appList, viewModel.showSystemApps) { + val appCounts = remember(viewModel.appGroupList, viewModel.showSystemApps) { mapOf( - AppCategory.ALL to viewModel.appList.size, - AppCategory.ROOT to viewModel.appList.count { it.allowSu }, - AppCategory.CUSTOM to viewModel.appList.count { !it.allowSu && it.hasCustomProfile }, - AppCategory.DEFAULT to viewModel.appList.count { !it.allowSu && !it.hasCustomProfile } + AppCategory.ALL to viewModel.appGroupList.size, + AppCategory.ROOT to viewModel.appGroupList.count { it.allowSu }, + AppCategory.CUSTOM to viewModel.appGroupList.count { !it.allowSu && it.hasCustomProfile }, + AppCategory.DEFAULT to viewModel.appGroupList.count { !it.allowSu && !it.hasCustomProfile } ) } - // BottomSheet菜单项 - val bottomSheetMenuItems = remember(viewModel.showSystemApps) { - listOf( - BottomSheetMenuItem( - icon = Icons.Filled.Refresh, - titleRes = R.string.refresh, - onClick = { - scope.launch { - viewModel.fetchAppList() - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, - titleRes = if (viewModel.showSystemApps) { - R.string.hide_system_apps - } else { - R.string.show_system_apps - }, - onClick = { - viewModel.updateShowSystemApps(!viewModel.showSystemApps) - scope.launch { - kotlinx.coroutines.delay(100) - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.Save, - titleRes = R.string.backup_allowlist, - onClick = { - backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ), - BottomSheetMenuItem( - icon = Icons.Filled.RestoreFromTrash, - titleRes = R.string.restore_allowlist, - onClick = { - restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false - } - } - ) - ) - } - - // 记录FAB展开状态用于图标动画 - var isFabExpanded by remember { mutableStateOf(false) } - Scaffold( topBar = { SearchAppBar( - title = { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(stringResource(R.string.superuser)) - // 显示当前分类和应用数量 - if (selectedCategory != AppCategory.ALL) { - Surface( - shape = RoundedCornerShape(12.dp), - color = MaterialTheme.colorScheme.primaryContainer, - modifier = Modifier.padding(start = 4.dp) - ) { - Row( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(selectedCategory.displayNameRes), - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - Text( - text = "(${appCounts[selectedCategory] ?: 0})", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onPrimaryContainer - ) - } - } - } - } - }, + title = { TopBarTitle(viewModel.selectedCategory, appCounts) }, searchText = viewModel.search, onSearchTextChange = { viewModel.search = it }, onClearClick = { viewModel.search = "" }, dropdownContent = { - IconButton( - onClick = { - showBottomSheet = true - }, - ) { + IconButton(onClick = { showBottomSheet = true }) { Icon( imageVector = Icons.Filled.MoreVert, contentDescription = stringResource(id = R.string.settings), @@ -359,179 +187,326 @@ fun SuperUserScreen(navigator: DestinationsNavigator) { snackbarHost = { SnackbarHost(snackBarHostState) }, contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), floatingActionButton = { - VerticalExpandableFab( - menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - FabMenuPresets.getBatchActionMenuItems( - onCancel = { - viewModel.selectedApps = emptySet() - viewModel.showBatchActions = false - }, - onDeny = { - scope.launch { - viewModel.updateBatchPermissions(false) - } - }, - onAllow = { - scope.launch { - viewModel.updateBatchPermissions(true) - } - }, - onUnmountModules = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, - umountModules = true - ) - } - }, - onDisableUnmount = { - scope.launch { - viewModel.updateBatchPermissions( - allowSu = false, - umountModules = false - ) - } - } - ) - } else { - FabMenuPresets.getScrollMenuItems( - onScrollToTop = { - scope.launch { - listState.animateScrollToItem(0) - } - }, - onScrollToBottom = { - scope.launch { - val lastIndex = filteredAndSortedApps.size - 1 - if (lastIndex >= 0) { - listState.animateScrollToItem(lastIndex) - } - } - } - ) - }, - buttonSpacing = 72.dp, - animationDurationMs = 300, - staggerDelayMs = 50, - // 根据模式选择不同的图标 - mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { - getMultiSelectMainIcon(isFabExpanded) - } else { - getSingleSelectMainIcon(isFabExpanded) - }, - mainButtonExpandedIcon = Icons.Filled.Close - ) + SuperUserFab(viewModel, filteredAndSortedAppGroups, listState, scope) } ) { innerPadding -> - PullToRefreshBox( - modifier = Modifier.padding(innerPadding), - onRefresh = { - scope.launch { viewModel.fetchAppList() } - }, - isRefreshing = viewModel.isRefreshing - ) { - LazyColumn( - state = listState, - modifier = Modifier - .fillMaxSize() - .nestedScroll(scrollBehavior.nestedScrollConnection) + SuperUserContent( + innerPadding = innerPadding, + viewModel = viewModel, + filteredAndSortedAppGroups = filteredAndSortedAppGroups, + listState = listState, + scrollBehavior = scrollBehavior, + navigator = navigator, + scope = scope + ) + + if (showBottomSheet) { + SuperUserBottomSheet( + bottomSheetState = bottomSheetState, + onDismiss = { showBottomSheet = false }, + viewModel = viewModel, + appCounts = appCounts, + backupLauncher = backupLauncher, + restoreLauncher = restoreLauncher, + scope = scope, + listState = listState + ) + } + } +} + +@Composable +private fun TopBarTitle( + selectedCategory: AppCategory, + appCounts: Map +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(stringResource(R.string.superuser)) + + if (selectedCategory != AppCategory.ALL) { + Surface( + shape = RoundedCornerShape(12.dp), + color = MaterialTheme.colorScheme.primaryContainer, + modifier = Modifier.padding(start = 4.dp) ) { - items(filteredAndSortedApps, key = { it.packageName + it.uid }) { app -> - AppItem( - app = app, - isSelected = viewModel.selectedApps.contains(app.packageName), - onToggleSelection = { viewModel.toggleAppSelection(app.packageName) }, + Row( + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + Text( + text = stringResource(selectedCategory.displayNameRes), + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + Text( + text = "(${appCounts[selectedCategory] ?: 0})", + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + } + } +} + +@Composable +private fun SuperUserFab( + viewModel: SuperUserViewModel, + filteredAndSortedAppGroups: List, + listState: androidx.compose.foundation.lazy.LazyListState, + scope: CoroutineScope +) { + VerticalExpandableFab( + menuItems = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + FabMenuPresets.getBatchActionMenuItems( + onCancel = { + viewModel.selectedApps = emptySet() + viewModel.showBatchActions = false + }, + onDeny = { scope.launch { viewModel.updateBatchPermissions(false) } }, + onAllow = { scope.launch { viewModel.updateBatchPermissions(true) } }, + onUnmountModules = { + scope.launch { viewModel.updateBatchPermissions( + allowSu = false, + umountModules = true + ) } + }, + onDisableUnmount = { + scope.launch { viewModel.updateBatchPermissions( + allowSu = false, + umountModules = false + ) } + } + ) + } else { + FabMenuPresets.getScrollMenuItems( + onScrollToTop = { scope.launch { listState.animateScrollToItem(0) } }, + onScrollToBottom = { + scope.launch { + val lastIndex = filteredAndSortedAppGroups.size - 1 + if (lastIndex >= 0) listState.animateScrollToItem(lastIndex) + } + } + ) + }, + mainButtonIcon = if (viewModel.showBatchActions && viewModel.selectedApps.isNotEmpty()) { + Icons.Filled.GridView + } else { + Icons.Filled.Add + }, + mainButtonExpandedIcon = Icons.Filled.Close + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SuperUserContent( + innerPadding: PaddingValues, + viewModel: SuperUserViewModel, + filteredAndSortedAppGroups: List, + listState: androidx.compose.foundation.lazy.LazyListState, + scrollBehavior: TopAppBarScrollBehavior, + navigator: DestinationsNavigator, + scope: CoroutineScope +) { + val expandedGroups = remember { mutableStateOf(setOf()) } + + PullToRefreshBox( + modifier = Modifier.padding(innerPadding), + onRefresh = { scope.launch { viewModel.fetchAppList() } }, + isRefreshing = viewModel.isRefreshing + ) { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + filteredAndSortedAppGroups.forEachIndexed { _, appGroup -> + item(key = "${appGroup.uid}-${appGroup.mainApp.packageName}") { + AppGroupItem( + appGroup = appGroup, + isSelected = appGroup.packageNames.any { viewModel.selectedApps.contains(it) }, + onToggleSelection = { + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } + }, onClick = { if (viewModel.showBatchActions) { - viewModel.toggleAppSelection(app.packageName) + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } + } else if (appGroup.apps.size > 1) { + expandedGroups.value = if (expandedGroups.value.contains(appGroup.uid)) { + expandedGroups.value - appGroup.uid + } else { + expandedGroups.value + appGroup.uid + } } else { - navigator.navigate(AppProfileScreenDestination(app)) + navigator.navigate(AppProfileScreenDestination(appGroup.mainApp)) } }, onLongClick = { if (!viewModel.showBatchActions) { viewModel.toggleBatchMode() - viewModel.toggleAppSelection(app.packageName) + appGroup.packageNames.forEach { viewModel.toggleAppSelection(it) } } }, viewModel = viewModel ) } - // 当没有应用显示时显示加载动画或空状态 - if (filteredAndSortedApps.isEmpty()) { - item { - Box( + items(appGroup.apps, key = { "${it.packageName}-${it.uid}" }) { app -> + AnimatedVisibility( + visible = expandedGroups.value.contains(appGroup.uid) && appGroup.apps.size > 1, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + ListItem( modifier = Modifier .fillMaxWidth() - .height(400.dp), - contentAlignment = Alignment.Center - ) { - // 根据加载状态显示不同内容 - if ((viewModel.isRefreshing || viewModel.appList.isEmpty()) && viewModel.search.isEmpty()) { - LoadingAnimation( - isLoading = true - ) - } else { - EmptyState( - selectedCategory = selectedCategory, - isSearchEmpty = viewModel.search.isNotEmpty() + .padding(start = 10.dp) + .clickable { + navigator.navigate(AppProfileScreenDestination(app)) + }, + headlineContent = { Text(app.label, style = MaterialTheme.typography.bodyMedium) }, + supportingContent = { Text(app.packageName, style = MaterialTheme.typography.bodySmall) }, + leadingContent = { + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(app.packageInfo) + .crossfade(true) + .build(), + contentDescription = app.label, + modifier = Modifier.padding(4.dp).width(36.dp).height(36.dp) ) } - } - } - } - } - } - - // BottomSheet - if (showBottomSheet) { - ModalBottomSheet( - onDismissRequest = { - showBottomSheet = false - }, - sheetState = bottomSheetState, - dragHandle = { - Surface( - modifier = Modifier.padding(vertical = 11.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), - shape = RoundedCornerShape(16.dp) - ) { - Box( - Modifier.size( - width = 32.dp, - height = 4.dp - ) ) } } - ) { - BottomSheetContent( - menuItems = bottomSheetMenuItems, - currentSortType = currentSortType, - onSortTypeChanged = { newSortType -> - viewModel.updateCurrentSortType(newSortType) - scope.launch { - bottomSheetState.hide() - showBottomSheet = false + } + + if (filteredAndSortedAppGroups.isEmpty()) { + item { + Box( + modifier = Modifier.fillMaxWidth().height(400.dp), + contentAlignment = Alignment.Center + ) { + if ((viewModel.isRefreshing || viewModel.appGroupList.isEmpty()) && viewModel.search.isEmpty()) { + LoadingAnimation(isLoading = true) + } else { + EmptyState( + selectedCategory = viewModel.selectedCategory, + isSearchEmpty = viewModel.search.isNotEmpty() + ) } - }, - selectedCategory = selectedCategory, - onCategorySelected = { newCategory -> - viewModel.updateSelectedCategory(newCategory) - scope.launch { - listState.animateScrollToItem(0) - bottomSheetState.hide() - showBottomSheet = false - } - }, - appCounts = appCounts - ) + } + } } } } } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun SuperUserBottomSheet( + bottomSheetState: SheetState, + onDismiss: () -> Unit, + viewModel: SuperUserViewModel, + appCounts: Map, + backupLauncher: androidx.activity.result.ActivityResultLauncher, + restoreLauncher: androidx.activity.result.ActivityResultLauncher, + scope: CoroutineScope, + listState: androidx.compose.foundation.lazy.LazyListState +) { + val bottomSheetMenuItems = remember(viewModel.showSystemApps) { + listOf( + BottomSheetMenuItem( + icon = Icons.Filled.Refresh, + titleRes = R.string.refresh, + onClick = { + scope.launch { + viewModel.fetchAppList() + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = if (viewModel.showSystemApps) Icons.Filled.VisibilityOff else Icons.Filled.Visibility, + titleRes = if (viewModel.showSystemApps) R.string.hide_system_apps else R.string.show_system_apps, + onClick = { + viewModel.updateShowSystemApps(!viewModel.showSystemApps) + scope.launch { + kotlinx.coroutines.delay(100) + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.Save, + titleRes = R.string.backup_allowlist, + onClick = { + backupLauncher.launch(ModuleModify.createAllowlistBackupIntent()) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + } + ), + BottomSheetMenuItem( + icon = Icons.Filled.RestoreFromTrash, + titleRes = R.string.restore_allowlist, + onClick = { + restoreLauncher.launch(ModuleModify.createAllowlistRestoreIntent()) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + } + ) + ) + } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = bottomSheetState, + dragHandle = { + Surface( + modifier = Modifier.padding(vertical = 11.dp), + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.4f), + shape = RoundedCornerShape(16.dp) + ) { + Box(Modifier.size(width = 32.dp, height = 4.dp)) + } + } + ) { + BottomSheetContent( + menuItems = bottomSheetMenuItems, + currentSortType = viewModel.currentSortType, + onSortTypeChanged = { newSortType -> + viewModel.updateCurrentSortType(newSortType) + scope.launch { + bottomSheetState.hide() + onDismiss() + } + }, + selectedCategory = viewModel.selectedCategory, + onCategorySelected = { newCategory -> + viewModel.updateSelectedCategory(newCategory) + scope.launch { + listState.animateScrollToItem(0) + bottomSheetState.hide() + onDismiss() + } + }, + appCounts = appCounts + ) + } +} + @Composable private fun BottomSheetContent( menuItems: List, @@ -542,11 +517,8 @@ private fun BottomSheetContent( appCounts: Map ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 24.dp) + modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp) ) { - // 标题 Text( text = stringResource(R.string.menu_options), style = MaterialTheme.typography.headlineSmall, @@ -554,7 +526,6 @@ private fun BottomSheetContent( modifier = Modifier.padding(horizontal = 24.dp, vertical = 16.dp) ) - // 菜单选项网格 LazyVerticalGrid( columns = GridCells.Fixed(4), modifier = Modifier.fillMaxWidth(), @@ -563,13 +534,10 @@ private fun BottomSheetContent( verticalArrangement = Arrangement.spacedBy(16.dp) ) { items(menuItems) { menuItem -> - BottomSheetMenuItemView( - menuItem = menuItem - ) + BottomSheetMenuItemView(menuItem = menuItem) } } - // 排序选项 Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) @@ -594,7 +562,6 @@ private fun BottomSheetContent( } } - // 应用分类选项 Spacer(modifier = Modifier.height(24.dp)) HorizontalDivider(modifier = Modifier.padding(horizontal = 24.dp)) @@ -648,10 +615,7 @@ private fun CategoryChip( modifier = modifier .fillMaxWidth() .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null - ) { onClick() }, + .clickable(interactionSource = interactionSource, indication = null) { onClick() }, shape = RoundedCornerShape(12.dp), color = if (isSelected) { MaterialTheme.colorScheme.primaryContainer @@ -661,13 +625,10 @@ private fun CategoryChip( tonalElevation = if (isSelected) 4.dp else 0.dp ) { Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), + modifier = Modifier.fillMaxWidth().padding(16.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - // 分类信息行 Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, @@ -687,7 +648,6 @@ private fun CategoryChip( overflow = TextOverflow.Ellipsis ) - // 选中指示器 AnimatedVisibility( visible = isSelected, enter = scaleIn() + fadeIn(), @@ -702,7 +662,6 @@ private fun CategoryChip( } } - // 应用数量 Text( text = "$appCount apps", style = MaterialTheme.typography.labelSmall, @@ -718,7 +677,6 @@ private fun CategoryChip( @Composable private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { - // 添加交互状态 val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() @@ -735,10 +693,7 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { modifier = Modifier .fillMaxWidth() .scale(scale) - .clickable( - interactionSource = interactionSource, - indication = null - ) { menuItem.onClick() } + .clickable(interactionSource = interactionSource, indication = null) { menuItem.onClick() } .padding(8.dp), horizontalAlignment = Alignment.CenterHorizontally ) { @@ -748,9 +703,7 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { color = MaterialTheme.colorScheme.primaryContainer, contentColor = MaterialTheme.colorScheme.onPrimaryContainer ) { - Box( - contentAlignment = Alignment.Center - ) { + Box(contentAlignment = Alignment.Center) { Icon( imageVector = menuItem.icon, contentDescription = stringResource(menuItem.titleRes), @@ -770,40 +723,110 @@ private fun BottomSheetMenuItemView(menuItem: BottomSheetMenuItem) { } } +@Composable +private fun LoadingAnimation( + modifier: Modifier = Modifier, + isLoading: Boolean = true +) { + val infiniteTransition = rememberInfiniteTransition(label = "loading") + + val alpha by infiniteTransition.animateFloat( + initialValue = 0.3f, + targetValue = 1f, + animationSpec = infiniteRepeatable( + animation = tween(600, easing = FastOutSlowInEasing), + repeatMode = RepeatMode.Reverse + ), + label = "alpha" + ) + + AnimatedVisibility( + visible = isLoading, + enter = fadeIn() + scaleIn(), + exit = fadeOut() + scaleOut(), + modifier = modifier + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + LinearProgressIndicator( + modifier = Modifier.width(200.dp).height(4.dp), + color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), + trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) + ) + } + } +} + +@Composable +@SuppressLint("ModifierParameter") +private fun EmptyState( + selectedCategory: AppCategory, + modifier: Modifier = Modifier, + isSearchEmpty: Boolean = false +) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = modifier + ) { + Icon( + imageVector = if (isSearchEmpty) Icons.Filled.SearchOff else Icons.Filled.Archive, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), + modifier = Modifier.size(96.dp).padding(bottom = 16.dp) + ) + Text( + text = if (isSearchEmpty || selectedCategory == AppCategory.ALL) { + stringResource(R.string.no_apps_found) + } else { + stringResource(R.string.no_apps_in_category) + }, + textAlign = TextAlign.Center, + style = MaterialTheme.typography.bodyLarge, + ) + } +} + @OptIn(ExperimentalLayoutApi::class, ExperimentalMaterial3Api::class) @Composable -private fun AppItem( - app: SuperUserViewModel.AppInfo, +private fun AppGroupItem( + appGroup: SuperUserViewModel.AppGroup, isSelected: Boolean, onToggleSelection: () -> Unit, onClick: () -> Unit, onLongClick: () -> Unit, viewModel: SuperUserViewModel ) { + val mainApp = appGroup.mainApp + ListItem( - modifier = Modifier - .pointerInput(Unit) { - detectTapGestures( - onLongPress = { onLongClick() }, - onTap = { onClick() } - ) - }, - headlineContent = { Text(app.label) }, + modifier = Modifier.pointerInput(Unit) { + detectTapGestures( + onLongPress = { onLongClick() }, + onTap = { onClick() } + ) + }, + headlineContent = { + Text(mainApp.label) + }, supportingContent = { Column { - Text(app.packageName) + val summaryText = if (appGroup.apps.size > 1) { + stringResource(R.string.group_contains_apps, appGroup.apps.size) + } else { + mainApp.packageName + } + Text(summaryText) Spacer(modifier = Modifier.height(4.dp)) - FlowRow( - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - if (app.allowSu) { - LabelItem( - text = "ROOT", - ) + FlowRow(horizontalArrangement = Arrangement.spacedBy(4.dp)) { + if (appGroup.allowSu) { + LabelItem(text = "ROOT") } else { - if (Natives.uidShouldUmount(app.uid)) { + if (Natives.uidShouldUmount(appGroup.uid)) { LabelItem( text = "UMOUNT", style = LabelItemDefaults.style.copy( @@ -813,15 +836,15 @@ private fun AppItem( ) } } - if (app.hasCustomProfile) { + if (appGroup.hasCustomProfile) { LabelItem( text = "CUSTOM", style = LabelItemDefaults.style.copy( - containerColor = MaterialTheme.colorScheme.onTertiary, + containerColor = MaterialTheme.colorScheme.tertiaryContainer, contentColor = MaterialTheme.colorScheme.onTertiaryContainer, ) ) - } else if (!app.allowSu) { + } else if (!appGroup.allowSu) { LabelItem( text = "DEFAULT", style = LabelItemDefaults.style.copy( @@ -829,24 +852,42 @@ private fun AppItem( ) ) } + if (appGroup.apps.size > 1) { + Natives.getUserName(appGroup.uid)?.let { + LabelItem( + text = it, + style = LabelItemDefaults.style.copy( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + ) + ) + } + } } } }, leadingContent = { AsyncImage( model = ImageRequest.Builder(LocalContext.current) - .data(app.packageInfo) + .data(mainApp.packageInfo) .crossfade(true) .build(), - contentDescription = app.label, - modifier = Modifier - .padding(4.dp) - .width(48.dp) - .height(48.dp) + contentDescription = mainApp.label, + modifier = Modifier.padding(4.dp).width(48.dp).height(48.dp) ) }, trailingContent = { - if (viewModel.showBatchActions) { + AnimatedVisibility( + visible = viewModel.showBatchActions, + enter = fadeIn(animationSpec = tween(200)) + scaleIn( + animationSpec = tween(200), + initialScale = 0.6f + ), + exit = fadeOut(animationSpec = tween(200)) + scaleOut( + animationSpec = tween(200), + targetScale = 0.6f + ) + ) { val checkboxInteractionSource = remember { MutableInteractionSource() } val isCheckboxPressed by checkboxInteractionSource.collectIsPressedAsState() @@ -874,103 +915,4 @@ private fun AppItem( } } ) -} - -@Composable -fun LabelText(label: String) { - Box( - modifier = Modifier - .padding(top = 4.dp, end = 4.dp) - .background( - Color.Black, - shape = RoundedCornerShape(4.dp) - ) - ) { - Text( - text = label, - modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), - style = TextStyle( - fontSize = 8.sp, - color = Color.White, - ) - ) - } -} - -/** - * 加载动画组件 - */ -@Composable -private fun LoadingAnimation( - modifier: Modifier = Modifier, - isLoading: Boolean = true -) { - val infiniteTransition = rememberInfiniteTransition(label = "loading") - - // 透明度动画 - val alpha by infiniteTransition.animateFloat( - initialValue = 0.3f, - targetValue = 1f, - animationSpec = infiniteRepeatable( - animation = tween(600, easing = FastOutSlowInEasing), - repeatMode = RepeatMode.Reverse - ), - label = "alpha" - ) - - AnimatedVisibility( - visible = isLoading, - enter = fadeIn() + scaleIn(), - exit = fadeOut() + scaleOut(), - modifier = modifier - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - // 进度指示器 - LinearProgressIndicator( - modifier = Modifier - .width(200.dp) - .height(4.dp), - color = MaterialTheme.colorScheme.primary.copy(alpha = alpha), - trackColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f) - ) - } - } -} - -/** - * 空状态组件 - */ -@Composable -@SuppressLint("ModifierParameter") -private fun EmptyState( - selectedCategory: AppCategory, - modifier: Modifier = Modifier, - isSearchEmpty: Boolean = false -) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - modifier = modifier - ) { - Icon( - imageVector = if (isSearchEmpty) Icons.Filled.SearchOff else Icons.Filled.Archive, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f), - modifier = Modifier - .size(96.dp) - .padding(bottom = 16.dp) - ) - Text( - text = if (isSearchEmpty || selectedCategory == AppCategory.ALL) { - stringResource(R.string.no_apps_found) - } else { - stringResource(R.string.no_apps_in_category) - }, - textAlign = TextAlign.Center, - style = MaterialTheme.typography.bodyLarge, - ) - } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt index 41eb32f..6aa1def 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/Template.kt @@ -3,10 +3,12 @@ package com.sukisu.ultra.ui.screen import android.content.ClipData import android.content.ClipboardManager import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -17,10 +19,13 @@ import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import androidx.core.content.getSystemService import androidx.lifecycle.compose.dropUnlessResumed import androidx.lifecycle.viewmodel.compose.viewModel @@ -253,4 +258,25 @@ private fun TopBar( windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), scrollBehavior = scrollBehavior ) -} \ No newline at end of file +} + +@Composable +fun LabelText(label: String) { + Box( + modifier = Modifier + .padding(top = 4.dp, end = 4.dp) + .background( + Color.Black, + shape = RoundedCornerShape(4.dp) + ) + ) { + Text( + text = label, + modifier = Modifier.padding(vertical = 2.dp, horizontal = 5.dp), + style = TextStyle( + fontSize = 8.sp, + color = Color.White, + ) + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt new file mode 100644 index 0000000..9a17c82 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/screen/UmountManagerScreen.kt @@ -0,0 +1,442 @@ +package com.sukisu.ultra.ui.screen + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +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.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.component.rememberConfirmDialog +import com.sukisu.ultra.ui.component.ConfirmResult +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.getCardColors +import com.sukisu.ultra.ui.theme.getCardElevation +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +private val SPACING_SMALL = 3.dp +private val SPACING_MEDIUM = 8.dp +private val SPACING_LARGE = 16.dp + +data class UmountPathEntry( + val path: String, + val checkMnt: Boolean, + val flags: Int, + val isDefault: Boolean +) + +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun UmountManagerScreen(navigator: DestinationsNavigator) { + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val snackBarHost = LocalSnackbarHost.current + val context = LocalContext.current + val scope = rememberCoroutineScope() + val confirmDialog = rememberConfirmDialog() + + var pathList by remember { mutableStateOf>(emptyList()) } + var isLoading by remember { mutableStateOf(false) } + var showAddDialog by remember { mutableStateOf(false) } + + fun loadPaths() { + scope.launch(Dispatchers.IO) { + isLoading = true + val result = listUmountPaths() + val entries = parseUmountPaths(result) + withContext(Dispatchers.Main) { + pathList = entries + isLoading = false + } + } + } + + LaunchedEffect(Unit) { + loadPaths() + } + + Scaffold( + topBar = { + TopAppBar( + title = { Text(stringResource(R.string.umount_path_manager)) }, + navigationIcon = { + IconButton(onClick = { navigator.navigateUp() }) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = null) + } + }, + actions = { + IconButton(onClick = { loadPaths() }) { + Icon(Icons.Filled.Refresh, contentDescription = null) + } + }, + scrollBehavior = scrollBehavior, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy( + alpha = CardConfig.cardAlpha + ) + ) + ) + }, + floatingActionButton = { + FloatingActionButton( + onClick = { showAddDialog = true } + ) { + Icon(Icons.Filled.Add, contentDescription = null) + } + }, + snackbarHost = { SnackbarHost(snackBarHost) } + ) { paddingValues -> + Column( + modifier = Modifier + .padding(paddingValues) + .nestedScroll(scrollBehavior.nestedScrollConnection) + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + colors = getCardColors(MaterialTheme.colorScheme.primaryContainer), + elevation = getCardElevation() + ) { + Column( + modifier = Modifier.padding(SPACING_LARGE) + ) { + Icon( + imageVector = Icons.Filled.Info, + contentDescription = null, + tint = MaterialTheme.colorScheme.onPrimaryContainer + ) + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + Text( + text = stringResource(R.string.umount_path_restart_notice), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer + ) + } + } + + if (isLoading) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center + ) { + CircularProgressIndicator() + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = PaddingValues(horizontal = SPACING_LARGE, vertical = SPACING_MEDIUM), + verticalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + items(pathList, key = { it.path }) { entry -> + UmountPathCard( + entry = entry, + onDelete = { + scope.launch(Dispatchers.IO) { + val success = removeUmountPath(entry.path) + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.umount_path_removed) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + ) + } + + item { + Spacer(modifier = Modifier.height(SPACING_LARGE)) + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = SPACING_LARGE), + horizontalArrangement = Arrangement.spacedBy(SPACING_MEDIUM) + ) { + Button( + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.confirm_action), + content = context.getString(R.string.confirm_clear_custom_paths) + ) == ConfirmResult.Confirmed) { + withContext(Dispatchers.IO) { + val success = clearCustomUmountPaths() + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.custom_paths_cleared) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + } + }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Filled.DeleteForever, contentDescription = null) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.clear_custom_paths)) + } + + Button( + onClick = { + scope.launch(Dispatchers.IO) { + val success = applyUmountConfigToKernel() + withContext(Dispatchers.Main) { + if (success) { + snackBarHost.showSnackbar( + context.getString(R.string.config_applied) + ) + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Filled.Check, contentDescription = null) + Spacer(modifier = Modifier.width(SPACING_MEDIUM)) + Text(stringResource(R.string.apply_config)) + } + } + } + } + } + } + + if (showAddDialog) { + AddUmountPathDialog( + onDismiss = { showAddDialog = false }, + onConfirm = { path, checkMnt, flags -> + showAddDialog = false + + scope.launch(Dispatchers.IO) { + val success = addUmountPath(path, checkMnt, flags) + withContext(Dispatchers.Main) { + if (success) { + saveUmountConfig() + snackBarHost.showSnackbar( + context.getString(R.string.umount_path_added) + ) + loadPaths() + } else { + snackBarHost.showSnackbar( + context.getString(R.string.operation_failed) + ) + } + } + } + } + ) + } + } +} + +@Composable +fun UmountPathCard( + entry: UmountPathEntry, + onDelete: () -> Unit +) { + val confirmDialog = rememberConfirmDialog() + val scope = rememberCoroutineScope() + val context = LocalContext.current + + Card( + modifier = Modifier.fillMaxWidth(), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerLow), + elevation = getCardElevation() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(SPACING_LARGE), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Folder, + contentDescription = null, + tint = if (entry.isDefault) + MaterialTheme.colorScheme.primary + else + MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(24.dp) + ) + + Spacer(modifier = Modifier.width(SPACING_LARGE)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = entry.path, + style = MaterialTheme.typography.titleMedium + ) + Spacer(modifier = Modifier.height(SPACING_SMALL)) + Text( + text = buildString { + append(context.getString(R.string.check_mount_type)) + append(": ") + append(if (entry.checkMnt) context.getString(R.string.yes) else context.getString(R.string.no)) + append(" | ") + append(context.getString(R.string.flags)) + append(": ") + append(entry.flags.toUmountFlagName(context)) + if (entry.isDefault) { + append(" | ") + append(context.getString(R.string.default_entry)) + } + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + + if (!entry.isDefault) { + IconButton( + onClick = { + scope.launch { + if (confirmDialog.awaitConfirm( + title = context.getString(R.string.confirm_delete), + content = context.getString(R.string.confirm_delete_umount_path, entry.path) + ) == ConfirmResult.Confirmed) { + onDelete() + } + } + } + ) { + Icon( + imageVector = Icons.Filled.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error + ) + } + } + } + } +} + +@Composable +fun AddUmountPathDialog( + onDismiss: () -> Unit, + onConfirm: (String, Boolean, Int) -> Unit +) { + var path by rememberSaveable { mutableStateOf("") } + var checkMnt by rememberSaveable { mutableStateOf(false) } + var flags by rememberSaveable { mutableStateOf("-1") } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.add_umount_path)) }, + text = { + Column { + OutlinedTextField( + value = path, + onValueChange = { path = it }, + label = { Text(stringResource(R.string.mount_path)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true + ) + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = checkMnt, + onCheckedChange = { checkMnt = it } + ) + Spacer(modifier = Modifier.width(SPACING_SMALL)) + Text(stringResource(R.string.check_mount_type_overlay)) + } + + Spacer(modifier = Modifier.height(SPACING_MEDIUM)) + + OutlinedTextField( + value = flags, + onValueChange = { flags = it }, + label = { Text(stringResource(R.string.umount_flags)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + supportingText = { Text(stringResource(R.string.umount_flags_hint)) } + ) + } + }, + confirmButton = { + TextButton( + onClick = { + val flagsInt = flags.toIntOrNull() ?: -1 + onConfirm(path, checkMnt, flagsInt) + }, + enabled = path.isNotBlank() + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(android.R.string.cancel)) + } + } + ) +} + +private fun parseUmountPaths(output: String): List { + val lines = output.lines().filter { it.isNotBlank() } + if (lines.size < 2) return emptyList() + + return lines.drop(2).mapNotNull { line -> + val parts = line.trim().split(Regex("\\s+")) + if (parts.size >= 4) { + UmountPathEntry( + path = parts[0], + checkMnt = parts[1].equals("true", ignoreCase = true), + flags = parts[2].toIntOrNull() ?: -1, + isDefault = parts[3].equals("Yes", ignoreCase = true) + ) + } else null + } +} + +private fun Int.toUmountFlagName(context: android.content.Context): String { + return when (this) { + -1 -> context.getString(R.string.mnt_detach) + else -> this.toString() + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt similarity index 90% rename from manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt index 583207b..00353b2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/screen/SuSFSConfig.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/SuSFSConfig.kt @@ -1,6 +1,7 @@ -package com.sukisu.ultra.ui.screen +package com.sukisu.ultra.ui.susfs import android.annotation.SuppressLint +import android.content.Context import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.layout.* @@ -25,11 +26,13 @@ import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator import com.sukisu.ultra.R -import com.sukisu.ultra.ui.component.* +import com.sukisu.ultra.ui.susfs.component.* import com.sukisu.ultra.ui.theme.CardConfig -import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158 -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion159 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion159 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion1512 +import com.sukisu.ultra.ui.util.getSuSFSVersion import com.sukisu.ultra.ui.util.isAbDevice import kotlinx.coroutines.launch import java.io.File @@ -43,6 +46,7 @@ enum class SuSFSTab(val displayNameRes: Int) { BASIC_SETTINGS(R.string.susfs_tab_basic_settings), SUS_PATHS(R.string.susfs_tab_sus_paths), SUS_LOOP_PATHS(R.string.susfs_tab_sus_loop_paths), + SUS_MAPS(R.string.susfs_tab_sus_maps), SUS_MOUNTS(R.string.susfs_tab_sus_mounts), TRY_UMOUNT(R.string.susfs_tab_try_umount), KSTAT_CONFIG(R.string.susfs_tab_kstat_config), @@ -50,11 +54,12 @@ enum class SuSFSTab(val displayNameRes: Int) { ENABLED_FEATURES(R.string.susfs_tab_enabled_features); companion object { - fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean): List { + fun getAllTabs(isSusVersion158: Boolean, isSusVersion159: Boolean, isSusVersion1512: Boolean): List { return when { - isSusVersion159 -> entries.toList() - isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS } - else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS } + isSusVersion1512 -> entries.toList() + isSusVersion159 -> entries.filter { it != SUS_MAPS} + isSusVersion158 -> entries.filter { it != SUS_LOOP_PATHS && it != SUS_MAPS } + else -> entries.filter { it != PATH_SETTINGS && it != SUS_LOOP_PATHS && it != SUS_MAPS } } } } @@ -93,6 +98,7 @@ fun SuSFSConfigScreen( // 路径管理相关状态 var susPaths by remember { mutableStateOf(emptySet()) } var susLoopPaths by remember { mutableStateOf(emptySet()) } + var susMaps by remember { mutableStateOf(emptySet()) } var susMounts by remember { mutableStateOf(emptySet()) } var tryUmounts by remember { mutableStateOf(emptySet()) } var androidDataPath by remember { mutableStateOf("") } @@ -117,16 +123,17 @@ fun SuSFSConfigScreen( // 对话框状态 var showAddPathDialog by remember { mutableStateOf(false) } var showAddLoopPathDialog by remember { mutableStateOf(false) } + var showAddSusMapDialog by remember { mutableStateOf(false) } var showAddAppPathDialog by remember { mutableStateOf(false) } var showAddMountDialog by remember { mutableStateOf(false) } var showAddUmountDialog by remember { mutableStateOf(false) } - var showRunUmountDialog by remember { mutableStateOf(false) } var showAddKstatStaticallyDialog by remember { mutableStateOf(false) } var showAddKstatDialog by remember { mutableStateOf(false) } // 编辑状态 var editingPath by remember { mutableStateOf(null) } var editingLoopPath by remember { mutableStateOf(null) } + var editingSusMap by remember { mutableStateOf(null) } var editingMount by remember { mutableStateOf(null) } var editingUmount by remember { mutableStateOf(null) } var editingKstatConfig by remember { mutableStateOf(null) } @@ -135,6 +142,7 @@ fun SuSFSConfigScreen( // 重置确认对话框状态 var showResetPathsDialog by remember { mutableStateOf(false) } var showResetLoopPathsDialog by remember { mutableStateOf(false) } + var showResetSusMapsDialog by remember { mutableStateOf(false) } var showResetMountsDialog by remember { mutableStateOf(false) } var showResetUmountsDialog by remember { mutableStateOf(false) } var showResetKstatDialog by remember { mutableStateOf(false) } @@ -148,7 +156,7 @@ fun SuSFSConfigScreen( var isNavigating by remember { mutableStateOf(false) } - val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159()) + val allTabs = SuSFSTab.getAllTabs(isSusVersion158(), isSusVersion159(), isSusVersion1512()) // 实时判断是否可以启用开机自启动 val canEnableAutoStart by remember { @@ -157,6 +165,38 @@ fun SuSFSConfigScreen( } } + var showVersionMismatchDialog by remember { mutableStateOf(false) } + + if (showVersionMismatchDialog) { + AlertDialog( + onDismissRequest = { showVersionMismatchDialog = false }, + title = { + Text( + text = stringResource(R.string.warning), + style = MaterialTheme.typography.headlineSmall, + fontWeight = FontWeight.Bold + ) + }, + text = { + Text( + stringResource( + R.string.susfs_version_mismatch, + try { getSuSFSVersion() } catch (_: Exception) { "unknown" }, + SuSFSManager.MAX_SUSFS_VERSION + ) + ) + }, + confirmButton = { + TextButton( + onClick = { showVersionMismatchDialog = false }, + modifier = Modifier.padding(8.dp) + ) { + Text(stringResource(R.string.confirm)) + } + } + ) + } + // 文件选择器 val backupFileLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.CreateDocument("application/json") @@ -242,25 +282,42 @@ fun SuSFSConfigScreen( // 加载当前配置 LaunchedEffect(Unit) { - unameValue = SuSFSManager.getUnameValue(context) - buildTimeValue = SuSFSManager.getBuildTimeValue(context) - autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) - executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) - susPaths = SuSFSManager.getSusPaths(context) - susLoopPaths = SuSFSManager.getSusLoopPaths(context) - susMounts = SuSFSManager.getSusMounts(context) - tryUmounts = SuSFSManager.getTryUmounts(context) - androidDataPath = SuSFSManager.getAndroidDataPath(context) - sdcardPath = SuSFSManager.getSdcardPath(context) - kstatConfigs = SuSFSManager.getKstatConfigs(context) - addKstatPaths = SuSFSManager.getAddKstatPaths(context) - hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) - enableHideBl = SuSFSManager.getEnableHideBl(context) - enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) - umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) - enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) + coroutineScope.launch { + try { + val version = getSuSFSVersion() + val binaryName = "ksu_susfs_${version.removePrefix("v")}" - loadSlotInfo() + val isBinaryAvailable = try { + context.assets.open(binaryName).use { true } + } catch (_: Exception) { false } + + if (!isBinaryAvailable) { + showVersionMismatchDialog = true + } + } catch (_: Exception) { + } + + unameValue = SuSFSManager.getUnameValue(context) + buildTimeValue = SuSFSManager.getBuildTimeValue(context) + autoStartEnabled = SuSFSManager.isAutoStartEnabled(context) + executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) + susPaths = SuSFSManager.getSusPaths(context) + susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) + susMounts = SuSFSManager.getSusMounts(context) + tryUmounts = SuSFSManager.getTryUmounts(context) + androidDataPath = SuSFSManager.getAndroidDataPath(context) + sdcardPath = SuSFSManager.getSdcardPath(context) + kstatConfigs = SuSFSManager.getKstatConfigs(context) + addKstatPaths = SuSFSManager.getAddKstatPaths(context) + hideSusMountsForAllProcs = SuSFSManager.getHideSusMountsForAllProcs(context) + enableHideBl = SuSFSManager.getEnableHideBl(context) + enableCleanupResidue = SuSFSManager.getEnableCleanupResidue(context) + umountForZygoteIsoService = SuSFSManager.getUmountForZygoteIsoService(context) + enableAvcLogSpoofing = SuSFSManager.getEnableAvcLogSpoofing(context) + + loadSlotInfo() + } } // 当切换到启用功能状态标签页时加载数据 @@ -419,6 +476,7 @@ fun SuSFSConfigScreen( executeInPostFsData = SuSFSManager.getExecuteInPostFsData(context) susPaths = SuSFSManager.getSusPaths(context) susLoopPaths = SuSFSManager.getSusLoopPaths(context) + susMaps = SuSFSManager.getSusMaps(context) susMounts = SuSFSManager.getSusMounts(context) tryUmounts = SuSFSManager.getTryUmounts(context) androidDataPath = SuSFSManager.getAndroidDataPath(context) @@ -537,6 +595,35 @@ fun SuSFSConfigScreen( initialValue = editingLoopPath ?: "" ) + AddPathDialog( + showDialog = showAddSusMapDialog, + onDismiss = { + showAddSusMapDialog = false + editingSusMap = null + }, + onConfirm = { path -> + coroutineScope.launch { + isLoading = true + val success = if (editingSusMap != null) { + SuSFSManager.editSusMap(context, editingSusMap!!, path) + } else { + SuSFSManager.addSusMap(context, path) + } + if (success) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + showAddSusMapDialog = false + editingSusMap = null + } + }, + isLoading = isLoading, + titleRes = if (editingSusMap != null) R.string.susfs_edit_sus_map else R.string.susfs_add_sus_map, + labelRes = R.string.susfs_sus_map_label, + placeholderRes = R.string.susfs_sus_map_placeholder, + initialValue = editingSusMap ?: "" + ) + AddAppPathDialog( showDialog = showAddAppPathDialog, onDismiss = { showAddAppPathDialog = false }, @@ -629,8 +716,21 @@ fun SuSFSConfigScreen( isLoading = true val success = if (editingKstatConfig != null) { SuSFSManager.editKstatConfig( - context, editingKstatConfig!!, path, ino, dev, nlink, size, atime, atimeNsec, - mtime, mtimeNsec, ctime, ctimeNsec, blocks, blksize + context, + editingKstatConfig!!, + path, + ino, + dev, + nlink, + size, + atime, + atimeNsec, + mtime, + mtimeNsec, + ctime, + ctimeNsec, + blocks, + blksize ) } else { SuSFSManager.addKstatStatically( @@ -680,22 +780,6 @@ fun SuSFSConfigScreen( ) // 确认对话框 - ConfirmDialog( - showDialog = showRunUmountDialog, - onDismiss = { showRunUmountDialog = false }, - onConfirm = { - coroutineScope.launch { - isLoading = true - SuSFSManager.runTryUmount(context) - isLoading = false - showRunUmountDialog = false - } - }, - titleRes = R.string.susfs_run_umount_confirm_title, - messageRes = R.string.susfs_run_umount_confirm_message, - isLoading = isLoading - ) - ConfirmDialog( showDialog = showConfirmReset, onDismiss = { showConfirmReset = false }, @@ -760,6 +844,27 @@ fun SuSFSConfigScreen( isDestructive = true ) + ConfirmDialog( + showDialog = showResetSusMapsDialog, + onDismiss = { showResetSusMapsDialog = false }, + onConfirm = { + coroutineScope.launch { + isLoading = true + SuSFSManager.saveSusMaps(context, emptySet()) + susMaps = emptySet() + if (SuSFSManager.isAutoStartEnabled(context)) { + SuSFSManager.configureAutoStart(context, true) + } + isLoading = false + showResetSusMapsDialog = false + } + }, + titleRes = R.string.susfs_reset_sus_maps_title, + messageRes = R.string.susfs_reset_sus_maps_message, + isLoading = isLoading, + isDestructive = true + ) + ConfirmDialog( showDialog = showResetMountsDialog, onDismiss = { showResetMountsDialog = false }, @@ -979,6 +1084,28 @@ fun SuSFSConfigScreen( } } + SuSFSTab.SUS_MAPS -> { + OutlinedButton( + onClick = { showResetSusMapsDialog = true }, + enabled = !isLoading && susMaps.isNotEmpty(), + shape = RoundedCornerShape(8.dp), + modifier = Modifier + .fillMaxWidth() + .height(40.dp) + ) { + Icon( + imageVector = Icons.Default.RestoreFromTrash, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(6.dp)) + Text( + stringResource(R.string.susfs_reset_sus_maps_title), + fontWeight = FontWeight.Medium + ) + } + } + SuSFSTab.SUS_MOUNTS -> { OutlinedButton( onClick = { showResetMountsDialog = true }, @@ -1110,12 +1237,12 @@ fun SuSFSConfigScreen( .padding(horizontal = 12.dp) ) { // 标签页 - ScrollableTabRow( + PrimaryScrollableTabRow( selectedTabIndex = allTabs.indexOf(selectedTab), - edgePadding = 0.dp, modifier = Modifier.fillMaxWidth(), containerColor = MaterialTheme.colorScheme.surface, - contentColor = MaterialTheme.colorScheme.onSurface + contentColor = MaterialTheme.colorScheme.onSurface, + edgePadding = 0.dp ) { allTabs.forEach { tab -> Tab( @@ -1243,6 +1370,26 @@ fun SuSFSConfigScreen( } ) } + SuSFSTab.SUS_MAPS -> { + SusMapsContent( + susMaps = susMaps, + isLoading = isLoading, + onAddSusMap = { showAddSusMapDialog = true }, + onRemoveSusMap = { map -> + coroutineScope.launch { + isLoading = true + if (SuSFSManager.removeSusMap(context, map)) { + susMaps = SuSFSManager.getSusMaps(context) + } + isLoading = false + } + }, + onEditSusMap = { map -> + editingSusMap = map + showAddSusMapDialog = true + } + ) + } SuSFSTab.SUS_MOUNTS -> { val isSusVersion158 = remember { isSusVersion158() } @@ -1268,7 +1415,11 @@ fun SuSFSConfigScreen( onToggleHideSusMountsForAllProcs = { hideForAll -> coroutineScope.launch { isLoading = true - if (SuSFSManager.setHideSusMountsForAllProcs(context, hideForAll)) { + if (SuSFSManager.setHideSusMountsForAllProcs( + context, + hideForAll + ) + ) { hideSusMountsForAllProcs = hideForAll } isLoading = false @@ -1283,7 +1434,6 @@ fun SuSFSConfigScreen( umountForZygoteIsoService = umountForZygoteIsoService, isLoading = isLoading, onAddUmount = { showAddUmountDialog = true }, - onRunUmount = { showRunUmountDialog = true }, onRemoveUmount = { umountEntry -> coroutineScope.launch { isLoading = true @@ -1300,7 +1450,8 @@ fun SuSFSConfigScreen( onToggleUmountForZygoteIsoService = { enabled -> coroutineScope.launch { isLoading = true - val success = SuSFSManager.setUmountForZygoteIsoService(context, enabled) + val success = + SuSFSManager.setUmountForZygoteIsoService(context, enabled) if (success) { umountForZygoteIsoService = enabled } @@ -1411,7 +1562,7 @@ private fun BasicSettingsContent( isLoading: Boolean, onAutoStartToggle: (Boolean) -> Unit, onShowSlotInfo: () -> Unit, - context: android.content.Context, + context: Context, onShowBackupDialog: () -> Unit, onShowRestoreDialog: () -> Unit, enableHideBl: Boolean, @@ -1422,7 +1573,9 @@ private fun BasicSettingsContent( onEnableAvcLogSpoofingChange: (Boolean) -> Unit ) { var scriptLocationExpanded by remember { mutableStateOf(false) } - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value val isSusVersion159 = isSusVersion159() Column( @@ -1498,7 +1651,7 @@ private fun BasicSettingsContent( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = scriptLocationExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable, true), + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), shape = RoundedCornerShape(8.dp), enabled = !isLoading ) @@ -1912,7 +2065,9 @@ private fun SlotInfoDialog( onUseUname: (String) -> Unit, onUseBuildTime: (String) -> Unit ) { - val isAbDevice = isAbDevice() + val isAbDevice = produceState(initialValue = false) { + value = isAbDevice() + }.value if (showDialog && isAbDevice) { AlertDialog( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt index f3fb780..41a0c4c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigDialogs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigDialogs.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package com.sukisu.ultra.ui.susfs.component import android.annotation.SuppressLint import android.content.pm.PackageInfo @@ -29,7 +29,7 @@ import coil.compose.AsyncImage import coil.request.ImageRequest import com.google.accompanist.drawablepainter.rememberDrawablePainter import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import kotlinx.coroutines.launch @@ -464,7 +464,7 @@ fun AddTryUmountDialog( trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = umountModeExpanded) }, modifier = Modifier .fillMaxWidth() - .menuAnchor(MenuAnchorType.PrimaryEditable, true), + .menuAnchor(ExposedDropdownMenuAnchorType.PrimaryEditable, true), shape = RoundedCornerShape(8.dp) ) ExposedDropdownMenu( diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt similarity index 87% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt index 5e2bd89..be683f2 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SuSFSConfigTabs.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/component/SuSFSConfigTabs.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package com.sukisu.ultra.ui.susfs.component import android.annotation.SuppressLint import androidx.compose.foundation.layout.* @@ -18,8 +18,8 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.sukisu.ultra.R -import com.sukisu.ultra.ui.util.SuSFSManager -import com.sukisu.ultra.ui.util.SuSFSManager.isSusVersion158 +import com.sukisu.ultra.ui.susfs.util.SuSFSManager +import com.sukisu.ultra.ui.susfs.util.SuSFSManager.isSusVersion158 import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel /** @@ -310,6 +310,116 @@ fun SusLoopPathsContent( } } +/** + * SUS Maps内容组件 + */ +@Composable +fun SusMapsContent( + susMaps: Set, + isLoading: Boolean, + onAddSusMap: () -> Unit, + onRemoveSusMap: (String) -> Unit, + onEditSusMap: ((String) -> Unit)? = null +) { + Box(modifier = Modifier.fillMaxSize()) { + LazyColumn( + modifier = Modifier.fillMaxSize(), + verticalArrangement = Arrangement.spacedBy(12.dp) + ) { + // 说明卡片 + item { + Card( + modifier = Modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.4f) + ), + shape = RoundedCornerShape(12.dp) + ) { + Column( + modifier = Modifier.padding(12.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.sus_maps_description_title), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary + ) + Text( + text = stringResource(R.string.sus_maps_description_text), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = stringResource(R.string.sus_maps_warning), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.secondary + ) + Text( + text = stringResource(R.string.sus_maps_debug_info), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f) + ) + } + } + } + + if (susMaps.isEmpty()) { + item { + EmptyStateCard( + message = stringResource(R.string.susfs_no_sus_maps_configured) + ) + } + } else { + item { + SectionHeader( + title = stringResource(R.string.sus_maps_section), + subtitle = null, + icon = Icons.Default.Security, + count = susMaps.size + ) + } + + items(susMaps.toList()) { map -> + PathItemCard( + path = map, + icon = Icons.Default.Security, + onDelete = { onRemoveSusMap(map) }, + onEdit = if (onEditSusMap != null) { { onEditSusMap(map) } } else null, + isLoading = isLoading + ) + } + } + + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(vertical = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = onAddSusMap, + modifier = Modifier + .weight(1f) + .height(48.dp), + shape = RoundedCornerShape(8.dp) + ) { + Icon( + imageVector = Icons.Default.Add, + contentDescription = null, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(text = stringResource(R.string.add)) + } + } + } + } + } +} + /** * SUS挂载内容组件 */ @@ -395,7 +505,6 @@ fun TryUmountContent( umountForZygoteIsoService: Boolean, isLoading: Boolean, onAddUmount: () -> Unit, - onRunUmount: () -> Unit, onRemoveUmount: (String) -> Unit, onEditUmount: ((String) -> Unit)? = null, onToggleUmountForZygoteIsoService: (Boolean) -> Unit @@ -509,24 +618,6 @@ fun TryUmountContent( Spacer(modifier = Modifier.width(8.dp)) Text(text = stringResource(R.string.add)) } - - if (tryUmounts.isNotEmpty()) { - Button( - onClick = onRunUmount, - modifier = Modifier - .weight(1f) - .height(48.dp), - shape = RoundedCornerShape(8.dp) - ) { - Icon( - imageVector = Icons.Default.PlayArrow, - contentDescription = null, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(text = stringResource(R.string.susfs_run)) - } - } } } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt similarity index 89% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt index 0da7793..9dca4cb 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSManager.kt @@ -1,14 +1,14 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.susfs.util import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo +import android.os.Build import android.util.Log import android.widget.Toast import com.dergoogler.mmrl.platform.Platform.Companion.context -import com.sukisu.ultra.Natives import com.sukisu.ultra.R import com.topjohnwu.superuser.Shell import kotlinx.coroutines.Dispatchers @@ -19,9 +19,14 @@ import java.io.File import java.io.FileOutputStream import java.io.IOException import androidx.core.content.edit +import com.sukisu.ultra.ui.util.getRootShell +import com.sukisu.ultra.ui.util.getSuSFSVersion +import com.sukisu.ultra.ui.util.getSuSFSFeatures import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll +import org.json.JSONArray import org.json.JSONObject import java.text.SimpleDateFormat import java.util.* @@ -37,6 +42,8 @@ object SuSFSManager { private const val KEY_AUTO_START_ENABLED = "auto_start_enabled" private const val KEY_SUS_PATHS = "sus_paths" private const val KEY_SUS_LOOP_PATHS = "sus_loop_paths" + + private const val KEY_SUS_MAPS = "sus_maps" private const val KEY_SUS_MOUNTS = "sus_mounts" private const val KEY_TRY_UMOUNTS = "try_umounts" private const val KEY_ANDROID_DATA_PATH = "android_data_path" @@ -60,6 +67,8 @@ object SuSFSManager { private const val MODULE_PATH = "/data/adb/modules/$MODULE_ID" private const val MIN_VERSION_FOR_HIDE_MOUNT = "1.5.8" private const val MIN_VERSION_FOR_LOOP_PATH = "1.5.9" + private const val MIN_VERSION_SUS_MAPS = "1.5.12" + const val MAX_SUSFS_VERSION = "2.0.0" private const val BACKUP_FILE_EXTENSION = ".susfs_backup" private const val MEDIA_DATA_PATH = "/data/media/0/Android/data" private const val CGROUP_UID_PATH_PREFIX = "/sys/fs/cgroup/uid_" @@ -112,7 +121,7 @@ object SuSFSManager { configurationsJson.keys().forEach { key -> val value = configurationsJson.get(key) configurations[key] = when (value) { - is org.json.JSONArray -> { + is JSONArray -> { val set = mutableSetOf() for (i in 0 until value.length()) { set.add(value.getString(i)) @@ -147,6 +156,7 @@ object SuSFSManager { val executeInPostFsData: Boolean, val susPaths: Set, val susLoopPaths: Set, + val susMaps: Set, val susMounts: Set, val tryUmounts: Set, val androidDataPath: String, @@ -169,6 +179,7 @@ object SuSFSManager { buildTimeValue != DEFAULT_BUILD_TIME || susPaths.isNotEmpty() || susLoopPaths.isNotEmpty() || + susMaps.isNotEmpty() || susMounts.isNotEmpty() || tryUmounts.isNotEmpty() || kstatConfigs.isNotEmpty() || @@ -180,11 +191,23 @@ object SuSFSManager { private fun getPrefs(context: Context): SharedPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) - private fun getSuSFSVersionUse(): String = try { - getSuSFSVersion() - } catch (_: Exception) { MIN_VERSION_FOR_HIDE_MOUNT } + private fun getSuSFSVersionUse(context: Context): String = try { + val version = getSuSFSVersion() + val binaryName = "${SUSFS_BINARY_TARGET_NAME}_${version.removePrefix("v")}" + if (isBinaryAvailable(context, binaryName)) { + version + } else { + MAX_SUSFS_VERSION + } + } catch (_: Exception) { + MAX_SUSFS_VERSION + } - private fun getSuSFSBinaryName(): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse().removePrefix("v")}" + fun isBinaryAvailable(context: Context, binaryName: String): Boolean = try { + context.assets.open(binaryName).use { true } + } catch (_: IOException) { false } + + private fun getSuSFSBinaryName(context: Context): String = "${SUSFS_BINARY_TARGET_NAME}_${getSuSFSVersionUse(context).removePrefix("v")}" private fun getSuSFSTargetPath(): String = "/data/adb/ksu/bin/$SUSFS_BINARY_TARGET_NAME" @@ -222,29 +245,19 @@ object SuSFSManager { return 0 } - /** - * 检查是否支持设置sdcard路径等功能(1.5.8+) - */ - fun isSusVersion158(): Boolean { - return try { - val currentVersion = getSuSFSVersion() - compareVersions(currentVersion, MIN_VERSION_FOR_HIDE_MOUNT) >= 0 - } catch (_: Exception) { - true - } + private fun isVersionAtLeast(minVersion: String): Boolean = try { + compareVersions(getSuSFSVersion(), minVersion) >= 0 + } catch (_: Exception) { + true } + // 检查是否支持设置sdcard路径等功能(1.5.8+) + fun isSusVersion158(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_HIDE_MOUNT) - /** - * 检查是否支持循环路径和AVC日志欺骗等功能(1.5.9+) - */ - fun isSusVersion159(): Boolean { - return try { - val currentVersion = getSuSFSVersion() - compareVersions(currentVersion, MIN_VERSION_FOR_LOOP_PATH) >= 0 - } catch (_: Exception) { - true - } - } + // 检查是否支持循环路径和AVC日志欺骗等功能(1.5.9+) + fun isSusVersion159(): Boolean = isVersionAtLeast(MIN_VERSION_FOR_LOOP_PATH) + + // 检查是否支持隐藏内存映射(1.5.12+) + fun isSusVersion1512(): Boolean = isVersionAtLeast(MIN_VERSION_SUS_MAPS) /** * 获取当前模块配置 @@ -257,6 +270,7 @@ object SuSFSManager { executeInPostFsData = getExecuteInPostFsData(context), susPaths = getSusPaths(context), susLoopPaths = getSusLoopPaths(context), + susMaps = getSusMaps(context), susMounts = getSusMounts(context), tryUmounts = getTryUmounts(context), androidDataPath = getAndroidDataPath(context), @@ -304,7 +318,7 @@ object SuSFSManager { fun saveExecuteInPostFsData(context: Context, executeInPostFsData: Boolean) { getPrefs(context).edit { putBoolean(KEY_EXECUTE_IN_POST_FS_DATA, executeInPostFsData) } if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } @@ -361,6 +375,12 @@ object SuSFSManager { fun getSusLoopPaths(context: Context): Set = getPrefs(context).getStringSet(KEY_SUS_LOOP_PATHS, emptySet()) ?: emptySet() + fun saveSusMaps(context: Context, maps: Set) = + getPrefs(context).edit { putStringSet(KEY_SUS_MAPS, maps) } + + fun getSusMaps(context: Context): Set = + getPrefs(context).getStringSet(KEY_SUS_MAPS, emptySet()) ?: emptySet() + fun saveSusMounts(context: Context, mounts: Set) = getPrefs(context).edit { putStringSet(KEY_SUS_MOUNTS, mounts) } @@ -526,6 +546,7 @@ object SuSFSManager { KEY_AUTO_START_ENABLED to isAutoStartEnabled(context), KEY_SUS_PATHS to getSusPaths(context), KEY_SUS_LOOP_PATHS to getSusLoopPaths(context), + KEY_SUS_MAPS to getSusMaps(context), KEY_SUS_MOUNTS to getSusMounts(context), KEY_TRY_UMOUNTS to getTryUmounts(context), KEY_ANDROID_DATA_PATH to getAndroidDataPath(context), @@ -552,7 +573,7 @@ object SuSFSManager { // 获取设备信息 private fun getDeviceInfo(): String { return try { - "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL} (${android.os.Build.VERSION.RELEASE})" + "${Build.MANUFACTURER} ${Build.MODEL} (${Build.VERSION.RELEASE})" } catch (_: Exception) { "Unknown Device" } @@ -710,7 +731,7 @@ object SuSFSManager { // 二进制文件管理 private suspend fun copyBinaryFromAssets(context: Context): String? = withContext(Dispatchers.IO) { try { - val binaryName = getSuSFSBinaryName() + val binaryName = getSuSFSBinaryName(context) val targetPath = getSuSFSTargetPath() val tempFile = File(context.cacheDir, binaryName) @@ -731,7 +752,7 @@ object SuSFSManager { } fun isBinaryAvailable(context: Context): Boolean = try { - context.assets.open(getSuSFSBinaryName()).use { true } + context.assets.open(getSuSFSBinaryName(context)).use { true } } catch (_: IOException) { false } // 命令执行 @@ -818,9 +839,10 @@ object SuSFSManager { // 功能状态获取 suspend fun getEnabledFeatures(context: Context): List = withContext(Dispatchers.IO) { try { - val status = Natives.getSusfsFeatureStatus() - if (status != null) { - parseEnabledFeaturesFromStatus(context, status) + val featuresOutput = getSuSFSFeatures() + + if (featuresOutput.isNotBlank() && featuresOutput != "Invalid") { + parseEnabledFeaturesFromOutput(context, featuresOutput) } else { getDefaultDisabledFeatures(context) } @@ -830,10 +852,47 @@ object SuSFSManager { } } + private fun parseEnabledFeaturesFromOutput(context: Context, featuresOutput: String): List { + val enabledConfigs = featuresOutput.lines() + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + + val featureMap = mapOf( + "CONFIG_KSU_SUSFS_SUS_PATH" to context.getString(R.string.sus_path_feature_label), + "CONFIG_KSU_SUSFS_SUS_MOUNT" to context.getString(R.string.sus_mount_feature_label), + "CONFIG_KSU_SUSFS_TRY_UMOUNT" to context.getString(R.string.try_umount_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_UNAME" to context.getString(R.string.spoof_uname_feature_label), + "CONFIG_KSU_SUSFS_SPOOF_CMDLINE_OR_BOOTCONFIG" to context.getString(R.string.spoof_cmdline_feature_label), + "CONFIG_KSU_SUSFS_OPEN_REDIRECT" to context.getString(R.string.open_redirect_feature_label), + "CONFIG_KSU_SUSFS_ENABLE_LOG" to context.getString(R.string.enable_log_feature_label), + "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_KSU_DEFAULT_MOUNT" to context.getString(R.string.auto_default_mount_feature_label), + "CONFIG_KSU_SUSFS_AUTO_ADD_SUS_BIND_MOUNT" to context.getString(R.string.auto_bind_mount_feature_label), + "CONFIG_KSU_SUSFS_AUTO_ADD_TRY_UMOUNT_FOR_BIND_MOUNT" to context.getString(R.string.auto_try_umount_bind_feature_label), + "CONFIG_KSU_SUSFS_HIDE_KSU_SUSFS_SYMBOLS" to context.getString(R.string.hide_symbols_feature_label), + "CONFIG_KSU_SUSFS_SUS_KSTAT" to context.getString(R.string.sus_kstat_feature_label), + "CONFIG_KSU_SUSFS_SUS_SU" to context.getString(R.string.sus_su_feature_label) + ) + + + return featureMap.map { (configKey, displayName) -> + val isEnabled = enabledConfigs.contains(configKey) + + val statusText = if (isEnabled) { + context.getString(R.string.susfs_feature_enabled) + } else { + context.getString(R.string.susfs_feature_disabled) + } + + val canConfigure = displayName == context.getString(R.string.enable_log_feature_label) + + EnabledFeature(displayName, isEnabled, statusText, canConfigure) + }.sortedBy { it.name } + } + private fun getDefaultDisabledFeatures(context: Context): List { val defaultFeatures = listOf( "sus_path_feature_label" to context.getString(R.string.sus_path_feature_label), - "sus_loop_path_feature_label" to context.getString(R.string.sus_loop_path_feature_label), "sus_mount_feature_label" to context.getString(R.string.sus_mount_feature_label), "try_umount_feature_label" to context.getString(R.string.try_umount_feature_label), "spoof_uname_feature_label" to context.getString(R.string.spoof_uname_feature_label), @@ -845,7 +904,6 @@ object SuSFSManager { "auto_try_umount_bind_feature_label" to context.getString(R.string.auto_try_umount_bind_feature_label), "hide_symbols_feature_label" to context.getString(R.string.hide_symbols_feature_label), "sus_kstat_feature_label" to context.getString(R.string.sus_kstat_feature_label), - "magic_mount_feature_label" to context.getString(R.string.magic_mount_feature_label), "sus_su_feature_label" to context.getString(R.string.sus_su_feature_label) ) @@ -859,31 +917,6 @@ object SuSFSManager { }.sortedBy { it.name } } - private fun parseEnabledFeaturesFromStatus(context: Context, status: Natives.SusfsFeatureStatus): List { - val featureList = listOf( - Triple("status_sus_path", context.getString(R.string.sus_path_feature_label), status.statusSusPath), - Triple("status_sus_mount", context.getString(R.string.sus_mount_feature_label), status.statusSusMount), - Triple("status_try_umount", context.getString(R.string.try_umount_feature_label), status.statusTryUmount), - Triple("status_spoof_uname", context.getString(R.string.spoof_uname_feature_label), status.statusSpoofUname), - Triple("status_spoof_cmdline", context.getString(R.string.spoof_cmdline_feature_label), status.statusSpoofCmdline), - Triple("status_open_redirect", context.getString(R.string.open_redirect_feature_label), status.statusOpenRedirect), - Triple("status_enable_log", context.getString(R.string.enable_log_feature_label), status.statusEnableLog), - Triple("status_auto_default_mount", context.getString(R.string.auto_default_mount_feature_label), status.statusAutoDefaultMount), - Triple("status_auto_bind_mount", context.getString(R.string.auto_bind_mount_feature_label), status.statusAutoBindMount), - Triple("status_auto_try_umount_bind", context.getString(R.string.auto_try_umount_bind_feature_label), status.statusAutoTryUmountBind), - Triple("status_hide_symbols", context.getString(R.string.hide_symbols_feature_label), status.statusHideSymbols), - Triple("status_sus_kstat", context.getString(R.string.sus_kstat_feature_label), status.statusSusKstat), - Triple("status_magic_mount", context.getString(R.string.magic_mount_feature_label), status.statusMagicMount), - Triple("status_sus_su", context.getString(R.string.sus_su_feature_label), status.statusSusSu) - ) - - return featureList.map { (id, displayName, isEnabled) -> - val statusText = if (isEnabled) context.getString(R.string.susfs_feature_enabled) else context.getString(R.string.susfs_feature_disabled) - val canConfigure = id == "status_enable_log" - EnabledFeature(displayName, isEnabled, statusText, canConfigure) - }.sortedBy { it.name } - } - // sus日志开关 suspend fun setEnableLog(context: Context, enabled: Boolean): Boolean { val success = executeSusfsCommand(context, "enable_log ${if (enabled) 1 else 0}") @@ -1107,6 +1140,54 @@ object SuSFSManager { } } + // 添加 SUS Maps + suspend fun addSusMap(context: Context, map: String): Boolean { + val success = executeSusfsCommand(context, "add_sus_map '$map'") + if (success) { + saveSusMaps(context, getSusMaps(context) + map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_added_success, map)) + } + return success + } + + suspend fun removeSusMap(context: Context, map: String): Boolean { + saveSusMaps(context, getSusMaps(context) - map) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, context.getString(R.string.susfs_sus_map_removed, map)) + return true + } + + suspend fun editSusMap(context: Context, oldMap: String, newMap: String): Boolean { + return try { + val currentMaps = getSusMaps(context).toMutableSet() + if (!currentMaps.remove(oldMap)) { + showToast(context, "Original SUS map not found: $oldMap") + return false + } + + saveSusMaps(context, currentMaps) + + val success = addSusMap(context, newMap) + + if (success) { + showToast(context, context.getString(R.string.susfs_sus_map_updated, oldMap, newMap)) + return true + } else { + // 如果添加新映射失败,恢复旧映射 + currentMaps.add(oldMap) + saveSusMaps(context, currentMaps) + if (isAutoStartEnabled(context)) updateMagiskModule(context) + showToast(context, "Failed to update SUS map, reverted to original") + return false + } + } catch (e: Exception) { + e.printStackTrace() + showToast(context, "Error updating SUS map: ${e.message}") + false + } + } + // 添加SUS挂载 suspend fun addSusMount(context: Context, mount: String): Boolean { val success = executeSusfsCommand(context, "add_sus_mount '$mount'") @@ -1208,8 +1289,6 @@ object SuSFSManager { } } - suspend fun runTryUmount(context: Context): Boolean = executeSusfsCommand(context, "run_try_umount") - // Zygote隔离服务卸载控制 suspend fun setUmountForZygoteIsoService(context: Context, enabled: Boolean): Boolean { if (!isSusVersion158()) { @@ -1361,7 +1440,7 @@ object SuSFSManager { if (success) { saveAndroidDataPath(context, path) if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } @@ -1375,7 +1454,7 @@ object SuSFSManager { if (success) { saveSdcardPath(context, path) if (isAutoStartEnabled(context)) { - kotlinx.coroutines.CoroutineScope(Dispatchers.Default).launch { + CoroutineScope(Dispatchers.Default).launch { updateMagiskModule(context) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt similarity index 97% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt index 1cf9cf8..e0d9bae 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/SuSFSModuleScripts.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/susfs/util/SuSFSModuleScripts.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.susfs.util import android.annotation.SuppressLint @@ -497,6 +497,10 @@ object ScriptGenerator { if (config.susLoopPaths.isNotEmpty()) { generateSusLoopPathsSection(config.susLoopPaths) } + + if (config.susMaps.isNotEmpty()) { + generateSusMapsSection(config.susMaps) + } } } @@ -504,6 +508,17 @@ object ScriptGenerator { } } + private fun StringBuilder.generateSusMapsSection(susMaps: Set) { + if (susMaps.isNotEmpty()) { + appendLine("# 添加SUS映射") + susMaps.forEach { map -> + appendLine("\"${'$'}SUSFS_BIN\" add_sus_map '$map'") + appendLine("echo \"$(get_current_time): 添加SUS映射: $map\" >> \"${'$'}LOG_FILE\"") + } + appendLine() + } + } + @SuppressLint("SdCardPath") private fun StringBuilder.generatePathSettingSection(androidDataPath: String, sdcardPath: String) { appendLine("# 路径配置") diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt index e05ce3b..2757852 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/CardManage.kt @@ -6,114 +6,187 @@ import androidx.compose.material3.CardDefaults import androidx.compose.runtime.* import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +@Stable object CardConfig { // 卡片透明度 var cardAlpha by mutableFloatStateOf(1f) + internal set // 卡片亮度 var cardDim by mutableFloatStateOf(0f) + internal set // 卡片阴影 var cardElevation by mutableStateOf(0.dp) - var isShadowEnabled by mutableStateOf(true) - var isCustomAlphaSet by mutableStateOf(false) - var isCustomDimSet by mutableStateOf(false) - var isUserDarkModeEnabled by mutableStateOf(false) - var isUserLightModeEnabled by mutableStateOf(false) - var isCustomBackgroundEnabled by mutableStateOf(false) + internal set + + // 功能开关 + var isShadowEnabled by mutableStateOf(true) + internal set + var isCustomBackgroundEnabled by mutableStateOf(false) + internal set + + var isCustomAlphaSet by mutableStateOf(false) + internal set + var isCustomDimSet by mutableStateOf(false) + internal set + var isUserDarkModeEnabled by mutableStateOf(false) + internal set + var isUserLightModeEnabled by mutableStateOf(false) + internal set + + // 配置键名 + private object Keys { + const val CARD_ALPHA = "card_alpha" + const val CARD_DIM = "card_dim" + const val CUSTOM_BACKGROUND_ENABLED = "custom_background_enabled" + const val IS_SHADOW_ENABLED = "is_shadow_enabled" + const val IS_CUSTOM_ALPHA_SET = "is_custom_alpha_set" + const val IS_CUSTOM_DIM_SET = "is_custom_dim_set" + const val IS_USER_DARK_MODE_ENABLED = "is_user_dark_mode_enabled" + const val IS_USER_LIGHT_MODE_ENABLED = "is_user_light_mode_enabled" + } + + fun updateAlpha(alpha: Float, isCustom: Boolean = true) { + cardAlpha = alpha.coerceIn(0f, 1f) + if (isCustom) isCustomAlphaSet = true + } + + fun updateDim(dim: Float, isCustom: Boolean = true) { + cardDim = dim.coerceIn(0f, 1f) + if (isCustom) isCustomDimSet = true + } + + fun updateShadow(enabled: Boolean, elevation: Dp = cardElevation) { + isShadowEnabled = enabled + cardElevation = if (enabled) elevation else cardElevation + } + + fun updateBackground(enabled: Boolean) { + isCustomBackgroundEnabled = enabled + // 自定义背景时自动禁用阴影以获得更好的视觉效果 + if (enabled) { + updateShadow(false) + } + } + + fun updateThemePreference(darkMode: Boolean?, lightMode: Boolean?) { + isUserDarkModeEnabled = darkMode ?: false + isUserLightModeEnabled = lightMode ?: false + } + + fun reset() { + cardAlpha = 1f + cardDim = 0f + cardElevation = 0.dp + isShadowEnabled = true + isCustomBackgroundEnabled = false + isCustomAlphaSet = false + isCustomDimSet = false + isUserDarkModeEnabled = false + isUserLightModeEnabled = false + } + + fun setThemeDefaults(isDarkMode: Boolean) { + if (!isCustomAlphaSet) { + updateAlpha(if (isDarkMode) 0.88f else 1f, false) + } + if (!isCustomDimSet) { + updateDim(if (isDarkMode) 0.25f else 0f, false) + } + // 暗色模式下默认启用轻微阴影 + if (isDarkMode && !isCustomBackgroundEnabled) { + updateShadow(true, 2.dp) + } + } - /** - * 保存卡片配置到SharedPreferences - */ fun save(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) prefs.edit().apply { - putFloat("card_alpha", cardAlpha) - putFloat("card_dim", cardDim) - putBoolean("custom_background_enabled", isCustomBackgroundEnabled) - putBoolean("is_shadow_enabled", isShadowEnabled) - putBoolean("is_custom_alpha_set", isCustomAlphaSet) - putBoolean("is_custom_dim_set", isCustomDimSet) - putBoolean("is_user_dark_mode_enabled", isUserDarkModeEnabled) - putBoolean("is_user_light_mode_enabled", isUserLightModeEnabled) + putFloat(Keys.CARD_ALPHA, cardAlpha) + putFloat(Keys.CARD_DIM, cardDim) + putBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, isCustomBackgroundEnabled) + putBoolean(Keys.IS_SHADOW_ENABLED, isShadowEnabled) + putBoolean(Keys.IS_CUSTOM_ALPHA_SET, isCustomAlphaSet) + putBoolean(Keys.IS_CUSTOM_DIM_SET, isCustomDimSet) + putBoolean(Keys.IS_USER_DARK_MODE_ENABLED, isUserDarkModeEnabled) + putBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, isUserLightModeEnabled) apply() } } - /** - * 从SharedPreferences加载卡片配置 - */ fun load(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - cardAlpha = prefs.getFloat("card_alpha", 1f) - cardDim = prefs.getFloat("card_dim", 0f) - isCustomBackgroundEnabled = prefs.getBoolean("custom_background_enabled", false) - isShadowEnabled = prefs.getBoolean("is_shadow_enabled", true) - isCustomAlphaSet = prefs.getBoolean("is_custom_alpha_set", false) - isCustomDimSet = prefs.getBoolean("is_custom_dim_set", false) - isUserDarkModeEnabled = prefs.getBoolean("is_user_dark_mode_enabled", false) - isUserLightModeEnabled = prefs.getBoolean("is_user_light_mode_enabled", false) - updateShadowEnabled(isShadowEnabled) + val prefs = context.getSharedPreferences("card_settings", Context.MODE_PRIVATE) + cardAlpha = prefs.getFloat(Keys.CARD_ALPHA, 1f).coerceIn(0f, 1f) + cardDim = prefs.getFloat(Keys.CARD_DIM, 0f).coerceIn(0f, 1f) + isCustomBackgroundEnabled = prefs.getBoolean(Keys.CUSTOM_BACKGROUND_ENABLED, false) + isShadowEnabled = prefs.getBoolean(Keys.IS_SHADOW_ENABLED, true) + isCustomAlphaSet = prefs.getBoolean(Keys.IS_CUSTOM_ALPHA_SET, false) + isCustomDimSet = prefs.getBoolean(Keys.IS_CUSTOM_DIM_SET, false) + isUserDarkModeEnabled = prefs.getBoolean(Keys.IS_USER_DARK_MODE_ENABLED, false) + isUserLightModeEnabled = prefs.getBoolean(Keys.IS_USER_LIGHT_MODE_ENABLED, false) + + // 应用阴影设置 + updateShadow(isShadowEnabled, if (isShadowEnabled) cardElevation else 0.dp) } - /** - * 更新阴影启用状态 - */ + @Deprecated("使用 updateShadow 替代", ReplaceWith("updateShadow(enabled)")) fun updateShadowEnabled(enabled: Boolean) { - isShadowEnabled = enabled - cardElevation = 0.dp - } - - /** - * 设置主题模式默认值 - */ - fun setThemeDefaults(isDarkMode: Boolean) { - if (!isCustomAlphaSet) { - cardAlpha = 1f - } - if (!isCustomDimSet) { - cardDim = if (isDarkMode) 0.5f else 0f - } - updateShadowEnabled(isShadowEnabled) + updateShadow(enabled) } } -/** - * 获取卡片颜色配置 - */ -@Composable -fun getCardColors(originalColor: Color) = CardDefaults.cardColors( - containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), - contentColor = determineContentColor(originalColor) -) +object CardStyleProvider { -/** - * 获取卡片阴影配置 - */ -@Composable -fun getCardElevation() = CardDefaults.cardElevation( - defaultElevation = CardConfig.cardElevation, - pressedElevation = CardConfig.cardElevation, - focusedElevation = CardConfig.cardElevation, - hoveredElevation = CardConfig.cardElevation, - draggedElevation = CardConfig.cardElevation, - disabledElevation = CardConfig.cardElevation -) + @Composable + fun getCardColors(originalColor: Color) = CardDefaults.cardColors( + containerColor = originalColor.copy(alpha = CardConfig.cardAlpha), + contentColor = determineContentColor(originalColor), + disabledContainerColor = originalColor.copy(alpha = CardConfig.cardAlpha * 0.38f), + disabledContentColor = determineContentColor(originalColor).copy(alpha = 0.38f) + ) -/** - * 根据背景颜色、主题模式和用户设置确定内容颜色 - */ -@Composable -private fun determineContentColor(originalColor: Color): Color { - val isDarkTheme = isSystemInDarkTheme() - if (ThemeConfig.isThemeChanging) { - return if (isDarkTheme) Color.White else Color.Black - } + @Composable + fun getCardElevation() = CardDefaults.cardElevation( + defaultElevation = CardConfig.cardElevation, + pressedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + focusedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + hoveredElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + draggedElevation = if (CardConfig.isShadowEnabled) { + (CardConfig.cardElevation.value + 0).dp + } else 0.dp, + disabledElevation = 0.dp + ) - return when { - CardConfig.isUserLightModeEnabled -> Color.Black - !isDarkTheme && originalColor.luminance() > 0.5f -> Color.Black - isDarkTheme -> Color.White - else -> if (originalColor.luminance() > 0.5f) Color.Black else Color.White + @Composable + private fun determineContentColor(originalColor: Color): Color { + val isDarkTheme = isSystemInDarkTheme() + + return when { + ThemeConfig.isThemeChanging -> { + if (isDarkTheme) Color.White else Color.Black + } + CardConfig.isUserLightModeEnabled -> Color.Black + CardConfig.isUserDarkModeEnabled -> Color.White + else -> { + val luminance = originalColor.luminance() + val threshold = if (isDarkTheme) 0.4f else 0.6f + if (luminance > threshold) Color.Black else Color.White + } + } } } + +// 向后兼容 +@Composable +fun getCardColors(originalColor: Color) = CardStyleProvider.getCardColors(originalColor) + +@Composable +fun getCardElevation() = CardStyleProvider.getCardElevation() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt index ad8cb63..87ac86d 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/Theme.kt @@ -1,6 +1,5 @@ package com.sukisu.ultra.ui.theme -import android.content.ContentResolver import android.content.Context import android.net.Uri import android.os.Build @@ -9,9 +8,7 @@ import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.enableEdgeToEdge import androidx.annotation.RequiresApi -import androidx.compose.animation.core.animateFloat -import androidx.compose.animation.core.spring -import androidx.compose.animation.core.updateTransition +import androidx.compose.animation.core.* import androidx.compose.foundation.background import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Box @@ -28,36 +25,38 @@ import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import androidx.core.content.edit import androidx.core.net.toUri import coil.compose.AsyncImagePainter import coil.compose.rememberAsyncImagePainter -import com.sukisu.ultra.ui.util.BackgroundTransformation -import com.sukisu.ultra.ui.util.saveTransformedBackground +import com.sukisu.ultra.ui.theme.util.BackgroundTransformation +import com.sukisu.ultra.ui.theme.util.saveTransformedBackground +import kotlinx.coroutines.DelicateCoroutinesApi +import kotlinx.coroutines.launch import java.io.File import java.io.FileOutputStream -import java.io.InputStream -/** - * 主题配置对象,管理应用的主题相关状态 - */ +@Stable object ThemeConfig { + // 主题状态 var customBackgroundUri by mutableStateOf(null) var forceDarkMode by mutableStateOf(null) var currentTheme by mutableStateOf(ThemeColors.Default) var useDynamicColor by mutableStateOf(false) + + // 背景状态 var backgroundImageLoaded by mutableStateOf(false) - var needsResetOnThemeChange by mutableStateOf(false) var isThemeChanging by mutableStateOf(false) var preventBackgroundRefresh by mutableStateOf(false) + // 主题变化检测 private var lastDarkModeState: Boolean? = null + fun detectThemeChange(currentDarkMode: Boolean): Boolean { - val isChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode + val hasChanged = lastDarkModeState != null && lastDarkModeState != currentDarkMode lastDarkModeState = currentDarkMode - return isChanged + return hasChanged } fun resetBackgroundState() { @@ -66,11 +65,171 @@ object ThemeConfig { } isThemeChanging = true } + + fun updateTheme( + theme: ThemeColors? = null, + dynamicColor: Boolean? = null, + darkMode: Boolean? = null + ) { + theme?.let { currentTheme = it } + dynamicColor?.let { useDynamicColor = it } + darkMode?.let { forceDarkMode = it } + } + + fun reset() { + customBackgroundUri = null + forceDarkMode = null + currentTheme = ThemeColors.Default + useDynamicColor = false + backgroundImageLoaded = false + isThemeChanging = false + preventBackgroundRefresh = false + lastDarkModeState = null + } +} + +object ThemeManager { + private const val PREFS_NAME = "theme_prefs" + + fun saveThemeMode(context: Context, forceDark: Boolean?) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putString("theme_mode", when (forceDark) { + true -> "dark" + false -> "light" + null -> "system" + }) + } + ThemeConfig.forceDarkMode = forceDark + } + + fun loadThemeMode(context: Context) { + val mode = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString("theme_mode", "system") + + ThemeConfig.forceDarkMode = when (mode) { + "dark" -> true + "light" -> false + else -> null + } + } + + fun saveThemeColors(context: Context, themeName: String) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putString("theme_colors", themeName) + } + ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + } + + fun loadThemeColors(context: Context) { + val themeName = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getString("theme_colors", "default") ?: "default" + ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + } + + fun saveDynamicColorState(context: Context, enabled: Boolean) { + context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE).edit { + putBoolean("use_dynamic_color", enabled) + } + ThemeConfig.useDynamicColor = enabled + } + + + fun loadDynamicColorState(context: Context) { + val enabled = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + .getBoolean("use_dynamic_color", Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + ThemeConfig.useDynamicColor = enabled + } +} + +object BackgroundManager { + private const val TAG = "BackgroundManager" + + fun saveAndApplyCustomBackground( + context: Context, + uri: Uri, + transformation: BackgroundTransformation? = null + ) { + try { + val finalUri = if (transformation != null) { + context.saveTransformedBackground(uri, transformation) + } else { + copyImageToInternalStorage(context, uri) + } + + saveBackgroundUri(context, finalUri) + ThemeConfig.customBackgroundUri = finalUri + CardConfig.updateBackground(true) + resetBackgroundState(context) + + } catch (e: Exception) { + Log.e(TAG, "保存背景失败: ${e.message}", e) + } + } + + fun clearCustomBackground(context: Context) { + saveBackgroundUri(context, null) + ThemeConfig.customBackgroundUri = null + CardConfig.updateBackground(false) + resetBackgroundState(context) + } + + fun loadCustomBackground(context: Context) { + val uriString = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) + .getString("custom_background", null) + + val newUri = uriString?.toUri() + val preventRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) + .getBoolean("prevent_background_refresh", false) + + ThemeConfig.preventBackgroundRefresh = preventRefresh + + if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { + Log.d(TAG, "加载自定义背景: $uriString") + ThemeConfig.customBackgroundUri = newUri + ThemeConfig.backgroundImageLoaded = false + CardConfig.updateBackground(newUri != null) + } + } + + private fun saveBackgroundUri(context: Context, uri: Uri?) { + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putString("custom_background", uri?.toString()) + putBoolean("prevent_background_refresh", false) + } + } + + private fun resetBackgroundState(context: Context) { + ThemeConfig.backgroundImageLoaded = false + ThemeConfig.preventBackgroundRefresh = false + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putBoolean("prevent_background_refresh", false) + } + } + + private fun copyImageToInternalStorage(context: Context, uri: Uri): Uri? { + return try { + val inputStream = context.contentResolver.openInputStream(uri) ?: return null + val fileName = "custom_background_${System.currentTimeMillis()}.jpg" + val file = File(context.filesDir, fileName) + + FileOutputStream(file).use { outputStream -> + val buffer = ByteArray(8 * 1024) + var read: Int + while (inputStream.read(buffer).also { read = it } != -1) { + outputStream.write(buffer, 0, read) + } + outputStream.flush() + } + inputStream.close() + + Uri.fromFile(file) + } catch (e: Exception) { + Log.e(TAG, "复制图片失败: ${e.message}", e) + null + } + } } -/** - * 应用主题 - */ @Composable fun KernelSUTheme( darkTheme: Boolean = when(ThemeConfig.forceDarkMode) { @@ -84,198 +243,223 @@ fun KernelSUTheme( val context = LocalContext.current val systemIsDark = isSystemInDarkTheme() - // 检测系统主题变化并保存状态 - val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) - LaunchedEffect(systemIsDark, themeChanged) { - if (ThemeConfig.forceDarkMode == null && themeChanged) { - Log.d("ThemeSystem", "系统主题变化检测: 从 ${!systemIsDark} 变为 $systemIsDark") - ThemeConfig.resetBackgroundState() - - if (!ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - } - - CardConfig.apply { - load(context) - if (!isCustomAlphaSet) { - cardAlpha = if (systemIsDark) 0.50f else 1f - } - if (!isCustomDimSet) { - cardDim = if (systemIsDark) 0.5f else 0f - } - save(context) - } - } - } - - SystemBarStyle( - darkMode = darkTheme - ) - - // 初始加载配置 - LaunchedEffect(Unit) { - context.loadThemeMode() - context.loadThemeColors() - context.loadDynamicColorState() - CardConfig.load(context) - - if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { - context.loadCustomBackground() - ThemeConfig.backgroundImageLoaded = false - } - - ThemeConfig.preventBackgroundRefresh = context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", true) - } + // 初始化主题 + ThemeInitializer(context = context, systemIsDark = systemIsDark) // 创建颜色方案 - val colorScheme = when { - dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (darkTheme) createDynamicDarkColorScheme(context) else createDynamicLightColorScheme(context) - } - darkTheme -> createDarkColorScheme() - else -> createLightColorScheme() - } + val colorScheme = createColorScheme(context, darkTheme, dynamicColor) - // 根据暗色模式和自定义背景调整卡片配置 - val isDarkModeWithCustomBackground = darkTheme && ThemeConfig.customBackgroundUri != null - if (darkTheme && !dynamicColor) { - CardConfig.setThemeDefaults(true) - } else if (!darkTheme && !dynamicColor) { - CardConfig.setThemeDefaults(false) - } - CardConfig.updateShadowEnabled(!isDarkModeWithCustomBackground) - - val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } - - LaunchedEffect(ThemeConfig.customBackgroundUri) { - backgroundUri.value = ThemeConfig.customBackgroundUri - } - - val bgImagePainter = backgroundUri.value?.let { - rememberAsyncImagePainter( - model = it, - onError = { err -> - Log.e("ThemeSystem", "背景图加载失败: ${err.result.throwable.message}") - ThemeConfig.customBackgroundUri = null - context.saveCustomBackground(null) - }, - onSuccess = { - Log.d("ThemeSystem", "背景图加载成功") - ThemeConfig.backgroundImageLoaded = true - ThemeConfig.isThemeChanging = false - - ThemeConfig.preventBackgroundRefresh = true - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { putBoolean("prevent_background_refresh", true) } - } - ) - } - - val transition = updateTransition( - targetState = ThemeConfig.backgroundImageLoaded, - label = "bgTransition" - ) - val bgAlpha by transition.animateFloat( - label = "bgAlpha", - transitionSpec = { - spring( - dampingRatio = 0.8f, - stiffness = 300f - ) - } - ) { loaded -> if (loaded) 1f else 0f } - - DisposableEffect(systemIsDark) { - onDispose { - if (ThemeConfig.isThemeChanging) { - ThemeConfig.isThemeChanging = false - } - } - } - - // 计算适用的暗化值 - val dimFactor = CardConfig.cardDim + // 系统栏样式 + SystemBarController(darkTheme) MaterialTheme( colorScheme = colorScheme, typography = Typography ) { Box(modifier = Modifier.fillMaxSize()) { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-2f) - .background(if (darkTheme) if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background } - else if (CardConfig.isCustomBackgroundEnabled) { colorScheme.surfaceContainerLow } else { colorScheme.background }) - ) - - // 自定义背景层 - backgroundUri.value?.let { - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(-1f) - .alpha(bgAlpha) - ) { - // 背景图片 - bgImagePainter?.let { painter -> - Box( - modifier = Modifier - .fillMaxSize() - .paint( - painter = painter, - contentScale = ContentScale.Crop - ) - .graphicsLayer { - alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f - } - ) - } - - // 亮度调节层 (根据cardDim调整) - Box( - modifier = Modifier - .fillMaxSize() - .background( - if (darkTheme) Color.Black.copy(alpha = 0.6f + dimFactor * 0.3f) - else Color.White.copy(alpha = 0.1f + dimFactor * 0.2f) - ) - ) - - // 边缘渐变遮罩 - Box( - modifier = Modifier - .fillMaxSize() - .background( - Brush.radialGradient( - colors = listOf( - Color.Transparent, - if (darkTheme) Color.Black.copy(alpha = 0.5f + dimFactor * 0.2f) - else Color.Black.copy(alpha = 0.2f + dimFactor * 0.1f) - ), - radius = 1200f - ) - ) - ) - } - } - + // 背景层 + BackgroundLayer(darkTheme) // 内容层 - Box( - modifier = Modifier - .fillMaxSize() - .zIndex(1f) - ) { + Box(modifier = Modifier.fillMaxSize().zIndex(1f)) { content() } } } } -/** - * 创建动态深色颜色方案 - */ +@Composable +private fun ThemeInitializer(context: Context, systemIsDark: Boolean) { + val themeChanged = ThemeConfig.detectThemeChange(systemIsDark) + val scope = rememberCoroutineScope() + + // 处理系统主题变化 + LaunchedEffect(systemIsDark, themeChanged) { + if (ThemeConfig.forceDarkMode == null && themeChanged) { + Log.d("ThemeSystem", "系统主题变化: $systemIsDark") + ThemeConfig.resetBackgroundState() + + if (!ThemeConfig.preventBackgroundRefresh) { + BackgroundManager.loadCustomBackground(context) + } + + CardConfig.apply { + load(context) + setThemeDefaults(systemIsDark) + save(context) + } + } + } + + // 初始加载配置 + LaunchedEffect(Unit) { + scope.launch { + ThemeManager.loadThemeMode(context) + ThemeManager.loadThemeColors(context) + ThemeManager.loadDynamicColorState(context) + CardConfig.load(context) + + if (!ThemeConfig.backgroundImageLoaded && !ThemeConfig.preventBackgroundRefresh) { + BackgroundManager.loadCustomBackground(context) + } + } + } +} + +@Composable +private fun BackgroundLayer(darkTheme: Boolean) { + val backgroundUri = rememberSaveable { mutableStateOf(ThemeConfig.customBackgroundUri) } + + LaunchedEffect(ThemeConfig.customBackgroundUri) { + backgroundUri.value = ThemeConfig.customBackgroundUri + } + + // 默认背景 + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-2f) + .background( + if (CardConfig.isCustomBackgroundEnabled) { + MaterialTheme.colorScheme.surfaceContainerLow + } else { + MaterialTheme.colorScheme.background + } + ) + ) + + // 自定义背景 + backgroundUri.value?.let { uri -> + CustomBackgroundLayer(uri = uri, darkTheme = darkTheme) + } +} + +@Composable +private fun CustomBackgroundLayer(uri: Uri, darkTheme: Boolean) { + val painter = rememberAsyncImagePainter( + model = uri, + onError = { error -> + Log.e("ThemeSystem", "背景加载失败: ${error.result.throwable.message}") + ThemeConfig.customBackgroundUri = null + }, + onSuccess = { + Log.d("ThemeSystem", "背景加载成功") + ThemeConfig.backgroundImageLoaded = true + ThemeConfig.isThemeChanging = false + } + ) + + val transition = updateTransition( + targetState = ThemeConfig.backgroundImageLoaded, + label = "backgroundTransition" + ) + + val alpha by transition.animateFloat( + label = "backgroundAlpha", + transitionSpec = { + spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + } + ) { loaded -> if (loaded) 1f else 0f } + + Box( + modifier = Modifier + .fillMaxSize() + .zIndex(-1f) + .alpha(alpha) + ) { + // 背景图片 + Box( + modifier = Modifier + .fillMaxSize() + .paint(painter = painter, contentScale = ContentScale.Crop) + .graphicsLayer { + this.alpha = (painter.state as? AsyncImagePainter.State.Success)?.let { 1f } ?: 0f + } + ) + + // 遮罩层 + BackgroundOverlay(darkTheme = darkTheme) + } +} + +@Composable +private fun BackgroundOverlay(darkTheme: Boolean) { + val dimFactor = CardConfig.cardDim + + // 主要遮罩层 + Box( + modifier = Modifier + .fillMaxSize() + .background( + if (darkTheme) { + Color.Black.copy(alpha = 0.3f + dimFactor * 0.4f) + } else { + Color.White.copy(alpha = 0.05f + dimFactor * 0.3f) + } + ) + ) + + // 边缘渐变遮罩 + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color.Transparent, + if (darkTheme) { + Color.Black.copy(alpha = 0.2f + dimFactor * 0.2f) + } else { + Color.Black.copy(alpha = 0.05f + dimFactor * 0.1f) + } + ), + radius = 1000f + ) + ) + ) +} + +@Composable +private fun createColorScheme( + context: Context, + darkTheme: Boolean, + dynamicColor: Boolean +): ColorScheme { + return when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + if (darkTheme) createDynamicDarkColorScheme(context) + else createDynamicLightColorScheme(context) + } + darkTheme -> createDarkColorScheme() + else -> createLightColorScheme() + } +} + +@Composable +private fun SystemBarController(darkMode: Boolean) { + val context = LocalContext.current + val activity = context as ComponentActivity + + SideEffect { + activity.enableEdgeToEdge( + statusBarStyle = SystemBarStyle.auto( + Color.Transparent.toArgb(), + Color.Transparent.toArgb(), + ) { darkMode }, + navigationBarStyle = if (darkMode) { + SystemBarStyle.dark(Color.Transparent.toArgb()) + } else { + SystemBarStyle.light( + Color.Transparent.toArgb(), + Color.Transparent.toArgb() + ) + } + ) + } +} + @RequiresApi(Build.VERSION_CODES.S) @Composable private fun createDynamicDarkColorScheme(context: Context): ColorScheme { @@ -288,9 +472,6 @@ private fun createDynamicDarkColorScheme(context: Context): ColorScheme { ) } -/** - * 创建动态浅色颜色方案 - */ @RequiresApi(Build.VERSION_CODES.S) @Composable private fun createDynamicLightColorScheme(context: Context): ColorScheme { @@ -303,11 +484,6 @@ private fun createDynamicLightColorScheme(context: Context): ColorScheme { ) } - - -/** - * 创建深色颜色方案 - */ @Composable private fun createDarkColorScheme() = darkColorScheme( primary = ThemeConfig.currentTheme.primaryDark, @@ -347,9 +523,6 @@ private fun createDarkColorScheme() = darkColorScheme( surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestDark, ) -/** - * 创建浅色颜色方案 - */ @Composable private fun createLightColorScheme() = lightColorScheme( primary = ThemeConfig.currentTheme.primaryLight, @@ -389,218 +562,32 @@ private fun createLightColorScheme() = lightColorScheme( surfaceContainerHighest = ThemeConfig.currentTheme.surfaceContainerHighestLight, ) - -/** - * 复制图片到应用内部存储并提升持久性 - */ -private fun Context.copyImageToInternalStorage(uri: Uri): Uri? { - return try { - val contentResolver: ContentResolver = contentResolver - val inputStream: InputStream = contentResolver.openInputStream(uri) ?: return null - - val fileName = "custom_background.jpg" - val file = File(filesDir, fileName) - - val backupFile = File(filesDir, "${fileName}.backup") - val outputStream = FileOutputStream(backupFile) - val buffer = ByteArray(4 * 1024) - var read: Int - - while (inputStream.read(buffer).also { read = it } != -1) { - outputStream.write(buffer, 0, read) - } - - outputStream.flush() - outputStream.close() - inputStream.close() - - if (file.exists()) { - file.delete() - } - backupFile.renameTo(file) - - Uri.fromFile(file) - } catch (e: Exception) { - Log.e("ImageCopy", "复制图片失败: ${e.message}") - null - } -} - -/** - * 保存并应用自定义背景 - */ +// 向后兼容 +@OptIn(DelicateCoroutinesApi::class) fun Context.saveAndApplyCustomBackground(uri: Uri, transformation: BackgroundTransformation? = null) { - val finalUri = if (transformation != null) { - saveTransformedBackground(uri, transformation) - } else { - copyImageToInternalStorage(uri) + kotlinx.coroutines.GlobalScope.launch { + BackgroundManager.saveAndApplyCustomBackground(this@saveAndApplyCustomBackground, uri, transformation) } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", finalUri?.toString()) - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - - ThemeConfig.customBackgroundUri = finalUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true } -/** - * 保存自定义背景 - */ fun Context.saveCustomBackground(uri: Uri?) { - val newUri = uri?.let { copyImageToInternalStorage(it) } - - // 保存到配置文件 - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("custom_background", newUri?.toString()) - if (uri == null) { - // 如果清除背景,也重置阻止刷新标志 - putBoolean("prevent_background_refresh", false) - } else { - // 设置阻止刷新标志为false,允许新设置的背景加载一次 - putBoolean("prevent_background_refresh", false) - } - } - - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - ThemeConfig.preventBackgroundRefresh = false - if (uri != null) { - CardConfig.cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true + saveAndApplyCustomBackground(uri) + } else { + BackgroundManager.clearCustomBackground(this) } } -/** - * 加载自定义背景 - */ -fun Context.loadCustomBackground() { - val uriString = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("custom_background", null) - - val newUri = uriString?.toUri() - val preventRefresh = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("prevent_background_refresh", false) - - ThemeConfig.preventBackgroundRefresh = preventRefresh - - if (!preventRefresh || ThemeConfig.customBackgroundUri?.toString() != newUri?.toString()) { - Log.d("ThemeSystem", "加载自定义背景: $uriString, 阻止刷新: $preventRefresh") - ThemeConfig.customBackgroundUri = newUri - ThemeConfig.backgroundImageLoaded = false - } -} - -/** - * 保存主题模式 - */ fun Context.saveThemeMode(forceDark: Boolean?) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString( - "theme_mode", when (forceDark) { - true -> "dark" - false -> "light" - null -> "system" - } - ) - } - ThemeConfig.forceDarkMode = forceDark - ThemeConfig.needsResetOnThemeChange = forceDark == null + ThemeManager.saveThemeMode(this, forceDark) } -/** - * 加载主题模式 - */ -fun Context.loadThemeMode() { - val mode = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_mode", "system") - ThemeConfig.forceDarkMode = when(mode) { - "dark" -> true - "light" -> false - else -> null - } - ThemeConfig.needsResetOnThemeChange = ThemeConfig.forceDarkMode == null -} - -/** - * 保存主题颜色 - */ fun Context.saveThemeColors(themeName: String) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putString("theme_colors", themeName) - } - - ThemeConfig.currentTheme = ThemeColors.fromName(themeName) + ThemeManager.saveThemeColors(this, themeName) } -/** - * 加载主题颜色 - */ -fun Context.loadThemeColors() { - val themeName = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getString("theme_colors", "default") - ThemeConfig.currentTheme = ThemeColors.fromName(themeName ?: "default") -} - -/** - * 保存动态颜色状态 - */ fun Context.saveDynamicColorState(enabled: Boolean) { - getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean("use_dynamic_color", enabled) - } - ThemeConfig.useDynamicColor = enabled -} - -/** - * 加载动态颜色状态 - */ -fun Context.loadDynamicColorState() { - val enabled = getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .getBoolean("use_dynamic_color", true) - - ThemeConfig.useDynamicColor = enabled -} - -@Composable -private fun SystemBarStyle( - darkMode: Boolean, - statusBarScrim: Color = Color.Transparent, - navigationBarScrim: Color = Color.Transparent, -) { - val context = LocalContext.current - val activity = context as ComponentActivity - - SideEffect { - activity.enableEdgeToEdge( - statusBarStyle = SystemBarStyle.auto( - statusBarScrim.toArgb(), - statusBarScrim.toArgb(), - ) { darkMode }, - navigationBarStyle = when { - darkMode -> SystemBarStyle.dark( - navigationBarScrim.toArgb() - ) - - else -> SystemBarStyle.light( - navigationBarScrim.toArgb(), - navigationBarScrim.toArgb(), - ) - } - ) - } + ThemeManager.saveDynamicColorState(this, enabled) } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt new file mode 100644 index 0000000..803d1f0 --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/component/ImageEditorDialog.kt @@ -0,0 +1,411 @@ +package com.sukisu.ultra.ui.theme.component + +import android.net.Uri +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.gestures.detectTransformGestures +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Fullscreen +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +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 coil.compose.AsyncImage +import coil.request.ImageRequest +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.util.BackgroundTransformation +import com.sukisu.ultra.ui.theme.util.saveTransformedBackground +import kotlinx.coroutines.launch +import kotlin.math.abs +import kotlin.math.max + +@Composable +fun ImageEditorDialog( + imageUri: Uri, + onDismiss: () -> Unit, + onConfirm: (Uri) -> Unit +) { + // 图像变换状态 + val transformState = remember { ImageTransformState() } + val context = LocalContext.current + val scope = rememberCoroutineScope() + + // 尺寸状态 + var imageSize by remember { mutableStateOf(Size.Zero) } + var screenSize by remember { mutableStateOf(Size.Zero) } + + // 动画状态 + val animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessMedium + ) + + val animatedScale by animateFloatAsState( + targetValue = transformState.scale, + animationSpec = animationSpec, + label = "ScaleAnimation" + ) + + val animatedOffsetX by animateFloatAsState( + targetValue = transformState.offsetX, + animationSpec = animationSpec, + label = "OffsetXAnimation" + ) + + val animatedOffsetY by animateFloatAsState( + targetValue = transformState.offsetY, + animationSpec = animationSpec, + label = "OffsetYAnimation" + ) + + // 工具函数 + val scaleToFullScreen = remember { + { + if (imageSize.height > 0 && screenSize.height > 0) { + val newScale = screenSize.height / imageSize.height + transformState.updateTransform(newScale, 0f, 0f) + } + } + } + + val saveImage: () -> Unit = remember { + { + scope.launch { + try { + val transformation = BackgroundTransformation( + transformState.scale, + transformState.offsetX, + transformState.offsetY + ) + val savedUri = context.saveTransformedBackground(imageUri, transformation) + savedUri?.let { onConfirm(it) } + } catch (_: Exception) { + } + } + } + } + + Dialog( + onDismissRequest = onDismiss, + properties = DialogProperties( + dismissOnBackPress = true, + dismissOnClickOutside = false, + usePlatformDefaultWidth = false + ) + ) { + Box( + modifier = Modifier + .fillMaxSize() + .background( + Brush.radialGradient( + colors = listOf( + Color.Black.copy(alpha = 0.9f), + Color.Black.copy(alpha = 0.95f) + ), + radius = 800f + ) + ) + .onSizeChanged { size -> + screenSize = Size(size.width.toFloat(), size.height.toFloat()) + } + ) { + // 图像显示区域 + ImageDisplayArea( + imageUri = imageUri, + animatedScale = animatedScale, + animatedOffsetX = animatedOffsetX, + animatedOffsetY = animatedOffsetY, + transformState = transformState, + onImageSizeChanged = { imageSize = it }, + modifier = Modifier.fillMaxSize() + ) + + // 顶部工具栏 + TopToolbar( + onDismiss = onDismiss, + onFullscreen = scaleToFullScreen, + onConfirm = saveImage, + modifier = Modifier.align(Alignment.TopCenter) + ) + + // 底部提示信息 + BottomHintCard( + modifier = Modifier.align(Alignment.BottomCenter) + ) + } + } +} + +/** + * 图像变换状态管理类 + */ +private class ImageTransformState { + var scale by mutableFloatStateOf(1f) + var offsetX by mutableFloatStateOf(0f) + var offsetY by mutableFloatStateOf(0f) + + private var lastScale = 1f + private var lastOffsetX = 0f + private var lastOffsetY = 0f + + fun updateTransform(newScale: Float, newOffsetX: Float, newOffsetY: Float) { + val scaleDiff = abs(newScale - lastScale) + val offsetXDiff = abs(newOffsetX - lastOffsetX) + val offsetYDiff = abs(newOffsetY - lastOffsetY) + + if (scaleDiff > 0.01f || offsetXDiff > 1f || offsetYDiff > 1f) { + scale = newScale + offsetX = newOffsetX + offsetY = newOffsetY + lastScale = newScale + lastOffsetX = newOffsetX + lastOffsetY = newOffsetY + } + } + + fun resetToLast() { + scale = lastScale + offsetX = lastOffsetX + offsetY = lastOffsetY + } +} + +/** + * 图像显示区域组件 + */ +@Composable +private fun ImageDisplayArea( + imageUri: Uri, + animatedScale: Float, + animatedOffsetX: Float, + animatedOffsetY: Float, + transformState: ImageTransformState, + onImageSizeChanged: (Size) -> Unit, + modifier: Modifier = Modifier +) { + val scope = rememberCoroutineScope() + + AsyncImage( + model = ImageRequest.Builder(LocalContext.current) + .data(imageUri) + .crossfade(true) + .build(), + contentDescription = stringResource(R.string.settings_custom_background), + contentScale = ContentScale.Fit, + modifier = modifier + .graphicsLayer( + scaleX = animatedScale, + scaleY = animatedScale, + translationX = animatedOffsetX, + translationY = animatedOffsetY + ) + .pointerInput(Unit) { + detectTransformGestures { _, pan, zoom, _ -> + scope.launch { + try { + val newScale = (transformState.scale * zoom).coerceIn(0.5f, 3f) + val maxOffsetX = max(0f, size.width * (newScale - 1) / 2) + val maxOffsetY = max(0f, size.height * (newScale - 1) / 2) + + val newOffsetX = if (maxOffsetX > 0) { + (transformState.offsetX + pan.x).coerceIn(-maxOffsetX, maxOffsetX) + } else 0f + + val newOffsetY = if (maxOffsetY > 0) { + (transformState.offsetY + pan.y).coerceIn(-maxOffsetY, maxOffsetY) + } else 0f + + transformState.updateTransform(newScale, newOffsetX, newOffsetY) + } catch (_: Exception) { + transformState.resetToLast() + } + } + } + } + .onSizeChanged { size -> + onImageSizeChanged(Size(size.width.toFloat(), size.height.toFloat())) + } + ) +} + +/** + * 顶部工具栏组件 + */ +@Composable +private fun TopToolbar( + onDismiss: () -> Unit, + onFullscreen: () -> Unit, + onConfirm: () -> Unit, + modifier: Modifier = Modifier +) { + Row( + modifier = modifier + .fillMaxWidth() + .padding(24.dp), + horizontalArrangement = Arrangement.SpaceBetween + ) { + // 关闭按钮 + ActionButton( + onClick = onDismiss, + icon = Icons.Default.Close, + contentDescription = stringResource(R.string.cancel), + backgroundColor = MaterialTheme.colorScheme.error.copy(alpha = 0.9f) + ) + + // 全屏按钮 + ActionButton( + onClick = onFullscreen, + icon = Icons.Default.Fullscreen, + contentDescription = stringResource(R.string.reprovision), + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.9f) + ) + + // 确认按钮 + ActionButton( + onClick = onConfirm, + icon = Icons.Default.Check, + contentDescription = stringResource(R.string.confirm), + backgroundColor = Color(0xFF4CAF50).copy(alpha = 0.9f) + ) + } +} + +/** + * 操作按钮组件 + */ +@Composable +private fun ActionButton( + onClick: () -> Unit, + icon: androidx.compose.ui.graphics.vector.ImageVector, + contentDescription: String, + backgroundColor: Color, + modifier: Modifier = Modifier +) { + var isPressed by remember { mutableStateOf(false) } + + val buttonScale by animateFloatAsState( + targetValue = if (isPressed) 0.85f else 1f, + animationSpec = spring( + dampingRatio = Spring.DampingRatioMediumBouncy, + stiffness = Spring.StiffnessHigh + ), + label = "ButtonScale" + ) + + val buttonAlpha by animateFloatAsState( + targetValue = if (isPressed) 0.8f else 1f, + animationSpec = tween(100), + label = "ButtonAlpha" + ) + + Surface( + onClick = { + isPressed = true + onClick() + }, + modifier = modifier + .size(64.dp) + .graphicsLayer( + scaleX = buttonScale, + scaleY = buttonScale, + alpha = buttonAlpha + ), + shape = CircleShape, + color = backgroundColor, + shadowElevation = 8.dp + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxSize() + ) { + Icon( + imageVector = icon, + contentDescription = contentDescription, + tint = Color.White, + modifier = Modifier.size(28.dp) + ) + } + } + + LaunchedEffect(isPressed) { + if (isPressed) { + kotlinx.coroutines.delay(150) + isPressed = false + } + } +} + +/** + * 底部提示卡片组件 + */ +@Composable +private fun BottomHintCard( + modifier: Modifier = Modifier +) { + var isVisible by remember { mutableStateOf(true) } + + val cardAlpha by animateFloatAsState( + targetValue = if (isVisible) 1f else 0f, + animationSpec = tween( + durationMillis = 500, + easing = EaseInOutCubic + ), + label = "HintAlpha" + ) + + val cardTranslationY by animateFloatAsState( + targetValue = if (isVisible) 0f else 100f, + animationSpec = tween( + durationMillis = 500, + easing = EaseInOutCubic + ), + label = "HintTranslation" + ) + + LaunchedEffect(Unit) { + kotlinx.coroutines.delay(4000) + isVisible = false + } + + Card( + modifier = modifier + .fillMaxWidth() + .padding(24.dp) + .alpha(cardAlpha) + .graphicsLayer { + translationY = cardTranslationY + }, + colors = CardDefaults.cardColors( + containerColor = Color.Black.copy(alpha = 0.85f) + ), + shape = RoundedCornerShape(16.dp), + elevation = CardDefaults.cardElevation(defaultElevation = 12.dp) + ) { + Text( + text = stringResource(id = R.string.image_editor_hint), + color = Color.White, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier + .padding(20.dp) + .fillMaxWidth() + ) + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt similarity index 98% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt index 1f66221..daf089b 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/BackgroundUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/theme/util/BackgroundUtils.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.theme.util import android.content.ContentResolver import android.content.Context diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt index 1c1b070..b532e8c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/KsuCli.kt @@ -13,6 +13,7 @@ import android.util.Log import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.Shell import com.topjohnwu.superuser.ShellUtils +import com.topjohnwu.superuser.io.SuFile import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import kotlinx.parcelize.Parcelize @@ -21,6 +22,7 @@ import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp import org.json.JSONArray import java.io.File +import java.util.concurrent.CountDownLatch /** @@ -30,11 +32,11 @@ import java.io.File private const val TAG = "KsuCli" private fun getKsuDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozako.so" + return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksud.so" } object KsuCli { - val SHELL: Shell = createRootShell() + var SHELL: Shell = createRootShell() val GLOBAL_MNT_SHELL: Shell = createRootShell(true) } @@ -99,7 +101,7 @@ fun execKsud(args: String, newShell: Boolean = false): Boolean { fun install() { val start = SystemClock.elapsedRealtime() - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so").absolutePath + 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") } @@ -222,7 +224,7 @@ fun runModuleAction( fun restoreBoot( onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO( "${getKsuDaemonPath()} boot-restore -f --magiskboot $magiskboot", onStdout, @@ -235,7 +237,7 @@ fun restoreBoot( fun uninstallPermanently( onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit ): Boolean { - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") val result = flashWithIO("${getKsuDaemonPath()} uninstall --magiskboot $magiskboot", onStdout, onStderr) onFinish(result.isSuccess, result.code) @@ -253,6 +255,7 @@ fun installBoot( bootUri: Uri?, lkm: LkmSelection, ota: Boolean, + partition: String?, onFinish: (Boolean, Int) -> Unit, onStdout: (String) -> Unit, onStderr: (String) -> Unit, @@ -270,7 +273,7 @@ fun installBoot( } } - val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libzakoboot.so") + val magiskboot = File(ksuApp.applicationInfo.nativeLibraryDir, "libmagiskboot.so") var cmd = "boot-patch --magiskboot ${magiskboot.absolutePath}" cmd += if (bootFile == null) { @@ -312,6 +315,10 @@ fun installBoot( 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}") @@ -320,6 +327,11 @@ fun installBoot( // if boot uri is empty, it is direct install, when success, we should show reboot button onFinish(bootUri == null && result.isSuccess, result.code) + + if (bootUri == null && result.isSuccess) { + install() + } + return result.isSuccess } @@ -337,14 +349,6 @@ fun rootAvailable(): Boolean { return shell.isRoot } -fun isAbDevice(): Boolean { - val shell = getRootShell() - return ShellUtils.fastCmd(shell, "getprop ro.build.ab_update").trim().toBoolean() -} - -fun isInitBoot(): Boolean { - return !Os.uname().release.contains("android12-") -} suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { val shell = getRootShell() @@ -354,7 +358,40 @@ suspend fun getCurrentKmi(): String = withContext(Dispatchers.IO) { suspend fun getSupportedKmis(): List = withContext(Dispatchers.IO) { val shell = getRootShell() - val cmd = "boot-info supported-kmi" + 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() } } @@ -419,6 +456,69 @@ fun deleteAppProfileTemplate(id: String): Boolean { return shell.newJob().add("${getKsuDaemonPath()} profile delete-template '${id}'") .to(ArrayList(), null).exec().isSuccess } +// KPM控制 +fun loadKpmModule(path: String, args: String? = null): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm load $path ${args ?: ""}" + return ShellUtils.fastCmd(shell, cmd) +} + +fun unloadKpmModule(name: String): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm unload $name" + return ShellUtils.fastCmd(shell, cmd) +} + +fun getKpmModuleCount(): Int { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm num" + val result = ShellUtils.fastCmd(shell, cmd) + return result.trim().toIntOrNull() ?: 0 +} + +fun runCmd(shell: Shell, cmd: String): String { + return shell.newJob() + .add(cmd) + .to(mutableListOf(), null) + .exec().out + .joinToString("\n") +} + +fun listKpmModules(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm list" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to list KPM modules", e) + "" + } +} + +fun getKpmModuleInfo(name: String): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm info $name" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to get KPM module info: $name", e) + "" + } +} + +fun controlKpmModule(name: String, args: String? = null): Int { + val shell = getRootShell() + val cmd = """${getKsuDaemonPath()} kpm control $name "${args ?: ""}"""" + val result = runCmd(shell, cmd) + return result.trim().toIntOrNull() ?: -1 +} + +fun getKpmVersion(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} kpm version" + val result = ShellUtils.fastCmd(shell, cmd) + return result.trim() +} fun forceStopApp(packageName: String) { val shell = getRootShell() @@ -442,143 +542,59 @@ fun restartApp(packageName: String) { } fun getSuSFSDaemonPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libzakozakozako.so" -} - -fun getSuSFS(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} support") - return result + return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libksu_susfs.so" } fun getSuSFSVersion(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} version") + val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show version") return result } fun getSuSFSVariant(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} variant") + val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} show variant") return result } fun getSuSFSFeatures(): String { val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} features") - return result -} - -fun susfsSUS_SU_0(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 0") - return result -} - -fun susfsSUS_SU_2(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su 2") - return result -} - -fun susfsSUS_SU_Mode(): String { - val shell = getRootShell() - val result = ShellUtils.fastCmd(shell, "${getSuSFSDaemonPath()} sus_su mode") - return result -} - -fun getKpmmgrPath(): String { - return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libkpmmgr.so" -} - - -fun loadKpmModule(path: String, args: String? = null): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} load $path ${args ?: ""}" - return ShellUtils.fastCmd(shell, cmd) -} - -fun unloadKpmModule(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} unload $name" - return ShellUtils.fastCmd(shell, cmd) -} - -fun getKpmModuleCount(): Int { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} num" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim().toIntOrNull() ?: 0 -} - -fun runCmd(shell: Shell, cmd: String): String { - return shell.newJob() - .add(cmd) - .to(mutableListOf(), null) - .exec().out - .joinToString("\n") -} - -fun listKpmModules(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} list" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to list KPM modules", e) - "" - } -} - -fun getKpmModuleInfo(name: String): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} info $name" - return try { - runCmd(shell, cmd).trim() - } catch (e: Exception) { - Log.e(TAG, "Failed to get KPM module info: $name", e) - "" - } -} - -fun controlKpmModule(name: String, args: String? = null): Int { - val shell = getRootShell() - val cmd = """${getKpmmgrPath()} control $name "${args ?: ""}"""" - val result = runCmd(shell, cmd) - return result.trim().toIntOrNull() ?: -1 -} - -fun getKpmVersion(): String { - val shell = getRootShell() - val cmd = "${getKpmmgrPath()} version" - val result = ShellUtils.fastCmd(shell, cmd) - return result.trim() + val cmd = "${getSuSFSDaemonPath()} show enabled_features" + return runCmd(shell, cmd) } fun getZygiskImplement(): String { val shell = getRootShell() - val zygiskPath = "/data/adb/modules/zygisksu" - val rezygiskPath = "/data/adb/modules/rezygisk" - val result = if (ShellUtils.fastCmdResult(shell, "test -f $zygiskPath/module.prop && test ! -f $zygiskPath/disable")) { - ShellUtils.fastCmd(shell, "grep '^name=' $zygiskPath/module.prop | cut -d'=' -f2") - } else if (ShellUtils.fastCmdResult(shell, "test -f $rezygiskPath/module.prop && test ! -f $rezygiskPath/disable")) { - ShellUtils.fastCmd(shell, "grep '^name=' $rezygiskPath/module.prop | cut -d'=' -f2") - } else { - "None" + + val zygiskModuleIds = listOf( + "zygisksu", + "rezygisk", + "shirokozygisk" + ) + + for (moduleId in zygiskModuleIds) { + val modulePath = "/data/adb/modules/$moduleId" + when { + ShellUtils.fastCmdResult(shell, "test -f $modulePath/module.prop && test ! -f $modulePath/disable") -> { + val result = ShellUtils.fastCmd(shell, "grep '^name=' $modulePath/module.prop | cut -d'=' -f2") + Log.i(TAG, "Zygisk implement: $result") + return result + } + } } - Log.i(TAG, "Zygisk implement: $result") - return result + + Log.i(TAG, "Zygisk implement: None") + return "None" } fun getUidScannerDaemonPath(): String { return ksuApp.applicationInfo.nativeLibraryDir + File.separator + "libuid_scanner.so" } +private const val targetPath = "/data/adb/uid_scanner" fun ensureUidScannerExecutable(): Boolean { val shell = getRootShell() val uidScannerPath = getUidScannerDaemonPath() - val targetPath = "/data/adb/uid_scanner" - if (!ShellUtils.fastCmdResult(shell, "test -f $targetPath")) { val copyResult = ShellUtils.fastCmdResult(shell, "cp $uidScannerPath $targetPath") if (!copyResult) { @@ -589,7 +605,6 @@ fun ensureUidScannerExecutable(): Boolean { val result = ShellUtils.fastCmdResult(shell, "chmod 755 $targetPath") return result } -private const val targetPath = "/data/adb/uid_scanner" fun setUidAutoScan(enabled: Boolean): Boolean { val shell = getRootShell() @@ -600,7 +615,10 @@ fun setUidAutoScan(enabled: Boolean): Boolean { val enableValue = if (enabled) 1 else 0 val cmd = "$targetPath --auto-scan $enableValue && $targetPath reload" val result = ShellUtils.fastCmdResult(shell, cmd) - return result + + val throneResult = Natives.setUidScannerEnabled(enabled) + + return result && throneResult } fun setUidMultiUserScan(enabled: Boolean): Boolean { @@ -614,3 +632,96 @@ fun setUidMultiUserScan(enabled: Boolean): Boolean { val result = ShellUtils.fastCmdResult(shell, cmd) return result } + +fun getUidMultiUserScan(): Boolean { + val shell = getRootShell() + + val cmd = "grep 'multi_user_scan=' /data/misc/user_uid/uid_scanner.conf | cut -d'=' -f2" + val result = ShellUtils.fastCmd(shell, cmd).trim() + + return try { + result.toInt() == 1 + } catch (_: NumberFormatException) { + false + } +} + +fun cleanRuntimeEnvironment(): Boolean { + val shell = getRootShell() + return try { + try { + ShellUtils.fastCmd(shell, "/data/adb/uid_scanner stop") + } catch (_: Exception) { + } + ShellUtils.fastCmdResult(shell, "rm -rf /data/misc/user_uid") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/uid_scanner") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/ksu/bin/user_uid") + ShellUtils.fastCmdResult(shell, "rm -rf /data/adb/service.d/uid_scanner.sh") + Natives.clearUidScannerEnvironment() + true + } catch (_: Exception) { + false + } +} + +fun readUidScannerFile(): Boolean { + val shell = getRootShell() + return try { + ShellUtils.fastCmd(shell, "cat /data/adb/ksu/.uid_scanner").trim() == "1" + } catch (_: Exception) { + false + } +} + +fun addUmountPath(path: String, checkMnt: Boolean, flags: Int): Boolean { + val shell = getRootShell() + val checkMntFlag = if (checkMnt) "--check-mnt" else "" + val flagsArg = if (flags >= 0) "--flags $flags" else "" + val cmd = "${getKsuDaemonPath()} umount add $path $checkMntFlag $flagsArg" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "add umount path $path result: $result") + return result +} + +fun removeUmountPath(path: String): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount remove $path" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "remove umount path $path result: $result") + return result +} + +fun listUmountPaths(): String { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount list" + return try { + runCmd(shell, cmd).trim() + } catch (e: Exception) { + Log.e(TAG, "Failed to list umount paths", e) + "" + } +} + +fun clearCustomUmountPaths(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount clear-custom" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "clear custom umount paths result: $result") + return result +} + +fun saveUmountConfig(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount save" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "save umount config result: $result") + return result +} + +fun applyUmountConfigToKernel(): Boolean { + val shell = getRootShell() + val cmd = "${getKsuDaemonPath()} umount apply" + val result = ShellUtils.fastCmdResult(shell, cmd) + Log.i(TAG, "apply umount config to kernel result: $result") + return result +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt deleted file mode 100644 index ff8d881..0000000 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/RestartActivityUtils.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.sukisu.ultra.ui.util - -import android.app.Activity -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.pm.PackageManager -import com.sukisu.ultra.ui.MainActivity - -/** - * 重启应用程序 - **/ - -fun Context.restartApp( - activityClass: Class, - finishCurrent: Boolean = true, - clearTask: Boolean = true, - newTask: Boolean = true -) { - val intent = Intent(this, activityClass) - if (clearTask) intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK) - if (newTask) intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - startActivity(intent) - - if (finishCurrent && this is Activity) { - finish() - } -} - -/** - * 刷新启动器图标 - */ -fun toggleLauncherIcon(context: Context, useAlt: Boolean) { - val pm = context.packageManager - val main = ComponentName(context, MainActivity::class.java.name) - val alt = ComponentName(context, "${MainActivity::class.java.name}Alias") - if (useAlt) { - pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) - pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) - } else { - pm.setComponentEnabledSetting(alt, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP) - pm.setComponentEnabledSetting(main, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP) - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt similarity index 92% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt index 190ce9c..c0f52b1 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleModify.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleModify.kt @@ -1,16 +1,20 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module +import android.app.Activity import android.content.Context import android.content.Intent import android.net.Uri import android.util.Log import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import com.sukisu.ultra.R +import com.sukisu.ultra.ui.util.reboot import kotlinx.coroutines.CompletableDeferred +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -159,7 +163,8 @@ object ModuleModify { val moduleDir = "/data/adb/modules" // 直接从用户选择的文件读取并解压 - val process = Runtime.getRuntime().exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) + val process = Runtime.getRuntime() + .exec(arrayOf("su", "-c", "$busyboxPath tar -xz -C $moduleDir")) context.contentResolver.openInputStream(uri)?.use { input -> input.copyTo(process.outputStream) @@ -277,7 +282,11 @@ object ModuleModify { } } catch (e: Exception) { - Log.e("AllowlistRestore", context.getString(R.string.allowlist_restore_failed, ""), e) + Log.e( + "AllowlistRestore", + context.getString(R.string.allowlist_restore_failed, ""), + e + ) withContext(Dispatchers.Main) { snackBarHost.showSnackbar( context.getString(R.string.allowlist_restore_failed, e.message), @@ -292,11 +301,11 @@ object ModuleModify { fun rememberModuleBackupLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() + scope: CoroutineScope = rememberCoroutineScope() ) = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { backupModules(context, snackBarHost, uri) @@ -309,8 +318,8 @@ object ModuleModify { fun rememberModuleRestoreLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ): androidx.activity.result.ActivityResultLauncher { + scope: CoroutineScope = rememberCoroutineScope() + ): ActivityResultLauncher { var showRestoreDialog by remember { mutableStateOf(false) } var restoreConfirmResult by remember { mutableStateOf?>(null) } @@ -330,7 +339,7 @@ object ModuleModify { return rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { val confirmResult = CompletableDeferred() @@ -353,11 +362,11 @@ object ModuleModify { fun rememberAllowlistBackupLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() + scope: CoroutineScope = rememberCoroutineScope() ) = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { backupAllowlist(context, snackBarHost, uri) @@ -370,10 +379,14 @@ object ModuleModify { fun rememberAllowlistRestoreLauncher( context: Context, snackBarHost: SnackbarHostState, - scope: kotlinx.coroutines.CoroutineScope = rememberCoroutineScope() - ): androidx.activity.result.ActivityResultLauncher { + scope: CoroutineScope = rememberCoroutineScope() + ): ActivityResultLauncher { var showAllowlistRestoreDialog by remember { mutableStateOf(false) } - var allowlistRestoreConfirmResult by remember { mutableStateOf?>(null) } + var allowlistRestoreConfirmResult by remember { + mutableStateOf?>( + null + ) + } // 显示允许列表恢复确认对话框 AllowlistRestoreConfirmationDialog( @@ -391,7 +404,7 @@ object ModuleModify { return rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() ) { result -> - if (result.resultCode == android.app.Activity.RESULT_OK) { + if (result.resultCode == Activity.RESULT_OK) { result.data?.data?.let { uri -> scope.launch { val confirmResult = CompletableDeferred() diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt index 230b99f..5113b4a 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleUtils.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleUtils.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module import android.content.Context import android.content.Intent diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt similarity index 98% rename from manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt rename to manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt index 9e70c40..4194829 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/util/ModuleVerificationManager.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/util/module/ModuleVerificationManager.kt @@ -1,9 +1,10 @@ -package com.sukisu.ultra.ui.util +package com.sukisu.ultra.ui.util.module import android.content.Context import android.net.Uri import android.util.Log import com.sukisu.ultra.Natives +import com.sukisu.ultra.ui.util.getRootShell import java.io.File import java.io.FileOutputStream diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt index 838c229..72e069f 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/HomeViewModel.kt @@ -4,16 +4,11 @@ import android.annotation.SuppressLint import android.content.Context import android.os.Build import android.system.Os -import android.util.Log import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue -import androidx.core.content.edit import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.dergoogler.mmrl.platform.Platform.Companion.context -import com.google.gson.Gson -import com.google.gson.JsonSyntaxException import com.sukisu.ultra.KernelVersion import com.sukisu.ultra.Natives import com.sukisu.ultra.getKernelVersion @@ -21,20 +16,14 @@ import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.util.* import com.sukisu.ultra.ui.util.module.LatestVersionInfo import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow class HomeViewModel : ViewModel() { - companion object { - private const val TAG = "HomeViewModel" - private const val PREFS_NAME = "home_cache" - private const val KEY_SYSTEM_STATUS = "system_status" - private const val KEY_SYSTEM_INFO = "system_info" - private const val KEY_VERSION_INFO = "version_info" - private const val KEY_LAST_UPDATE = "last_update_time" - private const val KEY_ERROR_COUNT = "error_count" - private const val MAX_ERROR_COUNT = 2 - } // 系统状态 data class SystemStatus( @@ -60,7 +49,6 @@ class HomeViewModel : ViewModel() { val suSFSVersion: String = "", val suSFSVariant: String = "", val suSFSFeatures: String = "", - val susSUMode: String = "", val superuserCount: Int = 0, val moduleCount: Int = 0, val kpmModuleCount: Int = 0, @@ -69,9 +57,7 @@ class HomeViewModel : ViewModel() { val zygiskImplement: String = "" ) - private val gson = Gson() - private val prefs by lazy { ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) } - + // 状态变量 var systemStatus by mutableStateOf(SystemStatus()) private set @@ -98,199 +84,52 @@ class HomeViewModel : ViewModel() { var showKpmInfo by mutableStateOf(false) private set - private fun clearAllCache() { - try { - prefs.edit { clear() } - Log.i(TAG, "All cache cleared successfully") - } catch (e: Exception) { - Log.e(TAG, "Error clearing cache", e) - } - } + var isCoreDataLoaded by mutableStateOf(false) + private set + var isExtendedDataLoaded by mutableStateOf(false) + private set + var isRefreshing by mutableStateOf(false) + private set - private fun resetToDefaults() { - systemStatus = SystemStatus() - systemInfo = SystemInfo() - latestVersionInfo = LatestVersionInfo() - isSimpleMode = false - isKernelSimpleMode = false - isHideVersion = false - isHideOtherInfo = false - isHideSusfsStatus = false - isHideZygiskImplement = false - isHideLinkCard = false - showKpmInfo = false - } - - private fun handleError(error: Exception, operation: String) { - Log.e(TAG, "Error in $operation", error) + // 数据刷新状态流,用于监听变化 + private val _dataRefreshTrigger = MutableStateFlow(0L) + val dataRefreshTrigger: StateFlow = _dataRefreshTrigger - val errorCount = prefs.getInt(KEY_ERROR_COUNT, 0) - val newErrorCount = errorCount + 1 - - if (newErrorCount >= MAX_ERROR_COUNT) { - Log.w(TAG, "Too many errors ($newErrorCount), clearing cache and resetting") - clearAllCache() - resetToDefaults() - } else { - prefs.edit { - putInt(KEY_ERROR_COUNT, newErrorCount) - } - } - } - - private fun String?.orSafe(default: String = ""): String { - return if (this.isNullOrBlank()) default else this - } - - private fun Pair?.orSafe(default: Pair): Pair { - return if (this?.first == null || this.second == null) default else Pair(this.first!!, this.second!!) - } + private var loadingJobs = mutableListOf() + private var lastRefreshTime = 0L + private val refreshCooldown = 2000L fun loadUserSettings(context: Context) { viewModelScope.launch(Dispatchers.IO) { - try { - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false) - isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false) - isHideVersion = settingsPrefs.getBoolean("is_hide_version", false) - isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false) - isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false) - isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false) - isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false) - showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false) - } catch (e: Exception) { - handleError(e, "loadUserSettings") - } + val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + isSimpleMode = settingsPrefs.getBoolean("is_simple_mode", false) + isKernelSimpleMode = settingsPrefs.getBoolean("is_kernel_simple_mode", false) + isHideVersion = settingsPrefs.getBoolean("is_hide_version", false) + isHideOtherInfo = settingsPrefs.getBoolean("is_hide_other_info", false) + isHideSusfsStatus = settingsPrefs.getBoolean("is_hide_susfs_status", false) + isHideLinkCard = settingsPrefs.getBoolean("is_hide_link_card", false) + isHideZygiskImplement = settingsPrefs.getBoolean("is_hide_zygisk_Implement", false) + showKpmInfo = settingsPrefs.getBoolean("show_kpm_info", false) } } - fun initializeData() { - viewModelScope.launch { - try { - loadCachedData() - // 成功加载后重置错误计数 - prefs.edit { - putInt(KEY_ERROR_COUNT, 0) - } - } catch(e: Exception) { - handleError(e, "initializeData") - } - } - } + fun loadCoreData() { + if (isCoreDataLoaded) return - private fun loadCachedData() { - try { - prefs.getString(KEY_SYSTEM_STATUS, null)?.let { statusJson -> - try { - val cachedStatus = gson.fromJson(statusJson, SystemStatus::class.java) - if (cachedStatus != null) { - systemStatus = cachedStatus - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid system status JSON, using defaults", e) - } - } - - prefs.getString(KEY_SYSTEM_INFO, null)?.let { infoJson -> - try { - val cachedInfo = gson.fromJson(infoJson, SystemInfo::class.java) - if (cachedInfo != null) { - systemInfo = cachedInfo - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid system info JSON, using defaults", e) - } - } - - prefs.getString(KEY_VERSION_INFO, null)?.let { versionJson -> - try { - val cachedVersion = gson.fromJson(versionJson, LatestVersionInfo::class.java) - if (cachedVersion != null) { - latestVersionInfo = cachedVersion - } - } catch (e: JsonSyntaxException) { - Log.w(TAG, "Invalid version info JSON, using defaults", e) - } - } - } catch (e: Exception) { - Log.e(TAG, "Error loading cached data", e) - throw e - } - } - - private suspend fun fetchAndSaveData() { - try { - fetchSystemStatus() - fetchSystemInfo() - withContext(Dispatchers.IO) { - prefs.edit { - putString(KEY_SYSTEM_STATUS, gson.toJson(systemStatus)) - putString(KEY_SYSTEM_INFO, gson.toJson(systemInfo)) - putString(KEY_VERSION_INFO, gson.toJson(latestVersionInfo)) - putLong(KEY_LAST_UPDATE, System.currentTimeMillis()) - putInt(KEY_ERROR_COUNT, 0) - } - } - } catch (e: Exception) { - handleError(e, "fetchAndSaveData") - } - } - - fun checkForUpdates(context: Context) { - viewModelScope.launch(Dispatchers.IO) { - try { - val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val checkUpdate = settingsPrefs.getBoolean("check_update", true) - - if (checkUpdate) { - val newVersionInfo = checkNewVersion() - latestVersionInfo = newVersionInfo - prefs.edit { - putString(KEY_VERSION_INFO, gson.toJson(newVersionInfo)) - putLong(KEY_LAST_UPDATE, System.currentTimeMillis()) - } - } - } catch (e: Exception) { - handleError(e, "checkForUpdates") - } - } - } - - fun refreshAllData(context: Context) { - viewModelScope.launch { - try { - fetchAndSaveData() - checkForUpdates(context) - } catch (e: Exception) { - handleError(e, "refreshAllData") - } - } - } - - private suspend fun fetchSystemStatus() { - withContext(Dispatchers.IO) { + val job = viewModelScope.launch(Dispatchers.IO) { try { val kernelVersion = getKernelVersion() val isManager = try { - Natives.becomeManager(ksuApp.packageName.orSafe("com.sukisu.ultra")) - } catch (e: Exception) { - Log.w(TAG, "Failed to become manager", e) + Natives.isManager + } catch (_: Exception) { false } - val ksuVersion = if (isManager) { - try { - Natives.version - } catch (e: Exception) { - Log.w(TAG, "Failed to get KSU version", e) - null - } - } else null + val ksuVersion = if (isManager) Natives.version else null val fullVersion = try { - Natives.getFullVersion().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get full version", e) + Natives.getFullVersion() + } catch (_: Exception) { "Unknown" } @@ -309,8 +148,7 @@ class HomeViewModel : ViewModel() { } else { fullVersion } - } catch (e: Exception) { - Log.w(TAG, "Failed to process full version", e) + } catch (_: Exception) { fullVersion } } else { @@ -318,34 +156,24 @@ class HomeViewModel : ViewModel() { } val lkmMode = ksuVersion?.let { - try { - if (it >= Natives.MINIMAL_SUPPORTED_KERNEL_LKM && kernelVersion.isGKI()) { - Natives.isLkmMode - } else null - } catch (e: Exception) { - Log.w(TAG, "Failed to get LKM mode", e) - null - } + if (kernelVersion.isGKI()) Natives.isLkmMode else null } val isRootAvailable = try { rootAvailable() - } catch (e: Exception) { - Log.w(TAG, "Failed to check root availability", e) + } catch (_: Exception) { false } val isKpmConfigured = try { Natives.isKPMEnabled() - } catch (e: Exception) { - Log.w(TAG, "Failed to check KPM status", e) + } catch (_: Exception) { false } val requireNewKernel = try { isManager && Natives.requireNewKernel() - } catch (e: Exception) { - Log.w(TAG, "Failed to check kernel requirement", e) + } catch (_: Exception) { false } @@ -359,198 +187,321 @@ class HomeViewModel : ViewModel() { isKpmConfigured = isKpmConfigured, requireNewKernel = requireNewKernel ) - } catch (e: Exception) { - Log.e(TAG, "Error fetching system status", e) - throw e + + isCoreDataLoaded = true + } catch (_: Exception) { } } + loadingJobs.add(job) } - @SuppressLint("RestrictedApi") - private suspend fun fetchSystemInfo() { - withContext(Dispatchers.IO) { + fun loadExtendedData(context: Context) { + if (isExtendedDataLoaded) return + + val job = viewModelScope.launch(Dispatchers.IO) { try { - val uname = try { - Os.uname() - } catch (e: Exception) { - Log.w(TAG, "Failed to get uname", e) - null - } + // 分批加载 + delay(50) - val kpmVersion = try { - getKpmVersion().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get kpm version", e) - "Unknown" - } - - val suSFS = try { - getSuSFS().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS", e) - "Unknown" - } - - var suSFSVersion = "" - var suSFSVariant = "" - var suSFSFeatures = "" - var susSUMode = "" - - if (suSFS == "Supported") { - suSFSVersion = try { - getSuSFSVersion().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS version", e) - "" - } - - if (suSFSVersion.isNotEmpty()) { - suSFSVariant = try { - getSuSFSVariant().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS variant", e) - "" - } - - suSFSFeatures = try { - getSuSFSFeatures().orSafe("") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SuSFS features", e) - "" - } - - val isSUS_SU = suSFSFeatures == "CONFIG_KSU_SUSFS_SUS_SU" - if (isSUS_SU) { - susSUMode = try { - susfsSUS_SU_Mode() - } catch (e: Exception) { - Log.w(TAG, "Failed to get SUS SU mode", e) - "" - } - } - } - } - - // 获取动态管理器状态和管理器列表 - val dynamicSignConfig = try { - Natives.getDynamicManager() - } catch (e: Exception) { - Log.w(TAG, "Failed to get dynamic manager config", e) - null - } - - val isDynamicSignEnabled = try { - dynamicSignConfig?.isValid() == true - } catch (e: Exception) { - Log.w(TAG, "Failed to check dynamic manager validity", e) - false - } - - val managersList = if (isDynamicSignEnabled) { - try { - Natives.getManagersList() - } catch (e: Exception) { - Log.w(TAG, "Failed to get managers list", e) - null - } - } else { - null - } - - val deviceModel = try { - getDeviceModel().orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get device model", e) - "Unknown" - } - - val managerVersion = try { - getManagerVersion(ksuApp.applicationContext).orSafe(Pair("Unknown", 0L)) - } catch (e: Exception) { - Log.w(TAG, "Failed to get manager version", e) - Pair("Unknown", 0L) - } - - val seLinuxStatus = try { - getSELinuxStatus(context).orSafe("Unknown") - } catch (e: Exception) { - Log.w(TAG, "Failed to get SELinux status", e) - "Unknown" - } - - val superuserCount = try { - getSuperuserCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get superuser count", e) - 0 - } - - val moduleCount = try { - getModuleCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get module count", e) - 0 - } - - val kpmModuleCount = try { - getKpmModuleCount() - } catch (e: Exception) { - Log.w(TAG, "Failed to get kpm module count", e) - 0 - } - - val zygiskImplement = try { - getZygiskImplement().orSafe("None") - } catch (e: Exception) { - Log.w(TAG, "Failed to get Zygisk implement", e) - "None" - } - - systemInfo = SystemInfo( - kernelRelease = uname?.release.orSafe("Unknown"), - androidVersion = Build.VERSION.RELEASE.orSafe("Unknown"), - deviceModel = deviceModel, - managerVersion = managerVersion, - seLinuxStatus = seLinuxStatus, - kpmVersion = kpmVersion, - suSFSStatus = suSFS, - suSFSVersion = suSFSVersion, - suSFSVariant = suSFSVariant, - suSFSFeatures = suSFSFeatures, - susSUMode = susSUMode, - superuserCount = superuserCount, - moduleCount = moduleCount, - kpmModuleCount = kpmModuleCount, - managersList = managersList, - isDynamicSignEnabled = isDynamicSignEnabled, - zygiskImplement = zygiskImplement + val basicInfo = loadBasicSystemInfo(context) + systemInfo = systemInfo.copy( + kernelRelease = basicInfo.first, + androidVersion = basicInfo.second, + deviceModel = basicInfo.third, + managerVersion = basicInfo.fourth, + seLinuxStatus = basicInfo.fifth ) - } catch (e: Exception) { - Log.e(TAG, "Error fetching system info", e) - throw e + + delay(100) + + // 加载模块信息 + if (!isSimpleMode) { + val moduleInfo = loadModuleInfo() + systemInfo = systemInfo.copy( + kpmVersion = moduleInfo.first, + superuserCount = moduleInfo.second, + moduleCount = moduleInfo.third, + kpmModuleCount = moduleInfo.fourth, + zygiskImplement = moduleInfo.fifth + ) + } + + delay(100) + + // 加载SuSFS信息 + if (!isHideSusfsStatus) { + val suSFSInfo = loadSuSFSInfo() + systemInfo = systemInfo.copy( + suSFSStatus = suSFSInfo.first, + suSFSVersion = suSFSInfo.second, + suSFSVariant = suSFSInfo.third, + suSFSFeatures = suSFSInfo.fourth, + ) + } + + delay(100) + + // 加载管理器列表 + val managerInfo = loadManagerInfo() + systemInfo = systemInfo.copy( + managersList = managerInfo.first, + isDynamicSignEnabled = managerInfo.second + ) + + isExtendedDataLoaded = true + } catch (_: Exception) { + // 静默处理错误 + } + } + loadingJobs.add(job) + } + + fun refreshData(context: Context, forceRefresh: Boolean = false) { + val currentTime = System.currentTimeMillis() + + // 如果不是强制刷新,检查冷却时间 + if (!forceRefresh && currentTime - lastRefreshTime < refreshCooldown) { + return + } + + lastRefreshTime = currentTime + + viewModelScope.launch { + isRefreshing = true + + try { + // 取消正在进行的加载任务 + loadingJobs.forEach { it.cancel() } + loadingJobs.clear() + + // 重置状态 + isCoreDataLoaded = false + isExtendedDataLoaded = false + + // 触发数据刷新状态流 + _dataRefreshTrigger.value = currentTime + + // 重新加载用户设置 + loadUserSettings(context) + + // 重新加载核心数据 + loadCoreData() + delay(100) + + // 重新加载扩展数据 + loadExtendedData(context) + + // 检查更新 + val settingsPrefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val checkUpdate = settingsPrefs.getBoolean("check_update", true) + if (checkUpdate) { + try { + val newVersionInfo = withContext(Dispatchers.IO) { + checkNewVersion() + } + latestVersionInfo = newVersionInfo + } catch (_: Exception) { + } + } + } catch (_: Exception) { + // 静默处理错误 + } finally { + isRefreshing = false } } } - private fun getDeviceInfo(): String { - return try { - var manufacturer = Build.MANUFACTURER.orSafe("Unknown") - manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1) + // 手动触发刷新(下拉刷新使用) + fun onPullRefresh(context: Context) { + refreshData(context, forceRefresh = true) + } - val brand = Build.BRAND.orSafe("") - if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) { - manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1) + // 自动刷新数据(当检测到变化时) + fun autoRefreshIfNeeded(context: Context) { + viewModelScope.launch { + // 检查是否需要刷新数据 + val needsRefresh = checkIfDataNeedsRefresh() + if (needsRefresh) { + refreshData(context) + } + } + } + + private suspend fun checkIfDataNeedsRefresh(): Boolean { + return withContext(Dispatchers.IO) { + try { + // 检查KSU状态是否发生变化 + val currentKsuVersion = try { + if (Natives.isManager) { + Natives.version + } else null + } catch (_: Exception) { + null + } + + // 如果KSU版本发生变化,需要刷新 + if (currentKsuVersion != systemStatus.ksuVersion) { + return@withContext true + } + + // 检查模块数量是否发生变化 + val currentModuleCount = try { + getModuleCount() + } catch (_: Exception) { + systemInfo.moduleCount + } + + if (currentModuleCount != systemInfo.moduleCount) { + return@withContext true + } + + false + } catch (_: Exception) { + false + } + } + } + + private suspend fun loadBasicSystemInfo(context: Context): Tuple5, String> { + return withContext(Dispatchers.IO) { + val uname = try { + Os.uname() + } catch (_: Exception) { + null } - val model = Build.MODEL.orSafe("") - if (model.isNotEmpty()) { - manufacturer += " $model " + val deviceModel = try { + getDeviceModel() + } catch (_: Exception) { + "Unknown" } - manufacturer - } catch (e: Exception) { - Log.w(TAG, "Failed to get device info", e) - "Unknown Device" + val managerVersion = try { + getManagerVersion(context) + } catch (_: Exception) { + Pair("Unknown", 0L) + } + + val seLinuxStatus = try { + getSELinuxStatus(ksuApp.applicationContext) + } catch (_: Exception) { + "Unknown" + } + + Tuple5( + uname?.release ?: "Unknown", + Build.VERSION.RELEASE ?: "Unknown", + deviceModel, + managerVersion, + seLinuxStatus + ) + } + } + + private suspend fun loadModuleInfo(): Tuple5 { + return withContext(Dispatchers.IO) { + val kpmVersion = try { + getKpmVersion() + } catch (_: Exception) { + "Unknown" + } + + val superuserCount = try { + getSuperuserCount() + } catch (_: Exception) { + 0 + } + + val moduleCount = try { + getModuleCount() + } catch (_: Exception) { + 0 + } + + val kpmModuleCount = try { + getKpmModuleCount() + } catch (_: Exception) { + 0 + } + + val zygiskImplement = try { + getZygiskImplement() + } catch (_: Exception) { + "None" + } + + Tuple5(kpmVersion, superuserCount, moduleCount, kpmModuleCount, zygiskImplement) + } + } + + private suspend fun loadSuSFSInfo(): Tuple4 { + return withContext(Dispatchers.IO) { + val suSFS = try { + val rawFeature = getSuSFSFeatures() + if (rawFeature.isNotEmpty() && !rawFeature.startsWith("[-]")) { + "Supported" + } else { + rawFeature + } + } catch (_: Exception) { + "Unknown" + } + + if (suSFS != "Supported") { + return@withContext Tuple4(suSFS, "", "", "") + } + + val suSFSVersion = try { + getSuSFSVersion() + } catch (_: Exception) { + "" + } + + if (suSFSVersion.isEmpty()) { + return@withContext Tuple4(suSFS, "", "", "") + } + + val suSFSVariant = try { + getSuSFSVariant() + } catch (_: Exception) { + "" + } + + val suSFSFeatures = try { + getSuSFSFeatures() + } catch (_: Exception) { + "" + } + + Tuple4(suSFS, suSFSVersion, suSFSVariant, suSFSFeatures) + } + } + + private suspend fun loadManagerInfo(): Pair { + return withContext(Dispatchers.IO) { + val dynamicSignConfig = try { + Natives.getDynamicManager() + } catch (_: Exception) { + null + } + + val isDynamicSignEnabled = try { + dynamicSignConfig?.isValid() == true + } catch (_: Exception) { + false + } + + val managersList = if (isDynamicSignEnabled) { + try { + Natives.getManagersList() + } catch (_: Exception) { + null + } + } else { + null + } + + Pair(managersList, isDynamicSignEnabled) } } @@ -560,10 +511,10 @@ class HomeViewModel : ViewModel() { val systemProperties = Class.forName("android.os.SystemProperties") val getMethod = systemProperties.getMethod("get", String::class.java, String::class.java) val marketNameKeys = listOf( - "ro.product.marketname", // Xiaomi - "ro.vendor.oplus.market.name", // Oppo, OnePlus, Realme - "ro.vivo.market.name", // Vivo - "ro.config.marketing_name" // Huawei + "ro.product.marketname", + "ro.vendor.oplus.market.name", + "ro.vivo.market.name", + "ro.config.marketing_name" ) var result = getDeviceInfo() for (key in marketNameKeys) { @@ -573,26 +524,67 @@ class HomeViewModel : ViewModel() { result = marketName break } - } catch (e: Exception) { - Log.w(TAG, "Failed to get market name for key: $key", e) + } catch (_: Exception) { } } result - } catch (e: Exception) { - Log.w(TAG, "Error getting device model", e) + } catch ( + + _: Exception) { getDeviceInfo() } } + private fun getDeviceInfo(): String { + return try { + var manufacturer = Build.MANUFACTURER ?: "Unknown" + manufacturer = manufacturer[0].uppercaseChar().toString() + manufacturer.substring(1) + + val brand = Build.BRAND ?: "" + if (brand.isNotEmpty() && !brand.equals(Build.MANUFACTURER, ignoreCase = true)) { + manufacturer += " " + brand[0].uppercaseChar() + brand.substring(1) + } + + val model = Build.MODEL ?: "" + if (model.isNotEmpty()) { + manufacturer += " $model " + } + + manufacturer + } catch (_: Exception) { + "Unknown Device" + } + } + private fun getManagerVersion(context: Context): Pair { return try { val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0) val versionCode = androidx.core.content.pm.PackageInfoCompat.getLongVersionCode(packageInfo) - val versionName = packageInfo.versionName.orSafe("Unknown") + val versionName = packageInfo.versionName ?: "Unknown" Pair(versionName, versionCode) - } catch (e: Exception) { - Log.w(TAG, "Error getting manager version", e) + } catch (_: Exception) { Pair("Unknown", 0L) } } + + data class Tuple5( + val first: T1, + val second: T2, + val third: T3, + val fourth: T4, + val fifth: T5 + ) + + data class Tuple4( + val first: T1, + val second: T2, + val third: T3, + val fourth: T4 + ) + + override fun onCleared() { + super.onCleared() + loadingJobs.forEach { it.cancel() } + loadingJobs.clear() + } } \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt index f8dfe2e..91f66d8 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/ModuleViewModel.kt @@ -16,7 +16,7 @@ import kotlinx.coroutines.launch import com.sukisu.ultra.ui.util.HanziToPinyin import com.sukisu.ultra.ui.util.listModules import com.sukisu.ultra.ui.util.getRootShell -import com.sukisu.ultra.ui.util.ModuleVerificationManager +import com.sukisu.ultra.ui.util.module.ModuleVerificationManager import kotlinx.coroutines.withContext import org.json.JSONArray import org.json.JSONObject diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt index 0bd0320..bfa2949 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/viewmodel/SuperUserViewModel.kt @@ -3,9 +3,9 @@ package com.sukisu.ultra.ui.viewmodel import android.content.* import android.content.pm.ApplicationInfo import android.content.pm.PackageInfo +import android.graphics.drawable.Drawable import android.os.IBinder import android.os.Parcelable -import android.os.SystemClock import android.util.Log import androidx.compose.runtime.* import androidx.core.content.edit @@ -13,7 +13,7 @@ import androidx.lifecycle.ViewModel import com.sukisu.ultra.Natives import com.sukisu.ultra.ksuApp import com.sukisu.ultra.ui.KsuService -import com.sukisu.ultra.ui.util.HanziToPinyin +import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.Shell import kotlinx.coroutines.* import kotlinx.coroutines.sync.Mutex @@ -26,8 +26,8 @@ import java.util.concurrent.ThreadPoolExecutor import java.util.concurrent.TimeUnit import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine +import com.sukisu.zako.IKsuInterface -// 应用分类 enum class AppCategory(val displayNameRes: Int, val persistKey: String) { ALL(com.sukisu.ultra.R.string.category_all_apps, "ALL"), ROOT(com.sukisu.ultra.R.string.category_root_apps, "ROOT"), @@ -35,13 +35,10 @@ enum class AppCategory(val displayNameRes: Int, val persistKey: String) { DEFAULT(com.sukisu.ultra.R.string.category_default_apps, "DEFAULT"); companion object { - fun fromPersistKey(key: String): AppCategory { - return entries.find { it.persistKey == key } ?: ALL - } + fun fromPersistKey(key: String): AppCategory = entries.find { it.persistKey == key } ?: ALL } } -// 排序方式 enum class SortType(val displayNameRes: Int, val persistKey: String) { NAME_ASC(com.sukisu.ultra.R.string.sort_name_asc, "NAME_ASC"), NAME_DESC(com.sukisu.ultra.R.string.sort_name_desc, "NAME_DESC"), @@ -52,20 +49,24 @@ enum class SortType(val displayNameRes: Int, val persistKey: String) { USAGE_FREQ(com.sukisu.ultra.R.string.sort_usage_freq, "USAGE_FREQ"); companion object { - fun fromPersistKey(key: String): SortType { - return entries.find { it.persistKey == key } ?: NAME_ASC - } + fun fromPersistKey(key: String): SortType = entries.find { it.persistKey == key } ?: NAME_ASC } } -/** - * @author ShirkNeko - * @date 2025/5/31. - */ class SuperUserViewModel : ViewModel() { companion object { private const val TAG = "SuperUserViewModel" + private val appsLock = Any() var apps by mutableStateOf>(emptyList()) + var appGroups by mutableStateOf>(emptyList()) + + @JvmStatic + fun getAppIconDrawable(context: Context, packageName: String): Drawable? { + val appList = synchronized(appsLock) { apps } + return appList.find { it.packageName == packageName } + ?.packageInfo?.applicationInfo?.loadIcon(context.packageManager) + } + private const val PREFS_NAME = "settings" private const val KEY_SHOW_SYSTEM_APPS = "show_system_apps" private const val KEY_SELECTED_CATEGORY = "selected_category" @@ -82,31 +83,29 @@ class SuperUserViewModel : ViewModel() { val packageInfo: PackageInfo, val profile: Natives.Profile?, ) : Parcelable { - val packageName: String - get() = packageInfo.packageName - val uid: Int - get() = packageInfo.applicationInfo!!.uid + val packageName: String get() = packageInfo.packageName + val uid: Int get() = packageInfo.applicationInfo!!.uid + } - val allowSu: Boolean - get() = profile != null && profile.allowSu + @Parcelize + data class AppGroup( + val uid: Int, + val apps: List, + val profile: Natives.Profile? + ) : Parcelable { + val mainApp: AppInfo get() = apps.first() + val packageNames: List get() = apps.map { it.packageName } + val allowSu: Boolean get() = profile?.allowSu == true + + val userName: String? get() = Natives.getUserName(uid) val hasCustomProfile: Boolean - get() { - if (profile == null) { - return false - } - return if (profile.allowSu) { - !profile.rootUseDefault - } else { - !profile.nonRootUseDefault - } - } + get() = profile?.let { + if (it.allowSu) !it.rootUseDefault else !it.nonRootUseDefault + } ?: false } private val appProcessingThreadPool = ThreadPoolExecutor( - CORE_POOL_SIZE, - MAX_POOL_SIZE, - KEEP_ALIVE_TIME, - TimeUnit.SECONDS, + CORE_POOL_SIZE, MAX_POOL_SIZE, KEEP_ALIVE_TIME, TimeUnit.SECONDS, LinkedBlockingQueue() ) { runnable -> Thread(runnable, "AppProcessing-${System.currentTimeMillis()}").apply { @@ -116,65 +115,40 @@ class SuperUserViewModel : ViewModel() { }.asCoroutineDispatcher() private val appListMutex = Mutex() - private val configChangeListeners = mutableSetOf<(String) -> Unit>() - - private val prefs: SharedPreferences = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) + private val prefs = ksuApp.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE) var search by mutableStateOf("") - - var showSystemApps by mutableStateOf(loadShowSystemApps()) + var showSystemApps by mutableStateOf(prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false)) private set - var selectedCategory by mutableStateOf(loadSelectedCategory()) private set - var currentSortType by mutableStateOf(loadCurrentSortType()) private set var isRefreshing by mutableStateOf(false) private set - - // 批量操作相关状态 var showBatchActions by mutableStateOf(false) internal set var selectedApps by mutableStateOf>(emptySet()) internal set - - // 加载进度状态 var loadingProgress by mutableFloatStateOf(0f) private set - var loadingMessage by mutableStateOf("") - private set - /** - * 从SharedPreferences加载显示系统应用设置 - */ - private fun loadShowSystemApps(): Boolean { - return prefs.getBoolean(KEY_SHOW_SYSTEM_APPS, false) - } - - /** - * 从SharedPreferences加载选择的应用分类 - */ private fun loadSelectedCategory(): AppCategory { - val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) ?: AppCategory.ALL.persistKey + val categoryKey = prefs.getString(KEY_SELECTED_CATEGORY, AppCategory.ALL.persistKey) + ?: AppCategory.ALL.persistKey return AppCategory.fromPersistKey(categoryKey) } - /** - * 从SharedPreferences加载当前排序方式 - */ private fun loadCurrentSortType(): SortType { - val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) ?: SortType.NAME_ASC.persistKey + val sortKey = prefs.getString(KEY_CURRENT_SORT_TYPE, SortType.NAME_ASC.persistKey) + ?: SortType.NAME_ASC.persistKey return SortType.fromPersistKey(sortKey) } - /** - * 更新显示系统应用设置并保存到SharedPreferences - */ fun updateShowSystemApps(newValue: Boolean) { showSystemApps = newValue - saveShowSystemApps(newValue) + prefs.edit { putBoolean(KEY_SHOW_SYSTEM_APPS, newValue) } notifyAppListChanged() } @@ -184,88 +158,21 @@ class SuperUserViewModel : ViewModel() { apps = currentApps } - /** - * 更新选择的应用分类并保存到SharedPreferences - */ fun updateSelectedCategory(newCategory: AppCategory) { selectedCategory = newCategory - saveSelectedCategory(newCategory) + prefs.edit { putString(KEY_SELECTED_CATEGORY, newCategory.persistKey) } } - /** - * 更新当前排序方式并保存到SharedPreferences - */ fun updateCurrentSortType(newSortType: SortType) { currentSortType = newSortType - saveCurrentSortType(newSortType) + prefs.edit { putString(KEY_CURRENT_SORT_TYPE, newSortType.persistKey) } } - /** - * 保存显示系统应用设置到SharedPreferences - */ - private fun saveShowSystemApps(value: Boolean) { - prefs.edit { - putBoolean(KEY_SHOW_SYSTEM_APPS, value) - } - Log.d(TAG, "Saved show system apps: $value") - } - - /** - * 保存选择的应用分类到SharedPreferences - */ - private fun saveSelectedCategory(category: AppCategory) { - prefs.edit { - putString(KEY_SELECTED_CATEGORY, category.persistKey) - } - Log.d(TAG, "Saved selected category: ${category.persistKey}") - } - - /** - * 保存当前排序方式到SharedPreferences - */ - private fun saveCurrentSortType(sortType: SortType) { - prefs.edit { - putString(KEY_CURRENT_SORT_TYPE, sortType.persistKey) - } - Log.d(TAG, "Saved current sort type: ${sortType.persistKey}") - } - - private val sortedList by derivedStateOf { - val comparator = compareBy { - when { - it.allowSu -> 0 - it.hasCustomProfile -> 1 - else -> 2 - } - }.then(compareBy(Collator.getInstance(Locale.getDefault()), AppInfo::label)) - apps.sortedWith(comparator).also { - isRefreshing = false - } - } - - val appList by derivedStateOf { - val filtered = sortedList.filter { - it.label.contains(search, true) || it.packageName.contains( - search, - true - ) || HanziToPinyin.getInstance() - .toPinyinString(it.label).contains(search, true) - }.filter { - it.uid == 2000 || showSystemApps || it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 - } - - filtered - } - - // 切换批量操作模式 fun toggleBatchMode() { showBatchActions = !showBatchActions - if (!showBatchActions) { - clearSelection() - } + if (!showBatchActions) clearSelection() } - // 切换应用选择状态 fun toggleAppSelection(packageName: String) { selectedApps = if (selectedApps.contains(packageName)) { selectedApps - packageName @@ -274,35 +181,14 @@ class SuperUserViewModel : ViewModel() { } } - // 清除所有选择 fun clearSelection() { selectedApps = emptySet() } - // 批量更新权限 - suspend fun updateBatchPermissions(allowSu: Boolean) { - selectedApps.forEach { packageName -> - val app = apps.find { it.packageName == packageName } - app?.let { - val profile = Natives.getAppProfile(packageName, it.uid) - val updatedProfile = profile.copy(allowSu = allowSu) - if (Natives.setAppProfile(updatedProfile)) { - updateAppProfileLocally(packageName, updatedProfile) - notifyConfigChange(packageName) - } - } - } - clearSelection() - showBatchActions = false - refreshAppConfigurations() - } - - // 批量更新权限和umount模块设置 suspend fun updateBatchPermissions(allowSu: Boolean, umountModules: Boolean? = null) { selectedApps.forEach { packageName -> - val app = apps.find { it.packageName == packageName } - app?.let { - val profile = Natives.getAppProfile(packageName, it.uid) + apps.find { it.packageName == packageName }?.let { app -> + val profile = Natives.getAppProfile(packageName, app.uid) val updatedProfile = profile.copy( allowSu = allowSu, umountModules = umountModules ?: profile.umountModules, @@ -319,7 +205,6 @@ class SuperUserViewModel : ViewModel() { refreshAppConfigurations() } - // 更新本地应用配置 fun updateAppProfileLocally(packageName: String, updatedProfile: Natives.Profile) { appListMutex.tryLock().let { locked -> if (locked) { @@ -327,9 +212,7 @@ class SuperUserViewModel : ViewModel() { apps = apps.map { app -> if (app.packageName == packageName) { app.copy(profile = updatedProfile) - } else { - app - } + } else app } } finally { appListMutex.unlock() @@ -348,15 +231,11 @@ class SuperUserViewModel : ViewModel() { } } - /** - * 刷新应用配置状态 - */ suspend fun refreshAppConfigurations() { withContext(appProcessingThreadPool) { supervisorScope { val currentApps = apps.toList() val batches = currentApps.chunked(BATCH_SIZE) - loadingProgress = 0f val updatedApps = batches.mapIndexed { batchIndex, batch -> @@ -370,59 +249,45 @@ class SuperUserViewModel : ViewModel() { app } } - - val progress = (batchIndex + 1).toFloat() / batches.size - loadingProgress = progress - + loadingProgress = (batchIndex + 1).toFloat() / batches.size batchResult } }.awaitAll().flatten() - appListMutex.withLock { - apps = updatedApps - } - + appListMutex.withLock { apps = updatedApps } loadingProgress = 1f - - Log.i(TAG, "Refreshed configurations for ${updatedApps.size} apps") } } } private var serviceConnection: ServiceConnection? = null - private suspend fun connectKsuService( - onDisconnect: () -> Unit = {} - ): IBinder? = suspendCoroutine { continuation -> - val connection = object : ServiceConnection { - override fun onServiceDisconnected(name: ComponentName?) { - onDisconnect() - serviceConnection = null + private suspend fun connectKsuService(onDisconnect: () -> Unit = {}): IBinder? = + suspendCoroutine { continuation -> + val connection = object : ServiceConnection { + override fun onServiceDisconnected(name: ComponentName?) { + onDisconnect() + serviceConnection = null + } + override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { + continuation.resume(binder) + } } - - override fun onServiceConnected(name: ComponentName?, binder: IBinder?) { - continuation.resume(binder) + serviceConnection = connection + val intent = Intent(ksuApp, KsuService::class.java) + try { + val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( + intent, Shell.EXECUTOR, connection + ) + task?.let { Shell.getShell().execTask(it) } + } catch (e: Exception) { + Log.e(TAG, "Failed to bind KsuService", e) + continuation.resume(null) } } - serviceConnection = connection - val intent = Intent(ksuApp, KsuService::class.java) - - try { - val task = com.topjohnwu.superuser.ipc.RootService.bindOrTask( - intent, - Shell.EXECUTOR, - connection - ) - task?.let { Shell.getShell().execTask(it) } - } catch (e: Exception) { - Log.e(TAG, "Failed to bind KsuService", e) - continuation.resume(null) - } - } - private fun stopKsuService() { - serviceConnection?.let { connection -> + serviceConnection?.let { try { val intent = Intent(ksuApp, KsuService::class.java) com.topjohnwu.superuser.ipc.RootService.stop(intent) @@ -437,60 +302,77 @@ class SuperUserViewModel : ViewModel() { isRefreshing = true loadingProgress = 0f - val result = connectKsuService { - Log.w(TAG, "KsuService disconnected") - } - - if (result == null) { - Log.e(TAG, "Failed to connect to KsuService") - isRefreshing = false - return - } + val binder = connectKsuService() ?: run { isRefreshing = false; return } withContext(Dispatchers.IO) { val pm = ksuApp.packageManager - val start = SystemClock.elapsedRealtime() + val allPackages = IKsuInterface.Stub.asInterface(binder) + val total = allPackages.packageCount + val pageSize = 100 + val result = mutableListOf() - try { - val service = KsuService.Stub.asInterface(result) - val allPackages = service?.getPackages(0) + var start = 0 + while (start < total) { + val page = allPackages.getPackages(start, pageSize) + if (page.isEmpty()) break - withContext(Dispatchers.Main) { - stopKsuService() + result += page.mapNotNull { packageInfo -> + packageInfo.applicationInfo?.let { appInfo -> + AppInfo( + label = appInfo.loadLabel(pm).toString(), + packageInfo = packageInfo, + profile = Natives.getAppProfile(packageInfo.packageName, appInfo.uid) + ) + } } - loadingProgress = 0.3f - - val packages = allPackages?.list ?: emptyList() - - apps = packages.map { packageInfo -> - val appInfo = packageInfo.applicationInfo!! - val uid = appInfo.uid - val profile = Natives.getAppProfile(packageInfo.packageName, uid) - AppInfo( - label = appInfo.loadLabel(pm).toString(), - packageInfo = packageInfo, - profile = profile, - ) - }.filter { it.packageName != ksuApp.packageName } - - loadingProgress = 1f - - Log.i(TAG, "load cost: ${SystemClock.elapsedRealtime() - start}") - } catch (e: Exception) { - Log.e(TAG, "Error fetching app list", e) - withContext(Dispatchers.Main) { - stopKsuService() - } - } finally { - isRefreshing = false - loadingProgress = 0f - loadingMessage = "" + start += page.size + loadingProgress = start.toFloat() / total } + + stopKsuService() + + appListMutex.withLock { + val filteredApps = result.filter { it.packageName != ksuApp.packageName } + apps = filteredApps + appGroups = groupAppsByUid(filteredApps) + } + loadingProgress = 1f + } + isRefreshing = false + } + + val appGroupList by derivedStateOf { + appGroups.filter { group -> + group.apps.any { app -> + app.label.contains(search, true) || + app.packageName.contains(search, true) || + HanziToPinyin.getInstance().toPinyinString(app.label).contains(search, true) + } + }.filter { group -> + group.uid == 2000 || showSystemApps || + group.apps.any { it.packageInfo.applicationInfo!!.flags.and(ApplicationInfo.FLAG_SYSTEM) == 0 } } } - /** - * 清理资源 - */ + + private fun groupAppsByUid(appList: List): List { + return appList.groupBy { it.uid } + .map { (uid, apps) -> + val sortedApps = apps.sortedBy { it.label } + val profile = apps.firstOrNull()?.let { Natives.getAppProfile(it.packageName, uid) } + AppGroup(uid = uid, apps = sortedApps, profile = profile) + } + .sortedWith( + compareBy { + when { + it.allowSu -> 0 + it.hasCustomProfile -> 1 + else -> 2 + } + }.thenBy(Collator.getInstance(Locale.getDefault())) { + it.userName?.takeIf { name -> name.isNotBlank() } ?: it.uid.toString() + }.thenBy(Collator.getInstance(Locale.getDefault())) { it.mainApp.label } + ) +} override fun onCleared() { super.onCleared() try { diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java new file mode 100644 index 0000000..495be9f --- /dev/null +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/AppIconUtil.java @@ -0,0 +1,47 @@ +package com.sukisu.ultra.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 com.sukisu.ultra.ui.viewmodel.SuperUserViewModel; + +public class AppIconUtil { + // Limit cache size to 200 icons + private static final int CACHE_SIZE = 200; + private static final LruCache iconCache = new LruCache<>(CACHE_SIZE); + + public static synchronized Bitmap loadAppIconSync(Context context, String packageName, int sizePx) { + Bitmap cached = iconCache.get(packageName); + if (cached != null) return cached; + + try { + Drawable drawable = SuperUserViewModel.getAppIconDrawable(context, packageName); + if (drawable == null) { + return null; + } + Bitmap raw = drawableToBitmap(drawable, sizePx); + Bitmap icon = Bitmap.createScaledBitmap(raw, sizePx, sizePx, true); + if (raw != icon) raw.recycle(); + iconCache.put(packageName, icon); + return icon; + } catch (Exception e) { + return null; + } + } + + private static Bitmap drawableToBitmap(Drawable drawable, int size) { + if (drawable instanceof BitmapDrawable) return ((BitmapDrawable) drawable).getBitmap(); + + int width = drawable.getIntrinsicWidth() > 0 ? drawable.getIntrinsicWidth() : size; + int height = drawable.getIntrinsicHeight() > 0 ? drawable.getIntrinsicHeight() : size; + + Bitmap bmp = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmp); + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + return bmp; + } +} diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt index 7650def..dad41e3 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/KsuLibSuProvider.kt @@ -1,7 +1,6 @@ package com.sukisu.ultra.ui.webui import android.content.ServiceConnection -import android.content.pm.PackageInfo import android.util.Log import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.model.IProvider @@ -18,7 +17,7 @@ class KsuLibSuProvider : IProvider { override fun isAvailable() = true - override suspend fun isAuthorized() = Natives.becomeManager(ksuApp.packageName) + override suspend fun isAuthorized() = Natives.isManager private val serviceIntent get() = PlatformIntent( @@ -54,19 +53,4 @@ suspend fun initPlatform() = withContext(Dispatchers.IO) { Log.e("KsuLibSu", "Failed to initialize platform", e) return@withContext false } -} - -fun Platform.Companion.getInstalledPackagesAll(catch: (Exception) -> Unit = {}): List = - try { - val packages = mutableListOf() - val userInfos = userManager.getUsers() - - for (userInfo in userInfos) { - packages.addAll(packageManager.getInstalledPackages(0, userInfo.id)) - } - - packages - } catch (e: Exception) { - catch(e) - packageManager.getInstalledPackages(0, userManager.myUserId) - } \ No newline at end of file +} \ No newline at end of file diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt index f1b7773..8a924df 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIActivity.kt @@ -11,18 +11,23 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.activity.ComponentActivity import androidx.activity.enableEdgeToEdge +import androidx.activity.viewModels import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope import androidx.webkit.WebViewAssetLoader import com.dergoogler.mmrl.platform.model.ModId import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.sukisu.ultra.ui.util.createRootShell +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel +import kotlinx.coroutines.launch import java.io.File @SuppressLint("SetJavaScriptEnabled") class WebUIActivity : ComponentActivity() { private val rootShell by lazy { createRootShell(true) } + private val superUserViewModel: SuperUserViewModel by viewModels() private var webView = null as WebView? override fun onCreate(savedInstanceState: Bundle?) { @@ -35,6 +40,10 @@ class WebUIActivity : ComponentActivity() { super.onCreate(savedInstanceState) + lifecycleScope.launch { + superUserViewModel.fetchAppList() + } + val moduleId = intent.getStringExtra("id") ?: finishAndRemoveTask().let { return } val name = intent.getStringExtra("name") ?: finishAndRemoveTask().let { return } if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { @@ -64,7 +73,21 @@ class WebUIActivity : ComponentActivity() { view: WebView, request: WebResourceRequest ): WebResourceResponse? { - return webViewAssetLoader.shouldInterceptRequest(request.url) + val url = request.url + // Handle ksu://icon/[packageName] to serve app icon via WebView + 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(this@WebUIActivity, packageName, 512) + if (icon != null) { + val stream = java.io.ByteArrayOutputStream() + icon.compress(android.graphics.Bitmap.CompressFormat.PNG, 100, stream) + val inputStream = java.io.ByteArrayInputStream(stream.toByteArray()) + return WebResourceResponse("image/png", null, inputStream) + } + } + } + return webViewAssetLoader.shouldInterceptRequest(url) } } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt index 85781a9..0761274 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebUIXActivity.kt @@ -13,6 +13,7 @@ import androidx.lifecycle.lifecycleScope import com.dergoogler.mmrl.platform.Platform import com.dergoogler.mmrl.platform.model.ModId import com.dergoogler.mmrl.ui.component.Loading +import com.dergoogler.mmrl.webui.model.WebUIConfig import com.dergoogler.mmrl.webui.screen.WebUIScreen import com.dergoogler.mmrl.webui.util.rememberWebUIOptions import com.sukisu.ultra.BuildConfig @@ -95,6 +96,13 @@ class WebUIXActivity : ComponentActivity() { userAgentString = userAgent ) + // idk why webuix not allow root impl change webuiConfig + // so we use magic to force exitConfirm shutdown + val field = WebUIConfig::class.java.getDeclaredField("exitConfirm") + field.isAccessible = true + field.set(options.config, false) + field.isAccessible = false + WebUIScreen( webView = webView, options = options, diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt index 2335104..1e27104 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt +++ b/manager/app/src/main/java/com/sukisu/ultra/ui/webui/WebViewInterface.kt @@ -1,17 +1,20 @@ package com.sukisu.ultra.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.dergoogler.mmrl.webui.interfaces.WXInterface import com.dergoogler.mmrl.webui.interfaces.WXOptions import com.dergoogler.mmrl.webui.model.JavaScriptInterface +import com.sukisu.ultra.ui.viewmodel.SuperUserViewModel import com.sukisu.ultra.ui.util.* import com.topjohnwu.superuser.CallbackList import com.topjohnwu.superuser.ShellUtils @@ -138,7 +141,7 @@ class WebViewInterface( completableFuture.thenAccept { result -> val emitExitCode = - "(function() { try { ${callbackFunc}.emit('exit', ${result.code}); } catch(e) { console.error(`emitExit error: \${e}`); } })();" + $$"(function() { try { $${callbackFunc}.emit('exit', $${result.code}); } catch(e) { console.error(`emitExit error: ${e}`); } })();" webView.post { webView.evaluateJavascript(emitExitCode, null) } @@ -203,6 +206,56 @@ class WebViewInterface( 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() + } + // =================== KPM支持 ============================= @JavascriptInterface diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java b/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java deleted file mode 100644 index f829f61..0000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraShellHelper.java +++ /dev/null @@ -1,29 +0,0 @@ -package io.sukisu.ultra; - -import java.util.ArrayList; - -import com.sukisu.ultra.ui.util.KsuCli; - -public class UltraShellHelper { - public static String runCmd(String cmds) { - StringBuilder sb = new StringBuilder(); - for(String str : KsuCli.INSTANCE.getGLOBAL_MNT_SHELL() - .newJob() - .add(cmds) - .to(new ArrayList<>(), null) - .exec() - .getOut()) { - sb.append(str).append("\n"); - } - return sb.toString(); - } - - public static boolean isPathExists(String path) { - String result = runCmd("test -f '" + path + "' && echo 'exists'"); - return result.contains("exists"); - } - - public static void CopyFileTo(String path, String target) { - runCmd("cp -f '" + path + "' '" + target + "' 2>&1"); - } -} diff --git a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java b/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java deleted file mode 100644 index 84fddda..0000000 --- a/manager/app/src/main/java/io/sukisu/ultra/UltraToolInstall.java +++ /dev/null @@ -1,23 +0,0 @@ -package io.sukisu.ultra; - -import static com.sukisu.ultra.ui.util.KsuCliKt.*; -import android.annotation.SuppressLint; - -public class UltraToolInstall { - private static final String OUTSIDE_KPMMGR_PATH = "/data/adb/ksu/bin/kpmmgr"; - private static final String OUTSIDE_SUSFSD_PATH = "/data/adb/ksu/bin/susfsd"; - - @SuppressLint("SetWorldReadable") - public static void tryToInstall() { - String kpmmgrPath = getKpmmgrPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_KPMMGR_PATH)) { - UltraShellHelper.CopyFileTo(kpmmgrPath, OUTSIDE_KPMMGR_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_KPMMGR_PATH); - } - String SuSFSDaemonPath = getSuSFSDaemonPath(); - if (UltraShellHelper.isPathExists(OUTSIDE_SUSFSD_PATH)) { - UltraShellHelper.CopyFileTo(SuSFSDaemonPath, OUTSIDE_SUSFSD_PATH); - UltraShellHelper.runCmd("chmod a+rx " + OUTSIDE_SUSFSD_PATH); - } - } -} diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt deleted file mode 100644 index 3d364e4..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AnimatedBottomBar.kt +++ /dev/null @@ -1,20 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import androidx.compose.animation.* -import androidx.compose.runtime.Composable - -object AnimatedBottomBar { - @Composable - fun AnimatedBottomBarWrapper( - showBottomBar: Boolean, - content: @Composable () -> Unit - ) { - AnimatedVisibility( - visible = showBottomBar, - enter = slideInVertically(initialOffsetY = { it }) + fadeIn(), - exit = slideOutVertically(targetOffsetY = { it }) + fadeOut() - ) { - content() - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt deleted file mode 100644 index 50f5136..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/AppData.kt +++ /dev/null @@ -1,90 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import com.sukisu.ultra.Natives -import com.sukisu.ultra.ui.util.* -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.asStateFlow - -object AppData { - object DataRefreshManager { - // 私有状态流 - private val _superuserCount = MutableStateFlow(0) - private val _moduleCount = MutableStateFlow(0) - private val _kpmModuleCount = MutableStateFlow(0) - - // 公开的只读状态流 - val superuserCount: StateFlow = _superuserCount.asStateFlow() - val moduleCount: StateFlow = _moduleCount.asStateFlow() - val kpmModuleCount: StateFlow = _kpmModuleCount.asStateFlow() - - /** - * 刷新所有数据计数 - */ - fun refreshData() { - _superuserCount.value = getSuperuserCountUse() - _moduleCount.value = getModuleCountUse() - _kpmModuleCount.value = getKpmModuleCountUse() - } - - } - - /** - * 获取超级用户应用计数 - */ - fun getSuperuserCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getSuperuserCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取模块计数 - */ - fun getModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - getModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM模块计数 - */ - fun getKpmModuleCountUse(): Int { - return try { - if (!rootAvailable()) return 0 - val kpmVersion = getKpmVersionUse() - if (kpmVersion.isEmpty() || kpmVersion.startsWith("Error")) return 0 - getKpmModuleCount() - } catch (_: Exception) { - 0 - } - } - - /** - * 获取KPM版本 - */ - fun getKpmVersionUse(): String { - return try { - if (!rootAvailable()) return "" - val version = getKpmVersion() - version.ifEmpty { "" } - } catch (e: Exception) { - "Error: ${e.message}" - } - } - - /** - * 检查是否是完整功能模式 - */ - fun isFullFeatured(packageName: String): Boolean { - val isManager = Natives.becomeManager(packageName) - return isManager && !Natives.requireNewKernel() && rootAvailable() - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt deleted file mode 100644 index e09b194..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DataRefreshUtils.kt +++ /dev/null @@ -1,46 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.content.Context -import androidx.lifecycle.LifecycleCoroutineScope -import com.sukisu.ultra.ui.MainActivity -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.isActive -import kotlinx.coroutines.launch -import zako.zako.zako.zakoui.activity.util.AppData.DataRefreshManager - -object DataRefreshUtils { - - fun startDataRefreshCoroutine(scope: LifecycleCoroutineScope) { - scope.launch(Dispatchers.IO) { - while (isActive) { - DataRefreshManager.refreshData() - delay(5000) - } - } - } - - fun startSettingsMonitorCoroutine( - scope: LifecycleCoroutineScope, - activity: MainActivity, - settingsStateFlow: MutableStateFlow - ) { - scope.launch(Dispatchers.IO) { - while (isActive) { - val prefs = activity.getSharedPreferences("settings", Context.MODE_PRIVATE) - settingsStateFlow.value = MainActivity.SettingsState( - isHideOtherInfo = prefs.getBoolean("is_hide_other_info", false), - showKpmInfo = prefs.getBoolean("show_kpm_info", false) - ) - delay(1000) - } - } - } - - fun refreshData(scope: LifecycleCoroutineScope) { - scope.launch { - DataRefreshManager.refreshData() - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt deleted file mode 100644 index bd6c128..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/DisplayUtils.kt +++ /dev/null @@ -1,24 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.content.Context - -object DisplayUtils { - - fun applyCustomDpi(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val customDpi = prefs.getInt("app_dpi", 0) - - if (customDpi > 0) { - try { - val resources = context.resources - val metrics = resources.displayMetrics - metrics.density = customDpi / 160f - @Suppress("DEPRECATION") - metrics.scaledDensity = customDpi / 160f - metrics.densityDpi = customDpi - } catch (e: Exception) { - e.printStackTrace() - } - } - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt deleted file mode 100644 index 50debbc..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/activity/util/LocaleUtils.kt +++ /dev/null @@ -1,48 +0,0 @@ -package zako.zako.zako.zakoui.activity.util - -import android.annotation.SuppressLint -import android.content.Context -import android.content.res.Configuration -import android.os.Build -import java.util.* - -object LocaleUtils { - - @SuppressLint("ObsoleteSdkInt") - fun applyLanguageSetting(context: Context) { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val resources = context.resources - val config = Configuration(resources.configuration) - config.setLocale(locale) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - context.createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - resources.updateConfiguration(config, resources.displayMetrics) - } - } - } - - fun applyLocale(context: Context): Context { - val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) - val languageCode = prefs.getString("app_language", "") ?: "" - - var newContext = context - if (languageCode.isNotEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - Locale.setDefault(locale) - - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - newContext = context.createConfigurationContext(config) - } - - return newContext - } -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt deleted file mode 100644 index 3dbd4f2..0000000 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/MoreSettings.kt +++ /dev/null @@ -1,1711 +0,0 @@ -package zako.zako.zako.zakoui.screen - -import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent -import android.content.res.Configuration -import android.net.Uri -import android.os.Build -import android.widget.Toast -import androidx.activity.compose.rememberLauncherForActivityResult -import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.animation.* -import androidx.compose.animation.core.animateFloatAsState -import androidx.compose.foundation.* -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.automirrored.filled.NavigateNext -import androidx.compose.material.icons.filled.* -import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable -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.vector.ImageVector -import androidx.compose.ui.input.nestedscroll.nestedScroll -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.core.content.edit -import com.ramcosta.composedestinations.annotation.Destination -import com.ramcosta.composedestinations.annotation.RootGraph -import com.ramcosta.composedestinations.navigation.DestinationsNavigator -import com.sukisu.ultra.Natives -import com.sukisu.ultra.R -import com.sukisu.ultra.ksuApp -import com.sukisu.ultra.ui.component.ImageEditorDialog -import com.sukisu.ultra.ui.component.KsuIsValid -import com.sukisu.ultra.ui.theme.* -import com.sukisu.ultra.ui.theme.CardConfig.cardElevation -import com.sukisu.ultra.ui.util.* -import com.topjohnwu.superuser.Shell -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.util.* -import kotlin.math.roundToInt - -/** - * @author ShirkNeko - * @date 2025/5/31. - */ -private val SETTINGS_ITEM_HEIGHT = 56.dp -private val SETTINGS_GROUP_SPACING = 16.dp - -/** - * 保存卡片配置 - */ -fun saveCardConfig(context: Context) { - CardConfig.save(context) -} - -/** - * 更多设置屏幕 - */ -@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt") -@OptIn(ExperimentalMaterial3Api::class) -@Destination -@Composable -fun MoreSettingsScreen( - navigator: DestinationsNavigator -) { - // 顶部滚动行为 - val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) - val context = LocalContext.current - val coroutineScope = rememberCoroutineScope() - val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } - val systemIsDark = isSystemInDarkTheme() - - - // 主题模式选择 - var themeMode by remember { - mutableIntStateOf( - when(ThemeConfig.forceDarkMode) { - true -> 2 // 深色 - false -> 1 // 浅色 - null -> 0 // 跟随系统 - } - ) - } - - // 动态颜色开关状态 - var useDynamicColor by remember { - mutableStateOf(ThemeConfig.useDynamicColor) - } - - // 对话框显示状态 - var showThemeModeDialog by remember { mutableStateOf(false) } - var showLanguageDialog by remember { mutableStateOf(false) } - var showThemeColorDialog by remember { mutableStateOf(false) } - var showDpiConfirmDialog by remember { mutableStateOf(false) } - var showImageEditor by remember { mutableStateOf(false) } - - // 动态管理器配置状态 - var dynamicSignConfig by remember { mutableStateOf(null) } - var isDynamicSignEnabled by remember { mutableStateOf(false) } - var dynamicSignSize by remember { mutableStateOf("") } - var dynamicSignHash by remember { mutableStateOf("") } - var showDynamicSignDialog by remember { mutableStateOf(false) } - - // 主题模式选项 - val themeOptions = listOf( - stringResource(R.string.theme_follow_system), - stringResource(R.string.theme_light), - stringResource(R.string.theme_dark) - ) - - // 获取当前语言设置 - var currentLanguage by remember { - mutableStateOf(prefs.getString("app_language", "") ?: "") - } - - // 获取支持的语言列表 - val supportedLanguages = remember { - val languages = mutableListOf>() - languages.add("" to context.getString(R.string.language_follow_system)) - val locales = context.resources.configuration.locales - for (i in 0 until locales.size()) { - val locale = locales.get(i) - val code = locale.toLanguageTag() - if (!languages.any { it.first == code }) { - languages.add(code to locale.getDisplayName(locale)) - } - } - - val commonLocales = listOf( - Locale.forLanguageTag("en"), // 英语 - Locale.forLanguageTag("zh-CN"), // 简体中文 - Locale.forLanguageTag("zh-HK"), // 繁体中文(香港) - Locale.forLanguageTag("zh-TW"), // 繁体中文(台湾) - Locale.forLanguageTag("ja"), // 日语 - Locale.forLanguageTag("fr"), // 法语 - Locale.forLanguageTag("de"), // 德语 - Locale.forLanguageTag("es"), // 西班牙语 - Locale.forLanguageTag("it"), // 意大利语 - Locale.forLanguageTag("ru"), // 俄语 - Locale.forLanguageTag("pt"), // 葡萄牙语 - Locale.forLanguageTag("ko"), // 韩语 - Locale.forLanguageTag("vi") // 越南语 - ) - - for (locale in commonLocales) { - val code = locale.toLanguageTag() - if (!languages.any { it.first == code }) { - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - try { - val testContext = context.createConfigurationContext(config) - testContext.getString(R.string.language_follow_system) - languages.add(code to locale.getDisplayName(locale)) - } catch (_: Exception) { - } - } - } - languages - } - - // 简洁模式开关状态 - var isSimpleMode by remember { - mutableStateOf(prefs.getBoolean("is_simple_mode", false)) - } - - // 隐藏内核版本号开关状态 - var isHideVersion by remember { - mutableStateOf(prefs.getBoolean("is_hide_version", false)) - } - - // 隐藏模块数量等信息开关状态 - var isHideOtherInfo by remember { - mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) - } - - // 显示KPM开关状态 - var isShowKpmInfo by remember { - mutableStateOf(prefs.getBoolean("show_kpm_info", false)) - } - - // 隐藏 Zygisk 状态开关状态 - var isHideZygiskImplement by remember { - mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false)) - } - - // 隐藏SuSFS状态开关状态 - var isHideSusfsStatus by remember { - mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) - } - - // 隐藏链接状态开关状态 - var isHideLinkCard by remember { - mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) - } - - // 隐藏标签行开关状态 - var isHideTagRow by remember { - mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) - } - - // 内核版本简洁模式开关状态 - var isKernelSimpleMode by remember { - mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false)) - } - - // 显示更多模块信息开关状态 - var showMoreModuleInfo by remember { - mutableStateOf(prefs.getBoolean("show_more_module_info", false)) - } - - // SELinux状态 - var selinuxEnabled by remember { - mutableStateOf(Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing") - } - - // 卡片配置状态 - var cardAlpha by rememberSaveable { mutableFloatStateOf(CardConfig.cardAlpha) } - var cardDim by rememberSaveable { mutableFloatStateOf(CardConfig.cardDim) } - var isCustomBackgroundEnabled by rememberSaveable { - mutableStateOf(ThemeConfig.customBackgroundUri != null) - } - - // 备用图标状态 - var useAltIcon by remember { mutableStateOf(prefs.getBoolean("use_alt_icon", false)) } - - // 图片选择状态 - var selectedImageUri by remember { mutableStateOf(null) } - - // DPI 设置 - val systemDpi = remember { context.resources.displayMetrics.densityDpi } - var currentDpi by remember { - mutableIntStateOf(prefs.getInt("app_dpi", systemDpi)) - } - var tempDpi by remember { mutableIntStateOf(currentDpi) } - var isDpiCustom by remember { mutableStateOf(true) } - - // 预设 DPI 选项 - val dpiPresets = mapOf( - stringResource(R.string.dpi_size_small) to 240, - stringResource(R.string.dpi_size_medium) to 320, - stringResource(R.string.dpi_size_large) to 420, - stringResource(R.string.dpi_size_extra_large) to 560 - ) - - // 主题色选项 - val themeColorOptions = listOf( - stringResource(R.string.color_default) to ThemeColors.Default, - stringResource(R.string.color_green) to ThemeColors.Green, - stringResource(R.string.color_purple) to ThemeColors.Purple, - stringResource(R.string.color_orange) to ThemeColors.Orange, - stringResource(R.string.color_pink) to ThemeColors.Pink, - stringResource(R.string.color_gray) to ThemeColors.Gray, - stringResource(R.string.color_yellow) to ThemeColors.Yellow - ) - - - // 更新简洁模式开关状态 - val onSimpleModeChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_simple_mode", newValue) } - isSimpleMode = newValue - } - - // 内核版本简洁模式开关状态 - val onKernelSimpleModeChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_kernel_simple_mode", newValue) } - isKernelSimpleMode = newValue - } - - // 隐藏内核版本号开关状态 - val onHideVersionChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_version", newValue) } - isHideVersion = newValue - } - - // 隐藏模块数量等信息开关状态 - val onHideOtherInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_other_info", newValue) } - isHideOtherInfo = newValue - } - - // 更新显示KPM开关状态 - val onShowKpmInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("show_kpm_info", newValue) } - isShowKpmInfo = newValue - } - - // 隐藏SuSFS状态开关状态 - val onHideSusfsStatusChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_susfs_status", newValue) } - isHideSusfsStatus = newValue - } - - val onHideZygiskImplement = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) } - isHideZygiskImplement = newValue - - } - - // 隐藏链接状态开关状态 - val onHideLinkCardChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_link_card", newValue) } - isHideLinkCard = newValue - } - - // 隐藏标签行开关状态 - val onHideTagRowChange = { newValue: Boolean -> - prefs.edit { putBoolean("is_hide_tag_row", newValue) } - isHideTagRow = newValue - } - - // 显示更多模块信息开关状态 - val onShowMoreModuleInfoChange = { newValue: Boolean -> - prefs.edit { putBoolean("show_more_module_info", newValue) } - showMoreModuleInfo = newValue - } - - // 备用图标开关状态 - val onUseAltIconChange = { newValue: Boolean -> - prefs.edit { putBoolean("use_alt_icon", newValue) } - useAltIcon = newValue - toggleLauncherIcon(context, newValue) - Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show() - } - - - // 获取DPI大小友好名称 - @Composable - fun getDpiFriendlyName(dpi: Int): String { - return when (dpi) { - 240 -> stringResource(R.string.dpi_size_small) - 320 -> stringResource(R.string.dpi_size_medium) - 420 -> stringResource(R.string.dpi_size_large) - 560 -> stringResource(R.string.dpi_size_extra_large) - else -> stringResource(R.string.dpi_size_custom) - } - } - - // 应用 DPI 设置 - val applyDpiSetting = { dpi: Int -> - if (dpi != currentDpi) { - // 保存到 SharedPreferences - prefs.edit { - putInt("app_dpi", dpi) - } - - // 只修改应用级别的DPI设置 - currentDpi = dpi - tempDpi = dpi - Toast.makeText( - context, - context.getString(R.string.dpi_applied_success, dpi), - Toast.LENGTH_SHORT - ).show() - - val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) - restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(restartIntent) - - showDpiConfirmDialog = false - } - } - - // 应用语言设置 - val applyLanguageSetting = { code: String -> - if (currentLanguage != code) { - prefs.edit { - putString("app_language", code) - commit() - } - - currentLanguage = code - - Toast.makeText( - context, - context.getString(R.string.language_changed), - Toast.LENGTH_SHORT - ).show() - - val locale = if (code.isEmpty()) Locale.getDefault() else Locale.forLanguageTag(code) - Locale.setDefault(locale) - val config = Configuration(context.resources.configuration) - config.setLocale(locale) - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - context.createConfigurationContext(config) - } else { - @Suppress("DEPRECATION") - context.resources.updateConfiguration(config, context.resources.displayMetrics) - } - ksuApp.refreshCurrentActivity() - } - } - - // ========== 初始化 ========== - - // 初始化卡片配置 - LaunchedEffect(Unit) { - // 加载设置 - CardConfig.load(context) - cardAlpha = CardConfig.cardAlpha - cardDim = CardConfig.cardDim - isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null - - // 设置主题模式 - themeMode = when (ThemeConfig.forceDarkMode) { - true -> 2 - false -> 1 - null -> 0 - } - - // 确保卡片样式跟随主题模式 - when (themeMode) { - 2 -> { // 深色 - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - } - 1 -> { // 浅色 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = true - } - 0 -> { // 跟随系统 - CardConfig.isUserDarkModeEnabled = false - CardConfig.isUserLightModeEnabled = false - } - } - - // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 - if (themeMode == 0 && systemIsDark) { - CardConfig.setThemeDefaults(true) - } - - currentDpi = prefs.getInt("app_dpi", systemDpi) - tempDpi = currentDpi - - CardConfig.save(context) - } - - // 图片选择器 - val pickImageLauncher = rememberLauncherForActivityResult( - ActivityResultContracts.GetContent() - ) { uri: Uri? -> - uri?.let { - selectedImageUri = it - showImageEditor = true - } - } - - // ========== UI 构建 ========== - - // 显示图片编辑对话框 - if (showImageEditor && selectedImageUri != null) { - ImageEditorDialog( - imageUri = selectedImageUri!!, - onDismiss = { - showImageEditor = false - selectedImageUri = null - }, - onConfirm = { transformedUri -> - context.saveAndApplyCustomBackground(transformedUri) - isCustomBackgroundEnabled = true - cardElevation = 0.dp - CardConfig.isCustomBackgroundEnabled = true - saveCardConfig(context) - showImageEditor = false - selectedImageUri = null - - // 显示成功提示 - Toast.makeText( - context, - context.getString(R.string.background_set_success), - Toast.LENGTH_SHORT - ).show() - } - ) - } - - // 主题模式选择对话框 - if (showThemeModeDialog) { - SingleChoiceDialog( - title = stringResource(R.string.theme_mode), - options = themeOptions, - selectedIndex = themeMode, - onOptionSelected = { index -> - themeMode = index - val newThemeMode = when(index) { - 0 -> null // 跟随系统 - 1 -> false // 浅色 - 2 -> true // 深色 - else -> null - } - context.saveThemeMode(newThemeMode) - when (index) { - 2 -> { // 深色 - ThemeConfig.forceDarkMode = true - CardConfig.isUserDarkModeEnabled = true - CardConfig.isUserLightModeEnabled = false - CardConfig.setThemeDefaults(true) - CardConfig.save(context) - } - 1 -> { // 浅色 - ThemeConfig.forceDarkMode = false - CardConfig.isUserLightModeEnabled = true - CardConfig.isUserDarkModeEnabled = false - CardConfig.setThemeDefaults(false) - CardConfig.save(context) - } - 0 -> { // 跟随系统 - ThemeConfig.forceDarkMode = null - CardConfig.isUserLightModeEnabled = false - CardConfig.isUserDarkModeEnabled = false - val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES - CardConfig.setThemeDefaults(isNightModeActive) - CardConfig.save(context) - } - } - }, - onDismiss = { showThemeModeDialog = false } - ) - } - - // 语言切换对话框 - if (showLanguageDialog) { - KeyValueChoiceDialog( - title = stringResource(R.string.language_setting), - options = supportedLanguages, - selectedCode = currentLanguage, - onOptionSelected = { code -> - applyLanguageSetting(code) - }, - onDismiss = { showLanguageDialog = false } - ) - } - - // DPI 设置确认对话框 - if (showDpiConfirmDialog) { - ConfirmDialog( - title = stringResource(R.string.dpi_confirm_title), - message = stringResource(R.string.dpi_confirm_message, currentDpi, tempDpi), - summaryText = stringResource(R.string.dpi_confirm_summary), - confirmText = stringResource(R.string.confirm), - dismissText = stringResource(R.string.cancel), - onConfirm = { applyDpiSetting(tempDpi) }, - onDismiss = { - showDpiConfirmDialog = false - tempDpi = currentDpi - } - ) - } - - // 主题色选择对话框 - if (showThemeColorDialog) { - AlertDialog( - onDismissRequest = { showThemeColorDialog = false }, - title = { Text(stringResource(R.string.choose_theme_color)) }, - text = { - Column { - themeColorOptions.forEach { (name, theme) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - context.saveThemeColors(when (theme) { - ThemeColors.Green -> "green" - ThemeColors.Purple -> "purple" - ThemeColors.Orange -> "orange" - ThemeColors.Pink -> "pink" - ThemeColors.Gray -> "gray" - ThemeColors.Yellow -> "yellow" - else -> "default" - }) - showThemeColorDialog = false - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - val isDark = isSystemInDarkTheme() - Box( - modifier = Modifier.padding(end = 12.dp) - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - Text(name) - Spacer(modifier = Modifier.weight(1f)) - // 当前选中的主题显示选中标记 - if (ThemeConfig.currentTheme::class == theme::class) { - Icon( - Icons.Default.Check, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - } - } - } - }, - confirmButton = { - Button( - onClick = { showThemeColorDialog = false } - ) { - Text(stringResource(R.string.cancel)) - } - } - ) - } - - LaunchedEffect(Unit) { - // 初始化动态管理器配置 - dynamicSignConfig = Natives.getDynamicManager() - dynamicSignConfig?.let { config -> - if (config.isValid()) { - isDynamicSignEnabled = true - dynamicSignSize = config.size.toString() - dynamicSignHash = config.hash - } - } - } - - fun parseDynamicSignSize(input: String): Int? { - return try { - when { - input.startsWith("0x", true) -> input.substring(2).toInt(16) - else -> input.toInt() - } - } catch (_: NumberFormatException) { - null - } - } - - // 动态管理器配置对话框 - if (showDynamicSignDialog) { - AlertDialog( - onDismissRequest = { showDynamicSignDialog = false }, - title = { Text(stringResource(R.string.dynamic_manager_title)) }, - text = { - Column( - modifier = Modifier.verticalScroll(rememberScrollState()) - ) { - // 启用开关 - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { isDynamicSignEnabled = !isDynamicSignEnabled } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically - ) { - Switch( - checked = isDynamicSignEnabled, - onCheckedChange = { isDynamicSignEnabled = it } - ) - Spacer(modifier = Modifier.width(12.dp)) - Text(stringResource(R.string.enable_dynamic_manager)) - } - - Spacer(modifier = Modifier.height(16.dp)) - - // 签名大小输入 - OutlinedTextField( - value = dynamicSignSize, - onValueChange = { input -> - val isValid = when { - input.isEmpty() -> true - input.matches(Regex("^\\d+$")) -> true - input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true - else -> false - } - if (isValid) { - dynamicSignSize = input - } - }, - label = { Text(stringResource(R.string.signature_size)) }, - enabled = isDynamicSignEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - keyboardOptions = KeyboardOptions( - keyboardType = KeyboardType.Text - ) - ) - - Spacer(modifier = Modifier.height(12.dp)) - - // 签名哈希输入 - OutlinedTextField( - value = dynamicSignHash, - onValueChange = { hash -> - if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { - dynamicSignHash = hash - } - }, - label = { Text(stringResource(R.string.signature_hash)) }, - enabled = isDynamicSignEnabled, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - supportingText = { - Text(stringResource(R.string.hash_must_be_64_chars)) - }, - isError = isDynamicSignEnabled && dynamicSignHash.isNotEmpty() && dynamicSignHash.length != 64 - ) - } - }, - confirmButton = { - Button( - onClick = { - if (isDynamicSignEnabled) { - val size = parseDynamicSignSize(dynamicSignSize) - if (size != null && size > 0 && dynamicSignHash.length == 64) { - val success = Natives.setDynamicManager(size, dynamicSignHash) - if (success) { - dynamicSignConfig = Natives.DynamicManagerConfig(size, dynamicSignHash) - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_set_failed), - Toast.LENGTH_SHORT - ).show() - } - } else { - Toast.makeText( - context, - context.getString(R.string.invalid_sign_config), - Toast.LENGTH_SHORT - ).show() - return@Button - } - } else { - val success = Natives.clearDynamicManager() - if (success) { - dynamicSignConfig = null - dynamicSignSize = "" - dynamicSignHash = "" - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_disabled_success), - Toast.LENGTH_SHORT - ).show() - } else { - Toast.makeText( - context, - context.getString(R.string.dynamic_manager_clear_failed), - Toast.LENGTH_SHORT - ).show() - return@Button - } - } - showDynamicSignDialog = false - }, - enabled = if (isDynamicSignEnabled) { - parseDynamicSignSize(dynamicSignSize)?.let { it > 0 } == true && - dynamicSignHash.length == 64 - } else true - ) { - Text(stringResource(R.string.confirm)) - } - }, - dismissButton = { - TextButton(onClick = { showDynamicSignDialog = false }) { - Text(stringResource(R.string.cancel)) - } - } - ) - } - - Scaffold( - modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), - topBar = { - TopAppBar( - title = { - Text( - text = stringResource(R.string.more_settings), - style = MaterialTheme.typography.titleLarge - ) - }, - navigationIcon = { - IconButton(onClick = { - navigator.popBackStack() - }) { - Icon( - imageVector = Icons.AutoMirrored.Filled.ArrowBack, - contentDescription = stringResource(R.string.back) - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), - scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) - ), - windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), - scrollBehavior = scrollBehavior - ) - }, - contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) - ) { paddingValues -> - Column( - modifier = Modifier - .fillMaxSize() - .padding(paddingValues) - .verticalScroll(rememberScrollState()) - .padding(horizontal = 16.dp) - .padding(top = 8.dp) - ) { - // ========== 外观设置部分 ========== - SettingsCard( - title = stringResource(R.string.appearance_settings), - ) { - // 语言设置 - SettingItem( - icon = Icons.Default.Language, - title = stringResource(R.string.language_setting), - subtitle = supportedLanguages.find { it.first == currentLanguage }?.second - ?: stringResource(R.string.language_follow_system), - onClick = { showLanguageDialog = true } - ) - - // 主题模式 - SettingItem( - icon = Icons.Default.DarkMode, - title = stringResource(R.string.theme_mode), - subtitle = themeOptions[themeMode], - onClick = { showThemeModeDialog = true } - ) - - // 动态颜色开关 - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - SwitchSettingItem( - icon = Icons.Filled.ColorLens, - title = stringResource(R.string.dynamic_color_title), - summary = stringResource(R.string.dynamic_color_summary), - checked = useDynamicColor, - onChange = { enabled -> - useDynamicColor = enabled - context.saveDynamicColorState(enabled) - } - ) - } - - // 只在未启用动态颜色时显示主题色选择 - AnimatedVisibility( - visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !useDynamicColor, - enter = fadeIn() + expandVertically(), - exit = fadeOut() + shrinkVertically() - ) { - SettingItem( - icon = Icons.Default.Palette, - title = stringResource(R.string.theme_color), - subtitle = when (ThemeConfig.currentTheme) { - is ThemeColors.Green -> stringResource(R.string.color_green) - is ThemeColors.Purple -> stringResource(R.string.color_purple) - is ThemeColors.Orange -> stringResource(R.string.color_orange) - is ThemeColors.Pink -> stringResource(R.string.color_pink) - is ThemeColors.Gray -> stringResource(R.string.color_gray) - is ThemeColors.Yellow -> stringResource(R.string.color_yellow) - else -> stringResource(R.string.color_default) - }, - onClick = { showThemeColorDialog = true }, - trailingContent = { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(start = 8.dp) - ) { - // 显示当前主题的三种主题色调 - val theme = ThemeConfig.currentTheme - val isDark = isSystemInDarkTheme() - - // Primary color - ColorCircle( - color = if (isDark) theme.primaryDark else theme.primaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - - // Secondary color - ColorCircle( - color = if (isDark) theme.secondaryDark else theme.secondaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - - // Tertiary color - ColorCircle( - color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, - isSelected = false, - modifier = Modifier.padding(horizontal = 2.dp) - ) - } - } - ) - } - - SettingsDivider() - - // DPI 设置 - SettingItem( - icon = Icons.Default.FormatSize, - title = stringResource(R.string.app_dpi_title), - subtitle = stringResource(R.string.app_dpi_summary), - onClick = {}, - trailingContent = { - Text( - text = getDpiFriendlyName(tempDpi), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.primary - ) - } - ) - - // DPI 滑动条 - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - val sliderValue by animateFloatAsState( - targetValue = tempDpi.toFloat(), - label = "DPI Slider Animation" - ) - - Slider( - value = sliderValue, - onValueChange = { - tempDpi = it.toInt() - isDpiCustom = !dpiPresets.containsValue(tempDpi) - }, - valueRange = 160f..600f, - steps = 11, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - - Row( - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - ) { - dpiPresets.forEach { (name, dpi) -> - val isSelected = tempDpi == dpi - val buttonColor = if (isSelected) - MaterialTheme.colorScheme.primaryContainer - else - MaterialTheme.colorScheme.surfaceVariant - - Box( - modifier = Modifier - .weight(1f) - .padding(horizontal = 2.dp) - .clip(RoundedCornerShape(8.dp)) - .background(buttonColor) - .clickable { - tempDpi = dpi - isDpiCustom = false - } - .padding(vertical = 8.dp, horizontal = 4.dp), - contentAlignment = Alignment.Center - ) { - Text( - text = name, - style = MaterialTheme.typography.labelMedium, - color = if (isSelected) - MaterialTheme.colorScheme.onPrimaryContainer - else - MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = 1, - overflow = TextOverflow.Ellipsis - ) - } - } - } - - Text( - text = if (isDpiCustom) - "${stringResource(R.string.dpi_size_custom)}: $tempDpi" - else - "${getDpiFriendlyName(tempDpi)}: $tempDpi", - style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(top = 8.dp) - ) - - Button( - onClick = { - if (tempDpi != currentDpi) { - showDpiConfirmDialog = true - } - }, - modifier = Modifier - .fillMaxWidth() - .padding(top = 8.dp), - enabled = tempDpi != currentDpi - ) { - Icon( - Icons.Default.Check, - contentDescription = null, - modifier = Modifier.size(16.dp) - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(stringResource(R.string.dpi_apply_settings)) - } - } - - SettingsDivider() - - // 自定义背景开关 - SwitchSettingItem( - icon = Icons.Filled.Wallpaper, - title = stringResource(id = R.string.settings_custom_background), - summary = stringResource(id = R.string.settings_custom_background_summary), - checked = isCustomBackgroundEnabled, - onChange = { isChecked -> - if (isChecked) { - pickImageLauncher.launch("image/*") - } else { - context.saveCustomBackground(null) - isCustomBackgroundEnabled = false - CardConfig.cardAlpha = 1f - CardConfig.cardDim = 0f - CardConfig.isCustomAlphaSet = false - CardConfig.isCustomDimSet = false - CardConfig.isCustomBackgroundEnabled = false - saveCardConfig(context) - - // 重置其他相关设置 - ThemeConfig.needsResetOnThemeChange = true - ThemeConfig.preventBackgroundRefresh = false - - context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE) - .edit { - putBoolean( - "prevent_background_refresh", - false - ) - } - - Toast.makeText( - context, - context.getString(R.string.background_removed), - Toast.LENGTH_SHORT - ).show() - } - } - ) - - // 透明度和亮度调节滑动条 - AnimatedVisibility( - visible = ThemeConfig.customBackgroundUri != null, - enter = fadeIn() + slideInVertically(), - exit = fadeOut() + slideOutVertically() - ) { - Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { - // 透明度滑动条 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(bottom = 4.dp) - ) { - Icon( - Icons.Filled.Opacity, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_alpha), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(cardAlpha * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val alphaSliderValue by animateFloatAsState( - targetValue = cardAlpha, - label = "Alpha Slider Animation" - ) - - Slider( - value = alphaSliderValue, - onValueChange = { newValue -> - cardAlpha = newValue - CardConfig.cardAlpha = newValue - CardConfig.isCustomAlphaSet = true - prefs.edit { - putBoolean("is_custom_alpha_set", true) - putFloat("card_alpha", newValue) - } - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - - // 亮度调节滑动条 - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) - ) { - Icon( - Icons.Filled.LightMode, - contentDescription = null, - modifier = Modifier.size(20.dp), - tint = MaterialTheme.colorScheme.primary - ) - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.settings_card_dim), - style = MaterialTheme.typography.titleSmall - ) - Spacer(modifier = Modifier.weight(1f)) - Text( - text = "${(cardDim * 100).roundToInt()}%", - style = MaterialTheme.typography.labelMedium, - ) - } - - val dimSliderValue by animateFloatAsState( - targetValue = cardDim, - label = "Dim Slider Animation" - ) - - Slider( - value = dimSliderValue, - onValueChange = { newValue -> - cardDim = newValue - CardConfig.cardDim = newValue - CardConfig.isCustomDimSet = true - prefs.edit { - putBoolean("is_custom_dim_set", true) - putFloat("card_dim", newValue) - } - }, - onValueChangeFinished = { - coroutineScope.launch(Dispatchers.IO) { - saveCardConfig(context) - } - }, - valueRange = 0f..1f, - steps = 20, - colors = SliderDefaults.colors( - thumbColor = MaterialTheme.colorScheme.primary, - activeTrackColor = MaterialTheme.colorScheme.primary, - inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant - ) - ) - } - } - } - - // 自定义设置 - SettingsCard( - title = stringResource(R.string.custom_settings) - ) { - // 图标切换 - SwitchSettingItem( - icon = Icons.Default.Android, - title = stringResource(R.string.icon_switch_title), - summary = stringResource(R.string.icon_switch_summary), - checked = useAltIcon, - onChange = onUseAltIconChange - ) - - // 显示更多模块信息开关 - SwitchSettingItem( - icon = Icons.Filled.Info, - title = stringResource(R.string.show_more_module_info), - summary = stringResource(R.string.show_more_module_info_summary), - checked = showMoreModuleInfo, - onChange = onShowMoreModuleInfoChange - ) - - // 添加简洁模式开关 - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.simple_mode), - summary = stringResource(R.string.simple_mode_summary), - checked = isSimpleMode, - onChange = onSimpleModeChange - ) - - SwitchSettingItem( - icon = Icons.Filled.Brush, - title = stringResource(R.string.kernel_simple_kernel), - summary = stringResource(R.string.kernel_simple_kernel_summary), - checked = isKernelSimpleMode, - onChange = onKernelSimpleModeChange - ) - - // 隐藏内核部分版本号 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_kernel_kernelsu_version), - summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), - checked = isHideVersion, - onChange = onHideVersionChange - ) - - // 模块数量等信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_other_info), - summary = stringResource(R.string.hide_other_info_summary), - checked = isHideOtherInfo, - onChange = onHideOtherInfoChange - ) - - // SuSFS 状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_susfs_status), - summary = stringResource(R.string.hide_susfs_status_summary), - checked = isHideSusfsStatus, - onChange = onHideSusfsStatusChange - ) - - // Zygsik 实现状态信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_zygisk_implement), - summary = stringResource(R.string.hide_zygisk_implement_summary), - checked = isHideZygiskImplement, - onChange = onHideZygiskImplement - ) - - if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { - // 隐藏KPM开关 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.show_kpm_info), - summary = stringResource(R.string.show_kpm_info_summary), - checked = isShowKpmInfo, - onChange = onShowKpmInfoChange - ) - } - - // 隐藏链接信息 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_link_card), - summary = stringResource(R.string.hide_link_card_summary), - checked = isHideLinkCard, - onChange = onHideLinkCardChange - ) - - // 隐藏标签行 - SwitchSettingItem( - icon = Icons.Filled.VisibilityOff, - title = stringResource(R.string.hide_tag_card), - summary = stringResource(R.string.hide_tag_card_summary), - checked = isHideTagRow, - onChange = onHideTagRowChange - ) - } - KsuIsValid { - // 高级设置 - SettingsCard( - title = stringResource(R.string.advanced_settings) - ) { - // SELinux 开关 - SwitchSettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.selinux), - summary = if (selinuxEnabled) - stringResource(R.string.selinux_enabled) else - stringResource(R.string.selinux_disabled), - checked = selinuxEnabled, - onChange = { enabled -> - val command = if (enabled) "setenforce 1" else "setenforce 0" - Shell.getShell().newJob().add(command).exec().let { result -> - if (result.isSuccess) { - selinuxEnabled = enabled - // 显示成功提示 - val message = if (enabled) - context.getString(R.string.selinux_enabled_toast) - else - context.getString(R.string.selinux_disabled_toast) - - Toast.makeText(context, message, Toast.LENGTH_SHORT).show() - } else { - // 显示失败提示 - Toast.makeText( - context, - context.getString(R.string.selinux_change_failed), - Toast.LENGTH_SHORT - ).show() - } - } - } - ) - - // SuSFS 开关(仅在支持时显示) - val suSFS = getSuSFS() - val isSUS_SU = getSuSFSFeatures() - if (suSFS == "Supported" && isSUS_SU == "CONFIG_KSU_SUSFS_SUS_SU") { - // 默认启用 - var isEnabled by rememberSaveable { - mutableStateOf(true) - } - - // 在启动时检查状态 - LaunchedEffect(Unit) { - // 如果当前模式不是2就强制启用 - val currentMode = susfsSUS_SU_Mode() - val wasManuallyDisabled = prefs.getBoolean("enable_sus_su", true) - if (currentMode != "2" && wasManuallyDisabled) { - susfsSUS_SU_2() // 强制切换到模式2 - prefs.edit { putBoolean("enable_sus_su", true) } - } - isEnabled = currentMode == "2" - } - - SwitchSettingItem( - icon = Icons.Filled.Security, - title = stringResource(id = R.string.settings_susfs_toggle), - summary = stringResource(id = R.string.settings_susfs_toggle_summary), - checked = isEnabled, - onChange = { - if (it) { - // 手动启用 - susfsSUS_SU_2() - prefs.edit { putBoolean("enable_sus_su", true) } - Toast.makeText( - context, - context.getString(R.string.susfs_enabled), - Toast.LENGTH_SHORT - ).show() - } else { - // 手动关闭 - susfsSUS_SU_0() - prefs.edit { putBoolean("enable_sus_su", false) } - Toast.makeText( - context, - context.getString(R.string.susfs_disabled), - Toast.LENGTH_SHORT - ).show() - } - isEnabled = it - } - ) - } - // 动态管理器设置 - if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER) { - SettingItem( - icon = Icons.Filled.Security, - title = stringResource(R.string.dynamic_manager_title), - subtitle = if (isDynamicSignEnabled) { - stringResource( - R.string.dynamic_manager_enabled_summary, - dynamicSignSize - ) - } else { - stringResource(R.string.dynamic_manager_disabled) - }, - onClick = { showDynamicSignDialog = true } - ) - } - } - } - } - } -} - - - -@Composable -fun SettingsCard( - title: String, - icon: ImageVector? = null, - content: @Composable () -> Unit -) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(bottom = SETTINGS_GROUP_SPACING), - colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), - elevation = getCardElevation(), - shape = MaterialTheme.shapes.medium - ) { - Column(modifier = Modifier.padding(vertical = 8.dp)) { - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .height(48.dp) - .padding(horizontal = 16.dp) - ) { - if (icon != null) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(24.dp) - ) - Spacer(modifier = Modifier.width(12.dp)) - } - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - ) - } - content() - } - } -} - -@Composable -fun SettingItem( - icon: ImageVector, - title: String, - subtitle: String? = null, - onClick: () -> Unit, - iconTint: Color = MaterialTheme.colorScheme.primary, - trailingContent: @Composable (() -> Unit)? = { - Icon( - Icons.AutoMirrored.Filled.NavigateNext, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } -) { - Row( - modifier = Modifier - .fillMaxWidth() -// .height(if (subtitle != null) SETTINGS_ITEM_HEIGHT + 12.dp else SETTINGS_ITEM_HEIGHT) - .clickable(onClick = onClick) - .padding(horizontal = 16.dp, vertical = 5.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = iconTint, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - if (subtitle != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = subtitle, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - maxLines = Int.MAX_VALUE, - overflow = TextOverflow.Visible - ) - } - } - - trailingContent?.invoke() - } -} - -@Composable -fun SwitchSettingItem( - icon: ImageVector, - title: String, - summary: String? = null, - checked: Boolean, - onChange: (Boolean) -> Unit -) { - Row( - modifier = Modifier - .fillMaxWidth() -// .height(if (summary != null) SETTINGS_ITEM_HEIGHT + 12.dp else SETTINGS_ITEM_HEIGHT) - .clickable { onChange(!checked) } - .padding(horizontal = 16.dp, vertical = 10.dp), - verticalAlignment = Alignment.Top - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier - .padding(end = 16.dp) - .size(24.dp) - ) - - Column( - modifier = Modifier.weight(1f), - verticalArrangement = Arrangement.Center - ) { - Text( - text = title, - style = MaterialTheme.typography.titleMedium, - lineHeight = 20.sp, -// maxLines = 1, -// overflow = TextOverflow.Ellipsis - ) - if (summary != null) { - Spacer(modifier = Modifier.height(2.dp)) - Text( - text = summary, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - lineHeight = 16.sp, -// maxLines = 2, -// overflow = TextOverflow.Ellipsis - ) - } - } - - Switch( - checked = checked, - onCheckedChange = onChange - ) - } -} - -@Composable -fun SettingsDivider() { - HorizontalDivider( - modifier = Modifier.padding(vertical = 8.dp) - ) -} - -@Composable -fun ColorCircle( - color: Color, - isSelected: Boolean, - modifier: Modifier = Modifier -) { - Box( - modifier = modifier - .size(20.dp) - .clip(CircleShape) - .background(color) - .then( - if (isSelected) { - Modifier.border( - width = 2.dp, - color = MaterialTheme.colorScheme.primary, - shape = CircleShape - ) - } else { - Modifier - } - ) - ) -} - -@Composable -fun SingleChoiceDialog( - title: String, - options: List, - selectedIndex: Int, - onOptionSelected: (Int) -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - options.forEachIndexed { index, option -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onOptionSelected(index) - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedIndex == index, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(option) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} - -@Composable -fun ConfirmDialog( - title: String, - message: String, - summaryText: String? = null, - confirmText: String = stringResource(R.string.confirm), - dismissText: String = stringResource(R.string.cancel), - onConfirm: () -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column { - Text(message) - if (summaryText != null) { - Spacer(modifier = Modifier.height(8.dp)) - Text( - summaryText, - style = MaterialTheme.typography.bodySmall - ) - } - } - }, - confirmButton = { - TextButton(onClick = onConfirm) { - Text(confirmText) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(dismissText) - } - } - ) -} - -@Composable -fun KeyValueChoiceDialog( - title: String, - options: List>, - selectedCode: String, - onOptionSelected: (String) -> Unit, - onDismiss: () -> Unit -) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(title) }, - text = { - Column(modifier = Modifier.verticalScroll(rememberScrollState())) { - options.forEach { (code, name) -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { - onOptionSelected(code) - onDismiss() - } - .padding(vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - RadioButton( - selected = selectedCode == code, - onClick = null - ) - Spacer(modifier = Modifier.width(8.dp)) - Text(name) - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel)) - } - } - ) -} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt similarity index 91% rename from manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt index 75ff72f..a87558f 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/KernelFlash.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/KernelFlash.kt @@ -1,7 +1,9 @@ -package zako.zako.zako.zakoui.screen +package zako.zako.zako.zakoui.screen.kernelFlash +import android.content.Context import android.net.Uri import android.os.Environment +import androidx.activity.ComponentActivity import androidx.activity.compose.BackHandler import androidx.compose.animation.core.animateFloatAsState import androidx.compose.foundation.background @@ -27,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp +import androidx.core.content.edit import com.ramcosta.composedestinations.annotation.Destination import com.ramcosta.composedestinations.annotation.RootGraph import com.ramcosta.composedestinations.navigation.DestinationsNavigator @@ -39,9 +42,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import zako.zako.zako.zakoui.flash.FlashState -import zako.zako.zako.zakoui.flash.HorizonKernelState -import zako.zako.zako.zakoui.flash.HorizonKernelWorker +import zako.zako.zako.zakoui.screen.kernelFlash.state.FlashState +import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelState +import zako.zako.zako.zakoui.screen.kernelFlash.state.HorizonKernelWorker import java.io.File import java.text.SimpleDateFormat import java.util.* @@ -73,6 +76,12 @@ fun KernelFlashScreen( kpmUndoPatch: Boolean = false ) { val context = LocalContext.current + + val shouldAutoExit = remember { + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.getBoolean("auto_exit_after_flash", false) + } + val scrollState = rememberScrollState() val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) val snackBarHost = LocalSnackbarHost.current @@ -105,6 +114,16 @@ fun KernelFlashScreen( val onFlashComplete = { showFloatAction = true KernelFlashStateHolder.isFlashing = false + + // 如果需要自动退出,延迟1.5秒后退出 + if (shouldAutoExit) { + scope.launch { + delay(1500) + val sharedPref = context.getSharedPreferences("kernel_flash_prefs", Context.MODE_PRIVATE) + sharedPref.edit { remove("auto_exit_after_flash") } + (context as? ComponentActivity)?.finish() + } + } } // 开始刷写 @@ -165,6 +184,19 @@ fun KernelFlashScreen( } } + DisposableEffect(shouldAutoExit) { + onDispose { + if (shouldAutoExit) { + KernelFlashStateHolder.currentState = null + KernelFlashStateHolder.currentUri = null + KernelFlashStateHolder.currentSlot = null + KernelFlashStateHolder.currentKpmPatchEnabled = false + KernelFlashStateHolder.currentKpmUndoPatch = false + KernelFlashStateHolder.isFlashing = false + } + } + } + BackHandler(enabled = true) { onBack() } diff --git a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt similarity index 99% rename from manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt index 2f43c96..26da72c 100644 --- a/manager/app/src/main/java/com/sukisu/ultra/ui/component/SlotSelectionDialog.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/component/SlotSelectionDialog.kt @@ -1,4 +1,4 @@ -package com.sukisu.ultra.ui.component +package zako.zako.zako.zakoui.screen.kernelFlash.component import androidx.compose.foundation.background import androidx.compose.foundation.clickable diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt similarity index 98% rename from manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt rename to manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt index b577a6a..8cfa76b 100644 --- a/manager/app/src/main/java/zako/zako/zako/zakoui/flash/KernelFlash.kt +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/kernelFlash/state/KernelFlashState.kt @@ -1,4 +1,4 @@ -package zako.zako.zako.zakoui.flash +package zako.zako.zako.zakoui.screen.kernelFlash.state import android.annotation.SuppressLint import android.app.Activity @@ -7,6 +7,7 @@ import android.net.Uri import androidx.documentfile.provider.DocumentFile import com.sukisu.ultra.R import com.sukisu.ultra.network.RemoteToolsDownloader +import com.sukisu.ultra.ui.util.install import com.sukisu.ultra.ui.util.rootAvailable import com.sukisu.ultra.utils.AssetsUtil import com.topjohnwu.superuser.Shell @@ -171,6 +172,12 @@ class HorizonKernelWorker( runCommand(true, "resetprop ro.boot.slot_suffix $originalSlot") } + try { + install() + } catch (e: Exception) { + state.updateStep("ksud update skipped: ${e.message}") + } + state.updateStep(context.getString(R.string.horizon_flash_complete_status)) state.completeFlashing() diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt new file mode 100644 index 0000000..a219809 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettings.kt @@ -0,0 +1,726 @@ +package zako.zako.zako.zakoui.screen.moreSettings + +import android.annotation.SuppressLint +import android.content.Context +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.animation.* +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.filled.* +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.ramcosta.composedestinations.annotation.Destination +import com.ramcosta.composedestinations.annotation.RootGraph +import com.ramcosta.composedestinations.navigation.DestinationsNavigator +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.component.ImageEditorDialog +import com.sukisu.ultra.ui.component.KsuIsValid +import com.sukisu.ultra.ui.theme.* +import com.sukisu.ultra.ui.util.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import zako.zako.zako.zakoui.screen.moreSettings.component.ColorCircle +import zako.zako.zako.zakoui.screen.moreSettings.component.LanguageSelectionDialog +import zako.zako.zako.zakoui.screen.moreSettings.component.MoreSettingsDialogs +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingItem +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsCard +import zako.zako.zako.zakoui.screen.moreSettings.component.SettingsDivider +import zako.zako.zako.zakoui.screen.moreSettings.component.SwitchSettingItem +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState +import kotlin.math.roundToInt + +@SuppressLint("LocalContextConfigurationRead", "LocalContextResourcesRead", "ObsoleteSdkInt") +@OptIn(ExperimentalMaterial3Api::class) +@Destination +@Composable +fun MoreSettingsScreen( + navigator: DestinationsNavigator +) { + // 顶部滚动行为 + val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState()) + val context = LocalContext.current + val coroutineScope = rememberCoroutineScope() + val prefs = remember { context.getSharedPreferences("settings", Context.MODE_PRIVATE) } + val systemIsDark = isSystemInDarkTheme() + + // 创建设置状态管理器 + val settingsState = remember { MoreSettingsState(context, prefs, systemIsDark) } + val settingsHandlers = remember { MoreSettingsHandlers(context, prefs, settingsState) } + + // 图片选择器 + val pickImageLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.GetContent() + ) { uri: Uri? -> + uri?.let { + settingsState.selectedImageUri = it + settingsState.showImageEditor = true + } + } + + // 初始化设置 + LaunchedEffect(Unit) { + settingsHandlers.initializeSettings() + } + + // 显示图片编辑对话框 + if (settingsState.showImageEditor && settingsState.selectedImageUri != null) { + ImageEditorDialog( + imageUri = settingsState.selectedImageUri!!, + onDismiss = { + settingsState.showImageEditor = false + settingsState.selectedImageUri = null + }, + onConfirm = { transformedUri -> + settingsHandlers.handleCustomBackground(transformedUri) + settingsState.showImageEditor = false + settingsState.selectedImageUri = null + } + ) + } + + // 各种设置对话框 + MoreSettingsDialogs( + state = settingsState, + handlers = settingsHandlers + ) + + Scaffold( + modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection), + topBar = { + TopAppBar( + title = { + Text( + text = stringResource(R.string.more_settings), + style = MaterialTheme.typography.titleLarge + ) + }, + navigationIcon = { + IconButton(onClick = { navigator.popBackStack() }) { + Icon( + imageVector = Icons.AutoMirrored.Filled.ArrowBack, + contentDescription = stringResource(R.string.back) + ) + } + }, + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha), + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainerLow.copy(alpha = CardConfig.cardAlpha) + ), + windowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal), + scrollBehavior = scrollBehavior + ) + }, + contentWindowInsets = WindowInsets.safeDrawing.only(WindowInsetsSides.Top + WindowInsetsSides.Horizontal) + ) { paddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .padding(paddingValues) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 16.dp) + .padding(top = 8.dp) + ) { + // 外观设置 + AppearanceSettings( + state = settingsState, + handlers = settingsHandlers, + pickImageLauncher = pickImageLauncher, + coroutineScope = coroutineScope + ) + + // 自定义设置 + CustomizationSettings( + state = settingsState, + handlers = settingsHandlers + ) + + // 高级设置 + KsuIsValid { + AdvancedSettings( + state = settingsState, + handlers = settingsHandlers + ) + } + } + } +} + +@Composable +private fun AppearanceSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + pickImageLauncher: ActivityResultLauncher, + coroutineScope: CoroutineScope +) { + SettingsCard(title = stringResource(R.string.appearance_settings)) { + // 语言设置 + LanguageSetting(state = state) + + // 主题模式 + SettingItem( + icon = Icons.Default.DarkMode, + title = stringResource(R.string.theme_mode), + subtitle = state.themeOptions[state.themeMode], + onClick = { state.showThemeModeDialog = true } + ) + + // 动态颜色开关 + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + SwitchSettingItem( + icon = Icons.Filled.ColorLens, + title = stringResource(R.string.dynamic_color_title), + summary = stringResource(R.string.dynamic_color_summary), + checked = state.useDynamicColor, + onChange = handlers::handleDynamicColorChange + ) + } + + // 主题色选择 + AnimatedVisibility( + visible = Build.VERSION.SDK_INT < Build.VERSION_CODES.S || !state.useDynamicColor, + enter = fadeIn() + expandVertically(), + exit = fadeOut() + shrinkVertically() + ) { + ThemeColorSelection(state = state) + } + + SettingsDivider() + + // DPI 设置 + DpiSettings(state = state, handlers = handlers) + + SettingsDivider() + + // 自定义背景设置 + CustomBackgroundSettings( + state = state, + handlers = handlers, + pickImageLauncher = pickImageLauncher, + coroutineScope = coroutineScope + ) + } +} + +@Composable +private fun CustomizationSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + SettingsCard(title = stringResource(R.string.custom_settings)) { + // 图标切换 + SwitchSettingItem( + icon = Icons.Default.Android, + title = stringResource(R.string.icon_switch_title), + summary = stringResource(R.string.icon_switch_summary), + checked = state.useAltIcon, + onChange = handlers::handleIconChange + ) + + // 显示更多模块信息 + SwitchSettingItem( + icon = Icons.Filled.Info, + title = stringResource(R.string.show_more_module_info), + summary = stringResource(R.string.show_more_module_info_summary), + checked = state.showMoreModuleInfo, + onChange = handlers::handleShowMoreModuleInfoChange + ) + + // 简洁模式开关 + SwitchSettingItem( + icon = Icons.Filled.Brush, + title = stringResource(R.string.simple_mode), + summary = stringResource(R.string.simple_mode_summary), + checked = state.isSimpleMode, + onChange = handlers::handleSimpleModeChange + ) + + SwitchSettingItem( + icon = Icons.Filled.Brush, + title = stringResource(R.string.kernel_simple_kernel), + summary = stringResource(R.string.kernel_simple_kernel_summary), + checked = state.isKernelSimpleMode, + onChange = handlers::handleKernelSimpleModeChange + ) + + // 各种隐藏选项 + HideOptionsSettings(state = state, handlers = handlers) + } +} + +@Composable +private fun HideOptionsSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + // 隐藏内核版本号 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_kernel_kernelsu_version), + summary = stringResource(R.string.hide_kernel_kernelsu_version_summary), + checked = state.isHideVersion, + onChange = handlers::handleHideVersionChange + ) + + // 隐藏模块数量等信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_other_info), + summary = stringResource(R.string.hide_other_info_summary), + checked = state.isHideOtherInfo, + onChange = handlers::handleHideOtherInfoChange + ) + + // SuSFS 状态信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_susfs_status), + summary = stringResource(R.string.hide_susfs_status_summary), + checked = state.isHideSusfsStatus, + onChange = handlers::handleHideSusfsStatusChange + ) + + // Zygisk 实现状态信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_zygisk_implement), + summary = stringResource(R.string.hide_zygisk_implement_summary), + checked = state.isHideZygiskImplement, + onChange = handlers::handleHideZygiskImplementChange + ) + + if (Natives.version >= Natives.MINIMAL_SUPPORTED_KPM) { + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.show_kpm_info), + summary = stringResource(R.string.show_kpm_info_summary), + checked = state.isShowKpmInfo, + onChange = handlers::handleShowKpmInfoChange + ) + } + + // 隐藏链接信息 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_link_card), + summary = stringResource(R.string.hide_link_card_summary), + checked = state.isHideLinkCard, + onChange = handlers::handleHideLinkCardChange + ) + + // 隐藏标签行 + SwitchSettingItem( + icon = Icons.Filled.VisibilityOff, + title = stringResource(R.string.hide_tag_card), + summary = stringResource(R.string.hide_tag_card_summary), + checked = state.isHideTagRow, + onChange = handlers::handleHideTagRowChange + ) +} + +@Composable +private fun AdvancedSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + SettingsCard(title = stringResource(R.string.advanced_settings)) { + // SELinux 开关 + SwitchSettingItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.selinux), + summary = if (state.selinuxEnabled) + stringResource(R.string.selinux_enabled) else + stringResource(R.string.selinux_disabled), + checked = state.selinuxEnabled, + onChange = handlers::handleSelinuxChange + ) + + // 动态管理器设置 + if (Natives.version >= Natives.MINIMAL_SUPPORTED_DYNAMIC_MANAGER && Natives.version >= Natives.MINIMAL_NEW_IOCTL_KERNEL) { + SettingItem( + icon = Icons.Filled.Security, + title = stringResource(R.string.dynamic_manager_title), + subtitle = if (state.isDynamicSignEnabled) { + stringResource(R.string.dynamic_manager_enabled_summary, state.dynamicSignSize) + } else { + stringResource(R.string.dynamic_manager_disabled) + }, + onClick = { state.showDynamicSignDialog = true } + ) + } + } +} + +@Composable +private fun ThemeColorSelection(state: MoreSettingsState) { + SettingItem( + icon = Icons.Default.Palette, + title = stringResource(R.string.theme_color), + subtitle = when (ThemeConfig.currentTheme) { + is ThemeColors.Green -> stringResource(R.string.color_green) + is ThemeColors.Purple -> stringResource(R.string.color_purple) + is ThemeColors.Orange -> stringResource(R.string.color_orange) + is ThemeColors.Pink -> stringResource(R.string.color_pink) + is ThemeColors.Gray -> stringResource(R.string.color_gray) + is ThemeColors.Yellow -> stringResource(R.string.color_yellow) + else -> stringResource(R.string.color_default) + }, + onClick = { state.showThemeColorDialog = true }, + trailingContent = { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(start = 8.dp) + ) { + val theme = ThemeConfig.currentTheme + val isDark = isSystemInDarkTheme() + + ColorCircle( + color = if (isDark) theme.primaryDark else theme.primaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.secondaryDark else theme.secondaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } + } + ) +} + +@Composable +private fun DpiSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + SettingItem( + icon = Icons.Default.FormatSize, + title = stringResource(R.string.app_dpi_title), + subtitle = stringResource(R.string.app_dpi_summary), + onClick = {}, + trailingContent = { + Text( + text = handlers.getDpiFriendlyName(state.tempDpi), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary + ) + } + ) + + // DPI 滑动条和控制 + DpiSliderControls(state = state, handlers = handlers) +} + +@Composable +private fun DpiSliderControls( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + val sliderValue by animateFloatAsState( + targetValue = state.tempDpi.toFloat(), + label = "DPI Slider Animation" + ) + + Slider( + value = sliderValue, + onValueChange = { newValue -> + state.tempDpi = newValue.toInt() + state.isDpiCustom = !state.dpiPresets.containsValue(state.tempDpi) + }, + valueRange = 160f..600f, + steps = 11, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) + + // DPI 预设按钮行 + Row( + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + ) { + state.dpiPresets.forEach { (name, dpi) -> + val isSelected = state.tempDpi == dpi + val buttonColor = if (isSelected) + MaterialTheme.colorScheme.primaryContainer + else + MaterialTheme.colorScheme.surfaceVariant + + Box( + modifier = Modifier + .weight(1f) + .padding(horizontal = 2.dp) + .clip(RoundedCornerShape(8.dp)) + .background(buttonColor) + .clickable { + state.tempDpi = dpi + state.isDpiCustom = false + } + .padding(vertical = 8.dp, horizontal = 4.dp), + contentAlignment = Alignment.Center + ) { + Text( + text = name, + style = MaterialTheme.typography.labelMedium, + color = if (isSelected) + MaterialTheme.colorScheme.onPrimaryContainer + else + MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + + Text( + text = if (state.isDpiCustom) + "${stringResource(R.string.dpi_size_custom)}: ${state.tempDpi}" + else + "${handlers.getDpiFriendlyName(state.tempDpi)}: ${state.tempDpi}", + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(top = 8.dp) + ) + + Button( + onClick = { state.showDpiConfirmDialog = true }, + modifier = Modifier + .fillMaxWidth() + .padding(top = 8.dp), + enabled = state.tempDpi != state.currentDpi + ) { + Icon( + Icons.Default.Check, + contentDescription = null, + modifier = Modifier.size(16.dp) + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(stringResource(R.string.dpi_apply_settings)) + } + } +} + +@Composable +private fun CustomBackgroundSettings( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + pickImageLauncher: ActivityResultLauncher, + coroutineScope: CoroutineScope +) { + // 自定义背景开关 + SwitchSettingItem( + icon = Icons.Filled.Wallpaper, + title = stringResource(id = R.string.settings_custom_background), + summary = stringResource(id = R.string.settings_custom_background_summary), + checked = state.isCustomBackgroundEnabled, + onChange = { isChecked -> + if (isChecked) { + pickImageLauncher.launch("image/*") + } else { + handlers.handleRemoveCustomBackground() + } + } + ) + + // 透明度和亮度调节 + AnimatedVisibility( + visible = ThemeConfig.customBackgroundUri != null, + enter = fadeIn() + slideInVertically(), + exit = fadeOut() + slideOutVertically() + ) { + BackgroundAdjustmentControls( + state = state, + handlers = handlers, + coroutineScope = coroutineScope + ) + } +} + +@Composable +private fun BackgroundAdjustmentControls( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Column(modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp)) { + // 透明度滑动条 + AlphaSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) + + // 亮度调节滑动条 + DimSlider(state = state, handlers = handlers, coroutineScope = coroutineScope) + } +} + +@Composable +private fun AlphaSlider( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(bottom = 4.dp) + ) { + Icon( + Icons.Filled.Opacity, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.settings_card_alpha), + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${(state.cardAlpha * 100).roundToInt()}%", + style = MaterialTheme.typography.labelMedium, + ) + } + + val alphaSliderValue by animateFloatAsState( + targetValue = state.cardAlpha, + label = "Alpha Slider Animation" + ) + + Slider( + value = alphaSliderValue, + onValueChange = { newValue -> + handlers.handleCardAlphaChange(newValue) + }, + onValueChangeFinished = { + coroutineScope.launch(Dispatchers.IO) { + saveCardConfig(handlers.context) + } + }, + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) +} + +@Composable +private fun DimSlider( + state: MoreSettingsState, + handlers: MoreSettingsHandlers, + coroutineScope: CoroutineScope +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.padding(top = 16.dp, bottom = 4.dp) + ) { + Icon( + Icons.Filled.LightMode, + contentDescription = null, + modifier = Modifier.size(20.dp), + tint = MaterialTheme.colorScheme.primary + ) + Spacer(modifier = Modifier.width(8.dp)) + Text( + text = stringResource(R.string.settings_card_dim), + style = MaterialTheme.typography.titleSmall + ) + Spacer(modifier = Modifier.weight(1f)) + Text( + text = "${(state.cardDim * 100).roundToInt()}%", + style = MaterialTheme.typography.labelMedium, + ) + } + + val dimSliderValue by animateFloatAsState( + targetValue = state.cardDim, + label = "Dim Slider Animation" + ) + + Slider( + value = dimSliderValue, + onValueChange = { newValue -> + handlers.handleCardDimChange(newValue) + }, + onValueChangeFinished = { + coroutineScope.launch(Dispatchers.IO) { + saveCardConfig(handlers.context) + } + }, + valueRange = 0f..1f, + steps = 20, + colors = SliderDefaults.colors( + thumbColor = MaterialTheme.colorScheme.primary, + activeTrackColor = MaterialTheme.colorScheme.primary, + inactiveTrackColor = MaterialTheme.colorScheme.surfaceVariant + ) + ) +} + +fun saveCardConfig(context: Context) { + CardConfig.save(context) +} + +@Composable +private fun LanguageSetting(state: MoreSettingsState) { + val context = LocalContext.current + val language = stringResource(id = R.string.settings_language) + + // Compute display name based on current app locale + val currentLanguageDisplay = remember(state.currentAppLocale) { + val locale = state.currentAppLocale + if (locale != null) { + locale.getDisplayName(locale) + } else { + context.getString(R.string.language_system_default) + } + } + + SettingItem( + icon = Icons.Filled.Translate, + title = language, + subtitle = currentLanguageDisplay, + onClick = { state.showLanguageDialog = true } + ) + + // Language Selection Dialog + if (state.showLanguageDialog) { + LanguageSelectionDialog( + onLanguageSelected = { newLocale -> + // Update local state immediately + state.currentAppLocale = LocaleHelper.getCurrentAppLocale(context) + // Apply locale change immediately for Android < 13 + LocaleHelper.restartActivity(context) + }, + onDismiss = { state.showLanguageDialog = false } + ) + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt new file mode 100644 index 0000000..1584c8d --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/MoreSettingsHandlers.kt @@ -0,0 +1,436 @@ +package zako.zako.zako.zakoui.screen.moreSettings + +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.content.res.Configuration +import android.net.Uri +import android.widget.Toast +import androidx.compose.runtime.Composable +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.* +import com.sukisu.ultra.ui.util.* +import com.topjohnwu.superuser.Shell +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState +import zako.zako.zako.zakoui.screen.moreSettings.util.toggleLauncherIcon + +/** + * 更多设置处理器 + */ +class MoreSettingsHandlers( + val context: Context, + private val prefs: SharedPreferences, + private val state: MoreSettingsState +) { + + /** + * 初始化设置 + */ + fun initializeSettings() { + // 加载设置 + CardConfig.load(context) + state.cardAlpha = CardConfig.cardAlpha + state.cardDim = CardConfig.cardDim + state.isCustomBackgroundEnabled = ThemeConfig.customBackgroundUri != null + + // 设置主题模式 + state.themeMode = when (ThemeConfig.forceDarkMode) { + true -> 2 + false -> 1 + null -> 0 + } + + // 确保卡片样式跟随主题模式 + when (state.themeMode) { + 2 -> { // 深色 + CardConfig.isUserDarkModeEnabled = true + CardConfig.isUserLightModeEnabled = false + } + 1 -> { // 浅色 + CardConfig.isUserDarkModeEnabled = false + CardConfig.isUserLightModeEnabled = true + } + 0 -> { // 跟随系统 + CardConfig.isUserDarkModeEnabled = false + CardConfig.isUserLightModeEnabled = false + } + } + + // 如果启用了系统跟随且系统是深色模式,应用深色模式默认值 + if (state.themeMode == 0 && state.systemIsDark) { + CardConfig.setThemeDefaults(true) + } + + state.currentDpi = prefs.getInt("app_dpi", state.systemDpi) + state.tempDpi = state.currentDpi + + CardConfig.save(context) + + // 初始化 SELinux 状态 + state.selinuxEnabled = Shell.cmd("getenforce").exec().out.firstOrNull() == "Enforcing" + + // 初始化动态管理器配置 + state.dynamicSignConfig = Natives.getDynamicManager() + state.dynamicSignConfig?.let { config -> + if (config.isValid()) { + state.isDynamicSignEnabled = true + state.dynamicSignSize = config.size.toString() + state.dynamicSignHash = config.hash + } + } + } + + /** + * 处理主题模式变更 + */ + fun handleThemeModeChange(index: Int) { + state.themeMode = index + val newThemeMode = when (index) { + 0 -> null // 跟随系统 + 1 -> false // 浅色 + 2 -> true // 深色 + else -> null + } + context.saveThemeMode(newThemeMode) + ThemeConfig.updateTheme(darkMode = newThemeMode) + + when (index) { + 2 -> { // 深色 + ThemeConfig.updateTheme(darkMode = true) + CardConfig.updateThemePreference(darkMode = true, lightMode = false) + CardConfig.setThemeDefaults(true) + CardConfig.save(context) + } + 1 -> { // 浅色 + ThemeConfig.updateTheme(darkMode = false) + CardConfig.updateThemePreference(darkMode = false, lightMode = true) + CardConfig.setThemeDefaults(false) + CardConfig.save(context) + } + 0 -> { // 跟随系统 + ThemeConfig.updateTheme(darkMode = null) + CardConfig.updateThemePreference(darkMode = null, lightMode = null) + val isNightModeActive = (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES + CardConfig.setThemeDefaults(isNightModeActive) + CardConfig.save(context) + } + } + } + + /** + * 处理主题色变更 + */ + fun handleThemeColorChange(theme: ThemeColors) { + context.saveThemeColors(when (theme) { + ThemeColors.Green -> "green" + ThemeColors.Purple -> "purple" + ThemeColors.Orange -> "orange" + ThemeColors.Pink -> "pink" + ThemeColors.Gray -> "gray" + ThemeColors.Yellow -> "yellow" + else -> "default" + }) + ThemeConfig.updateTheme(theme = theme) + } + + /** + * 处理动态颜色变更 + */ + fun handleDynamicColorChange(enabled: Boolean) { + state.useDynamicColor = enabled + context.saveDynamicColorState(enabled) + ThemeConfig.updateTheme(dynamicColor = enabled) + } + + /** + * 获取DPI大小友好名称 + */ + @Composable + fun getDpiFriendlyName(dpi: Int): String { + return when (dpi) { + 240 -> stringResource(R.string.dpi_size_small) + 320 -> stringResource(R.string.dpi_size_medium) + 420 -> stringResource(R.string.dpi_size_large) + 560 -> stringResource(R.string.dpi_size_extra_large) + else -> stringResource(R.string.dpi_size_custom) + } + } + + /** + * 应用 DPI 设置 + */ + fun handleDpiApply() { + if (state.tempDpi != state.currentDpi) { + prefs.edit { + putInt("app_dpi", state.tempDpi) + } + + state.currentDpi = state.tempDpi + Toast.makeText( + context, + context.getString(R.string.dpi_applied_success, state.tempDpi), + Toast.LENGTH_SHORT + ).show() + + val restartIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) + restartIntent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(restartIntent) + + state.showDpiConfirmDialog = false + } + } + + /** + * 处理自定义背景 + */ + fun handleCustomBackground(transformedUri: Uri) { + context.saveAndApplyCustomBackground(transformedUri) + state.isCustomBackgroundEnabled = true + CardConfig.cardElevation = 0.dp + CardConfig.isCustomBackgroundEnabled = true + saveCardConfig(context) + + Toast.makeText( + context, + context.getString(R.string.background_set_success), + Toast.LENGTH_SHORT + ).show() + } + + /** + * 处理移除自定义背景 + */ + fun handleRemoveCustomBackground() { + context.saveCustomBackground(null) + state.isCustomBackgroundEnabled = false + CardConfig.cardAlpha = 1f + CardConfig.cardDim = 0f + CardConfig.isCustomAlphaSet = false + CardConfig.isCustomDimSet = false + CardConfig.isCustomBackgroundEnabled = false + saveCardConfig(context) + ThemeConfig.preventBackgroundRefresh = false + + context.getSharedPreferences("theme_prefs", Context.MODE_PRIVATE).edit { + putBoolean("prevent_background_refresh", false) + } + + Toast.makeText( + context, + context.getString(R.string.background_removed), + Toast.LENGTH_SHORT + ).show() + } + + /** + * 处理卡片透明度变更 + */ + fun handleCardAlphaChange(newValue: Float) { + state.cardAlpha = newValue + CardConfig.cardAlpha = newValue + CardConfig.isCustomAlphaSet = true + prefs.edit { + putBoolean("is_custom_alpha_set", true) + putFloat("card_alpha", newValue) + } + } + + /** + * 处理卡片亮度变更 + */ + fun handleCardDimChange(newValue: Float) { + state.cardDim = newValue + CardConfig.cardDim = newValue + CardConfig.isCustomDimSet = true + prefs.edit { + putBoolean("is_custom_dim_set", true) + putFloat("card_dim", newValue) + } + } + + /** + * 处理图标变更 + */ + fun handleIconChange(newValue: Boolean) { + prefs.edit { putBoolean("use_alt_icon", newValue) } + state.useAltIcon = newValue + toggleLauncherIcon(context, newValue) + Toast.makeText(context, context.getString(R.string.icon_switched), Toast.LENGTH_SHORT).show() + } + + /** + * 处理简洁模式变更 + */ + fun handleSimpleModeChange(newValue: Boolean) { + prefs.edit { putBoolean("is_simple_mode", newValue) } + state.isSimpleMode = newValue + } + + /** + * 处理内核简洁模式变更 + */ + fun handleKernelSimpleModeChange(newValue: Boolean) { + prefs.edit { putBoolean("is_kernel_simple_mode", newValue) } + state.isKernelSimpleMode = newValue + } + + /** + * 处理隐藏版本变更 + */ + fun handleHideVersionChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_version", newValue) } + state.isHideVersion = newValue + } + + /** + * 处理隐藏其他信息变更 + */ + fun handleHideOtherInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_other_info", newValue) } + state.isHideOtherInfo = newValue + } + + /** + * 处理显示KPM信息变更 + */ + fun handleShowKpmInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("show_kpm_info", newValue) } + state.isShowKpmInfo = newValue + } + + /** + * 处理隐藏SuSFS状态变更 + */ + fun handleHideSusfsStatusChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_susfs_status", newValue) } + state.isHideSusfsStatus = newValue + } + + /** + * 处理隐藏Zygisk实现变更 + */ + fun handleHideZygiskImplementChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_zygisk_Implement", newValue) } + state.isHideZygiskImplement = newValue + } + + /** + * 处理隐藏链接卡片变更 + */ + fun handleHideLinkCardChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_link_card", newValue) } + state.isHideLinkCard = newValue + } + + /** + * 处理隐藏标签行变更 + */ + fun handleHideTagRowChange(newValue: Boolean) { + prefs.edit { putBoolean("is_hide_tag_row", newValue) } + state.isHideTagRow = newValue + } + + /** + * 处理显示更多模块信息变更 + */ + fun handleShowMoreModuleInfoChange(newValue: Boolean) { + prefs.edit { putBoolean("show_more_module_info", newValue) } + state.showMoreModuleInfo = newValue + } + + /** + * 处理SELinux变更 + */ + fun handleSelinuxChange(enabled: Boolean) { + val command = if (enabled) "setenforce 1" else "setenforce 0" + Shell.getShell().newJob().add(command).exec().let { result -> + if (result.isSuccess) { + state.selinuxEnabled = enabled + val message = if (enabled) + context.getString(R.string.selinux_enabled_toast) + else + context.getString(R.string.selinux_disabled_toast) + + Toast.makeText(context, message, Toast.LENGTH_SHORT).show() + } else { + Toast.makeText( + context, + context.getString(R.string.selinux_change_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } + + /** + * 处理动态管理器配置 + */ + fun handleDynamicManagerConfig(enabled: Boolean, size: String, hash: String) { + if (enabled) { + val parsedSize = parseDynamicSignSize(size) + if (parsedSize != null && parsedSize > 0 && hash.length == 64) { + val success = Natives.setDynamicManager(parsedSize, hash) + if (success) { + state.dynamicSignConfig = Natives.DynamicManagerConfig(parsedSize, hash) + state.isDynamicSignEnabled = true + state.dynamicSignSize = size + state.dynamicSignHash = hash + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_set_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_set_failed), + Toast.LENGTH_SHORT + ).show() + } + } else { + Toast.makeText( + context, + context.getString(R.string.invalid_sign_config), + Toast.LENGTH_SHORT + ).show() + } + } else { + val success = Natives.clearDynamicManager() + if (success) { + state.dynamicSignConfig = null + state.isDynamicSignEnabled = false + state.dynamicSignSize = "" + state.dynamicSignHash = "" + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_disabled_success), + Toast.LENGTH_SHORT + ).show() + } else { + Toast.makeText( + context, + context.getString(R.string.dynamic_manager_clear_failed), + Toast.LENGTH_SHORT + ).show() + } + } + } + + /** + * 解析动态签名大小 + */ + private fun parseDynamicSignSize(input: String): Int? { + return try { + when { + input.startsWith("0x", true) -> input.substring(2).toInt(16) + else -> input.toInt() + } + } catch (_: NumberFormatException) { + null + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt new file mode 100644 index 0000000..3c182c1 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsComponents.kt @@ -0,0 +1,201 @@ +package zako.zako.zako.zakoui.screen.moreSettings.component + +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.NavigateNext +import androidx.compose.material3.* +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.graphics.vector.ImageVector +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.sukisu.ultra.ui.theme.* + +private val SETTINGS_GROUP_SPACING = 16.dp + +@Composable +fun SettingsCard( + title: String, + icon: ImageVector? = null, + content: @Composable () -> Unit +) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = SETTINGS_GROUP_SPACING), + colors = getCardColors(MaterialTheme.colorScheme.surfaceContainerHigh), + elevation = getCardElevation(), + shape = MaterialTheme.shapes.medium + ) { + Column(modifier = Modifier.padding(vertical = 8.dp)) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .height(48.dp) + .padding(horizontal = 16.dp) + ) { + if (icon != null) { + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(24.dp) + ) + Spacer(modifier = Modifier.width(12.dp)) + } + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + ) + } + content() + } + } +} + +@Composable +fun SettingItem( + icon: ImageVector, + title: String, + subtitle: String? = null, + onClick: () -> Unit, + iconTint: Color = MaterialTheme.colorScheme.primary, + trailingContent: @Composable (() -> Unit)? = { + Icon( + Icons.AutoMirrored.Filled.NavigateNext, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable(onClick = onClick) + .padding(horizontal = 16.dp, vertical = 5.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = iconTint, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Visible + ) + if (subtitle != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = Int.MAX_VALUE, + overflow = TextOverflow.Visible + ) + } + } + + trailingContent?.invoke() + } +} + +@Composable +fun SwitchSettingItem( + icon: ImageVector, + title: String, + summary: String? = null, + checked: Boolean, + onChange: (Boolean) -> Unit +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onChange(!checked) } + .padding(horizontal = 16.dp, vertical = 10.dp), + verticalAlignment = Alignment.Top + ) { + Icon( + imageVector = icon, + contentDescription = null, + tint = if (checked) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier + .padding(end = 16.dp) + .size(24.dp) + ) + + Column( + modifier = Modifier.weight(1f), + verticalArrangement = Arrangement.Center + ) { + Text( + text = title, + style = MaterialTheme.typography.titleMedium, + lineHeight = 20.sp, + ) + if (summary != null) { + Spacer(modifier = Modifier.height(2.dp)) + Text( + text = summary, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + lineHeight = 16.sp, + ) + } + } + + Switch( + checked = checked, + onCheckedChange = onChange + ) + } +} + +@Composable +fun SettingsDivider() { + HorizontalDivider( + modifier = Modifier.padding(vertical = 8.dp) + ) +} + +@Composable +fun ColorCircle( + color: Color, + isSelected: Boolean, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(20.dp) + .clip(CircleShape) + .background(color) + .then( + if (isSelected) { + Modifier.border( + width = 2.dp, + color = MaterialTheme.colorScheme.primary, + shape = CircleShape + ) + } else { + Modifier + } + ) + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt new file mode 100644 index 0000000..c321c93 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/component/MoreSettingsDialogs.kt @@ -0,0 +1,476 @@ +package zako.zako.zako.zakoui.screen.moreSettings.component + +import android.content.Context +import androidx.compose.foundation.* +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import com.maxkeppeker.sheets.core.models.base.Header +import com.maxkeppeker.sheets.core.models.base.rememberUseCaseState +import com.maxkeppeler.sheets.list.ListDialog +import com.maxkeppeler.sheets.list.models.ListOption +import com.maxkeppeler.sheets.list.models.ListSelection +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.* +import zako.zako.zako.zakoui.screen.moreSettings.MoreSettingsHandlers +import zako.zako.zako.zakoui.screen.moreSettings.state.MoreSettingsState + +@Composable +fun MoreSettingsDialogs( + state: MoreSettingsState, + handlers: MoreSettingsHandlers +) { + // 主题模式选择对话框 + if (state.showThemeModeDialog) { + SingleChoiceDialog( + title = stringResource(R.string.theme_mode), + options = state.themeOptions, + selectedIndex = state.themeMode, + onOptionSelected = { index -> + handlers.handleThemeModeChange(index) + }, + onDismiss = { state.showThemeModeDialog = false } + ) + } + + // DPI 设置确认对话框 + if (state.showDpiConfirmDialog) { + ConfirmDialog( + title = stringResource(R.string.dpi_confirm_title), + message = stringResource(R.string.dpi_confirm_message, state.currentDpi, state.tempDpi), + summaryText = stringResource(R.string.dpi_confirm_summary), + confirmText = stringResource(R.string.confirm), + dismissText = stringResource(R.string.cancel), + onConfirm = { handlers.handleDpiApply() }, + onDismiss = { + state.showDpiConfirmDialog = false + state.tempDpi = state.currentDpi + } + ) + } + + // 主题色选择对话框 + if (state.showThemeColorDialog) { + ThemeColorDialog( + onColorSelected = { theme -> + handlers.handleThemeColorChange(theme) + state.showThemeColorDialog = false + }, + onDismiss = { state.showThemeColorDialog = false } + ) + } + + // 动态管理器配置对话框 + if (state.showDynamicSignDialog) { + DynamicManagerDialog( + state = state, + onConfirm = { enabled, size, hash -> + handlers.handleDynamicManagerConfig(enabled, size, hash) + state.showDynamicSignDialog = false + }, + onDismiss = { state.showDynamicSignDialog = false } + ) + } +} + +@Composable +fun SingleChoiceDialog( + title: String, + options: List, + selectedIndex: Int, + onOptionSelected: (Int) -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column(modifier = Modifier.verticalScroll(rememberScrollState())) { + options.forEachIndexed { index, option -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onOptionSelected(index) + onDismiss() + } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + RadioButton( + selected = selectedIndex == index, + onClick = null + ) + Spacer(modifier = Modifier.width(8.dp)) + Text(option) + } + } + } + }, + confirmButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun ConfirmDialog( + title: String, + message: String, + summaryText: String? = null, + confirmText: String = stringResource(R.string.confirm), + dismissText: String = stringResource(R.string.cancel), + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(title) }, + text = { + Column { + Text(message) + if (summaryText != null) { + Spacer(modifier = Modifier.height(8.dp)) + Text( + summaryText, + style = MaterialTheme.typography.bodySmall + ) + } + } + }, + confirmButton = { + TextButton(onClick = onConfirm) { + Text(confirmText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(dismissText) + } + } + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LanguageSelectionDialog( + onLanguageSelected: (String) -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + + // Check if should use system language settings + if (LocaleHelper.useSystemLanguageSettings) { + // Android 13+ - Jump to system settings + LocaleHelper.launchSystemLanguageSettings(context) + onDismiss() + } else { + // Android < 13 - Show app language selector + // Dynamically detect supported locales from resources + val supportedLocales = remember { + val locales = mutableListOf() + + // Add system default first + locales.add(java.util.Locale.ROOT) // This will represent "System Default" + + // Dynamically detect available locales by checking resource directories + val resourceDirs = listOf( + "ar", "bg", "de", "fa", "fr", "hu", "in", "it", + "ja", "ko", "pl", "pt-rBR", "ru", "th", "tr", + "uk", "vi", "zh-rCN", "zh-rTW" + ) + + resourceDirs.forEach { dir -> + try { + val locale = when { + dir.contains("-r") -> { + val parts = dir.split("-r") + java.util.Locale.Builder() + .setLanguage(parts[0]) + .setRegion(parts[1]) + .build() + } + else -> java.util.Locale.Builder() + .setLanguage(dir) + .build() + } + + // Test if this locale has translated resources + val config = android.content.res.Configuration() + config.setLocale(locale) + val localizedContext = context.createConfigurationContext(config) + + // Try to get a translated string to verify the locale is supported + val testString = localizedContext.getString(R.string.settings_language) + val defaultString = context.getString(R.string.settings_language) + + // If the string is different or it's English, it's supported + if (testString != defaultString || locale.language == "en") { + locales.add(locale) + } + } catch (_: Exception) { + // Skip unsupported locales + } + } + + // Sort by display name + val sortedLocales = locales.drop(1).sortedBy { it.getDisplayName(it) } + mutableListOf().apply { + add(locales.first()) // System default first + addAll(sortedLocales) + } + } + + val allOptions = supportedLocales.map { locale -> + val tag = if (locale == java.util.Locale.ROOT) { + "system" + } else if (locale.country.isEmpty()) { + locale.language + } else { + "${locale.language}_${locale.country}" + } + + val displayName = if (locale == java.util.Locale.ROOT) { + context.getString(R.string.language_system_default) + } else { + locale.getDisplayName(locale) + } + + tag to displayName + } + + val currentLocale = prefs.getString("app_locale", "system") ?: "system" + val options = allOptions.map { (tag, displayName) -> + ListOption( + titleText = displayName, + selected = currentLocale == tag + ) + } + + var selectedIndex by remember { + mutableIntStateOf(allOptions.indexOfFirst { (tag, _) -> currentLocale == tag }) + } + + ListDialog( + state = rememberUseCaseState( + visible = true, + onFinishedRequest = { + if (selectedIndex >= 0 && selectedIndex < allOptions.size) { + val newLocale = allOptions[selectedIndex].first + prefs.edit { putString("app_locale", newLocale) } + onLanguageSelected(newLocale) + } + onDismiss() + }, + onCloseRequest = { + onDismiss() + } + ), + header = Header.Default( + title = stringResource(R.string.settings_language), + ), + selection = ListSelection.Single( + showRadioButtons = true, + options = options + ) { index, _ -> + selectedIndex = index + } + ) + } +} +@Composable +fun ThemeColorDialog( + onColorSelected: (ThemeColors) -> Unit, + onDismiss: () -> Unit +) { + val themeColorOptions = listOf( + stringResource(R.string.color_default) to ThemeColors.Default, + stringResource(R.string.color_green) to ThemeColors.Green, + stringResource(R.string.color_purple) to ThemeColors.Purple, + stringResource(R.string.color_orange) to ThemeColors.Orange, + stringResource(R.string.color_pink) to ThemeColors.Pink, + stringResource(R.string.color_gray) to ThemeColors.Gray, + stringResource(R.string.color_yellow) to ThemeColors.Yellow + ) + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.choose_theme_color)) }, + text = { + Column { + themeColorOptions.forEach { (name, theme) -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onColorSelected(theme) } + .padding(vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + val isDark = isSystemInDarkTheme() + Box( + modifier = Modifier.padding(end = 12.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + ColorCircle( + color = if (isDark) theme.primaryDark else theme.primaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.secondaryDark else theme.secondaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + ColorCircle( + color = if (isDark) theme.tertiaryDark else theme.tertiaryLight, + isSelected = false, + modifier = Modifier.padding(horizontal = 2.dp) + ) + } + } + Text(name) + Spacer(modifier = Modifier.weight(1f)) + // 当前选中的主题显示选中标记 + if (ThemeConfig.currentTheme::class == theme::class) { + Icon( + Icons.Default.Check, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + } + } + } + }, + confirmButton = { + Button( + onClick = onDismiss + ) { + Text(stringResource(R.string.cancel)) + } + } + ) +} + +@Composable +fun DynamicManagerDialog( + state: MoreSettingsState, + onConfirm: (Boolean, String, String) -> Unit, + onDismiss: () -> Unit +) { + var localEnabled by remember { mutableStateOf(state.isDynamicSignEnabled) } + var localSize by remember { mutableStateOf(state.dynamicSignSize) } + var localHash by remember { mutableStateOf(state.dynamicSignHash) } + + fun parseDynamicSignSize(input: String): Int? { + return try { + when { + input.startsWith("0x", true) -> input.substring(2).toInt(16) + else -> input.toInt() + } + } catch (_: NumberFormatException) { + null + } + } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.dynamic_manager_title)) }, + text = { + Column( + modifier = Modifier.verticalScroll(rememberScrollState()) + ) { + // 启用开关 + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { localEnabled = !localEnabled } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Switch( + checked = localEnabled, + onCheckedChange = { localEnabled = it } + ) + Spacer(modifier = Modifier.width(12.dp)) + Text(stringResource(R.string.enable_dynamic_manager)) + } + + Spacer(modifier = Modifier.height(16.dp)) + + // 签名大小输入 + OutlinedTextField( + value = localSize, + onValueChange = { input -> + val isValid = when { + input.isEmpty() -> true + input.matches(Regex("^\\d+$")) -> true + input.matches(Regex("^0[xX][0-9a-fA-F]*$")) -> true + else -> false + } + if (isValid) { + localSize = input + } + }, + label = { Text(stringResource(R.string.signature_size)) }, + enabled = localEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + keyboardOptions = KeyboardOptions( + keyboardType = KeyboardType.Text + ) + ) + + Spacer(modifier = Modifier.height(12.dp)) + + // 签名哈希输入 + OutlinedTextField( + value = localHash, + onValueChange = { hash -> + if (hash.all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }) { + localHash = hash + } + }, + label = { Text(stringResource(R.string.signature_hash)) }, + enabled = localEnabled, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + supportingText = { + Text(stringResource(R.string.hash_must_be_64_chars)) + }, + isError = localEnabled && localHash.isNotEmpty() && localHash.length != 64 + ) + } + }, + confirmButton = { + Button( + onClick = { onConfirm(localEnabled, localSize, localHash) }, + enabled = if (localEnabled) { + parseDynamicSignSize(localSize)?.let { it > 0 } == true && + localHash.length == 64 + } else true + ) { + Text(stringResource(R.string.confirm)) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(stringResource(R.string.cancel)) + } + } + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt new file mode 100644 index 0000000..ec1abf8 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/state/MoreSettingsState.kt @@ -0,0 +1,104 @@ +package zako.zako.zako.zakoui.screen.moreSettings.state + +import android.content.Context +import android.content.SharedPreferences +import android.net.Uri +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import zako.zako.zako.zakoui.screen.moreSettings.util.LocaleHelper +import com.sukisu.ultra.Natives +import com.sukisu.ultra.R +import com.sukisu.ultra.ui.theme.CardConfig +import com.sukisu.ultra.ui.theme.ThemeConfig + +/** + * 更多设置状态管理 + */ +@Stable +class MoreSettingsState( + val context: Context, + val prefs: SharedPreferences, + val systemIsDark: Boolean +) { + // 主题模式选择 + var themeMode by mutableIntStateOf( + when (ThemeConfig.forceDarkMode) { + true -> 2 // 深色 + false -> 1 // 浅色 + null -> 0 // 跟随系统 + } + ) + + // 动态颜色开关状态 + var useDynamicColor by mutableStateOf(ThemeConfig.useDynamicColor) + + // 语言设置 + var showLanguageDialog by mutableStateOf(false) + var currentAppLocale by mutableStateOf(LocaleHelper.getCurrentAppLocale(context)) + + // 对话框显示状态 + var showThemeModeDialog by mutableStateOf(false) + var showThemeColorDialog by mutableStateOf(false) + var showDpiConfirmDialog by mutableStateOf(false) + var showImageEditor by mutableStateOf(false) + + // 动态管理器配置状态 + var dynamicSignConfig by mutableStateOf(null) + var isDynamicSignEnabled by mutableStateOf(false) + var dynamicSignSize by mutableStateOf("") + var dynamicSignHash by mutableStateOf("") + var showDynamicSignDialog by mutableStateOf(false) + + + // 各种设置开关状态 + var isSimpleMode by mutableStateOf(prefs.getBoolean("is_simple_mode", false)) + var isHideVersion by mutableStateOf(prefs.getBoolean("is_hide_version", false)) + var isHideOtherInfo by mutableStateOf(prefs.getBoolean("is_hide_other_info", false)) + var isShowKpmInfo by mutableStateOf(prefs.getBoolean("show_kpm_info", false)) + var isHideZygiskImplement by mutableStateOf(prefs.getBoolean("is_hide_zygisk_Implement", false)) + var isHideSusfsStatus by mutableStateOf(prefs.getBoolean("is_hide_susfs_status", false)) + var isHideLinkCard by mutableStateOf(prefs.getBoolean("is_hide_link_card", false)) + var isHideTagRow by mutableStateOf(prefs.getBoolean("is_hide_tag_row", false)) + var isKernelSimpleMode by mutableStateOf(prefs.getBoolean("is_kernel_simple_mode", false)) + var showMoreModuleInfo by mutableStateOf(prefs.getBoolean("show_more_module_info", false)) + var useAltIcon by mutableStateOf(prefs.getBoolean("use_alt_icon", false)) + + // SELinux状态 + var selinuxEnabled by mutableStateOf(false) + + // SuSFS 状态 + var isSusFSEnabled by mutableStateOf(true) + + // 卡片配置状态 + var cardAlpha by mutableFloatStateOf(CardConfig.cardAlpha) + var cardDim by mutableFloatStateOf(CardConfig.cardDim) + var isCustomBackgroundEnabled by mutableStateOf(ThemeConfig.customBackgroundUri != null) + + // 图片选择状态 + var selectedImageUri by mutableStateOf(null) + + // DPI 设置 + val systemDpi = context.resources.displayMetrics.densityDpi + var currentDpi by mutableIntStateOf(prefs.getInt("app_dpi", systemDpi)) + var tempDpi by mutableIntStateOf(currentDpi) + var isDpiCustom by mutableStateOf(true) + + // 主题模式选项 + val themeOptions = listOf( + context.getString(R.string.theme_follow_system), + context.getString(R.string.theme_light), + context.getString(R.string.theme_dark) + ) + + // 预设 DPI 选项 + val dpiPresets = mapOf( + context.getString(R.string.dpi_size_small) to 240, + context.getString(R.string.dpi_size_medium) to 320, + context.getString(R.string.dpi_size_large) to 420, + context.getString(R.string.dpi_size_extra_large) to 560 + ) +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt new file mode 100644 index 0000000..f383ec9 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/LocaleHelper.kt @@ -0,0 +1,154 @@ +package zako.zako.zako.zakoui.screen.moreSettings.util + +import android.annotation.SuppressLint +import android.annotation.TargetApi +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.res.Configuration +import android.net.Uri +import android.os.Build +import android.provider.Settings +import java.util.* + +object LocaleHelper { + + /** + * Check if should use system language settings (Android 13+) + */ + val useSystemLanguageSettings: Boolean + get() = Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + + /** + * Launch system app locale settings (Android 13+) + */ + fun launchSystemLanguageSettings(context: Context) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + val intent = Intent(Settings.ACTION_APP_LOCALE_SETTINGS).apply { + data = Uri.fromParts("package", context.packageName, null) + } + context.startActivity(intent) + } catch (_: Exception) { + // Fallback to app language settings if system settings not available + } + } + } + + /** + * Apply saved language setting to context (for Android < 13) + */ + fun applyLanguage(context: Context): Context { + // On Android 13+, language is handled by system + if (useSystemLanguageSettings) { + return context + } + + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val localeTag = prefs.getString("app_locale", "system") ?: "system" + + return if (localeTag == "system") { + context + } else { + val locale = parseLocaleTag(localeTag) + setLocale(context, locale) + } + } + + /** + * Set locale for context (Android < 13) + */ + @SuppressLint("ObsoleteSdkInt") + private fun setLocale(context: Context, locale: Locale): Context { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + updateResources(context, locale) + } else { + updateResourcesLegacy(context, locale) + } + } + + @SuppressLint("UseRequiresApi", "ObsoleteSdkInt") + @TargetApi(Build.VERSION_CODES.N) + private fun updateResources(context: Context, locale: Locale): Context { + val configuration = Configuration() + configuration.setLocale(locale) + configuration.setLayoutDirection(locale) + return context.createConfigurationContext(configuration) + } + + @Suppress("DEPRECATION") + @SuppressWarnings("deprecation") + private fun updateResourcesLegacy(context: Context, locale: Locale): Context { + Locale.setDefault(locale) + val resources = context.resources + val configuration = resources.configuration + configuration.locale = locale + configuration.setLayoutDirection(locale) + resources.updateConfiguration(configuration, resources.displayMetrics) + return context + } + + /** + * Parse locale tag to Locale object + */ + private fun parseLocaleTag(tag: String): Locale { + return try { + if (tag.contains("_")) { + val parts = tag.split("_") + Locale.Builder() + .setLanguage(parts[0]) + .setRegion(parts.getOrNull(1) ?: "") + .build() + } else { + Locale.Builder() + .setLanguage(tag) + .build() + } + } catch (_: Exception) { + Locale.getDefault() + } + } + + /** + * Restart activity to apply language change (Android < 13) + */ + fun restartActivity(context: Context) { + if (context is Activity && !useSystemLanguageSettings) { + context.recreate() + } + } + + /** + * Get current app locale + */ + @SuppressLint("ObsoleteSdkInt") + fun getCurrentAppLocale(context: Context): Locale? { + return if (useSystemLanguageSettings) { + // Android 13+ - get from system app locale settings + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + try { + val localeManager = context.getSystemService(Context.LOCALE_SERVICE) as? android.app.LocaleManager + val locales = localeManager?.applicationLocales + if (locales != null && !locales.isEmpty) { + locales.get(0) + } else { + null // System default + } + } catch (_: Exception) { + null // System default + } + } else { + null // System default + } + } else { + // Android < 13 - get from SharedPreferences + val prefs = context.getSharedPreferences("settings", Context.MODE_PRIVATE) + val localeTag = prefs.getString("app_locale", "system") ?: "system" + if (localeTag == "system") { + null // System default + } else { + parseLocaleTag(localeTag) + } + } + } +} \ No newline at end of file diff --git a/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt new file mode 100644 index 0000000..8824505 --- /dev/null +++ b/manager/app/src/main/java/zako/zako/zako/zakoui/screen/moreSettings/util/RestartActivityUtils.kt @@ -0,0 +1,27 @@ +package zako.zako.zako.zakoui.screen.moreSettings.util + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import com.sukisu.ultra.ui.MainActivity + +/** + * 刷新启动器图标 + */ +fun toggleLauncherIcon(context: Context, useAlt: Boolean) { + val pm = context.packageManager + val main = ComponentName(context, MainActivity::class.java.name) + val alias = ComponentName(context, "${MainActivity::class.java.name}Alias") + + pm.setComponentEnabledSetting( + if (useAlt) alias else main, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + + pm.setComponentEnabledSetting( + if (useAlt) main else alias, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) +} \ No newline at end of file diff --git a/manager/app/src/main/jniLibs/.gitignore b/manager/app/src/main/jniLibs/.gitignore index 05ffdc0..939b930 100644 --- a/manager/app/src/main/jniLibs/.gitignore +++ b/manager/app/src/main/jniLibs/.gitignore @@ -1,5 +1,8 @@ -libzakozako.so -libzakozakozako.so -libkpmmgr.so -libzako.so -libandroidx.graphics.path.so \ No newline at end of file +libksud.so +libkernelsu.so +libsusfsd.so +libuid_scanner.so +libzakosign.so +libandroidx.graphics.path.so +libmmrl-file-manager.so +libmmrl-kernelsu.so diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so b/manager/app/src/main/jniLibs/arm64-v8a/libksu_susfs.so new file mode 100644 index 0000000000000000000000000000000000000000..7ac5dcfa446a34924f9c51f4bc32f57cfb58c7d3 GIT binary patch literal 21496 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 literal 0 HcmV?d00001 diff --git a/manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so b/manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/arm64-v8a/libzakoboot.so rename to manager/app/src/main/jniLibs/arm64-v8a/libmagiskboot.so diff --git a/manager/app/src/main/jniLibs/armeabi-v7a/libzakoboot.so b/manager/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/armeabi-v7a/libzakoboot.so rename to manager/app/src/main/jniLibs/armeabi-v7a/libmagiskboot.so diff --git a/manager/app/src/main/jniLibs/x86_64/libzakoboot.so b/manager/app/src/main/jniLibs/x86_64/libmagiskboot.so similarity index 100% rename from manager/app/src/main/jniLibs/x86_64/libzakoboot.so rename to manager/app/src/main/jniLibs/x86_64/libmagiskboot.so diff --git a/manager/app/src/main/res/values-ar/strings.xml b/manager/app/src/main/res/values-ar/strings.xml index 9f3ce80..f1e2a35 100644 --- a/manager/app/src/main/res/values-ar/strings.xml +++ b/manager/app/src/main/res/values-ar/strings.xml @@ -63,7 +63,6 @@ إصدار KernelSU الحالي %s منخفض جدًا بحيث لا يعمل المدير بشكل صحيح. الرجاء الترقية إلى الإصدار %s أو أعلى! الغاء تحميل الإضافات بشكل افتراضي القيمة الافتراضية العامة لـ\"إلغاء تحميل الإضافات\" في ملفات تعريف التطبيقات. إذا تم تمكينه، إزالة جميع تعديلات الإضافات على النظام للتطبيقات التي لا تحتوي على مجموعة ملف تعريف. - تعطيل روابط kprobe سيسمح تمكين هذا الخيار لـKernelSU باستعادة أي ملفات معدلة بواسطة الإضافات لهذا التطبيق. المجال القواعد @@ -235,7 +234,6 @@ نوع الملف غير صحيح! الرجاء تحديد ملف .kpm. إلغاء التثبيت سيتم إلغاء تثبيت KPM التالية: %s - تعطيل روابط kprobe التي أنشأتها KernelSU، باستخدام الروابط الواردة بدلاً من ذلك، والتي تشبه طريقة الربط غير GKI غير GKI. استخدم إصبعين لتكبير الصورة، وأصبع واحد لسحبها لضبط الموضع إعادة @@ -309,9 +307,8 @@ يحتاج التطبيق إلى إعادة تشغيل لتطبيق الإعدادات الجديدة لإدارة شؤون الإعلام، ولا يؤثر على شريط حالة النظام أو التطبيقات الأخرى تم تعيين DPI إلى %1$d، فعلي بعد إعادة تشغيل التطبيق - لغة التطبيق - اتبع النظام - تم تغيير اللغة، إعادة التشغيل لتطبيق التغييرات + لغة التطبيق + اتبع النظام تعديل ظلام البطاقة رمز الخطأ diff --git a/manager/app/src/main/res/values-az/strings.xml b/manager/app/src/main/res/values-az/strings.xml index 66a4548..32086b2 100644 --- a/manager/app/src/main/res/values-az/strings.xml +++ b/manager/app/src/main/res/values-az/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Defolt olaraq modulları umount et 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. - Disable kprobe hooks 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. Domen Qaydalar @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-bs/strings.xml b/manager/app/src/main/res/values-bs/strings.xml index 18808f7..4477a43 100644 --- a/manager/app/src/main/res/values-bs/strings.xml +++ b/manager/app/src/main/res/values-bs/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! 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. - Disable kprobe hooks Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. Domena Pravila @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-da/strings.xml b/manager/app/src/main/res/values-da/strings.xml index 2c2d8b0..1c26353 100644 --- a/manager/app/src/main/res/values-da/strings.xml +++ b/manager/app/src/main/res/values-da/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Afmontere moduler som standard Den globale standard værdi for \"Afmonter moduler\" i App Profiler. Hvis aktiveret vil den fjerne alle modulers modifikationer til system applikationerne der ikke har en sat Profil. - Disable kprobe hooks Aktivering af denne indstilling vil tillade KernelSU at gendanne hvilken som helst modificeret filer af modulet for denne applikation. Domæne Regler @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-de/strings.xml b/manager/app/src/main/res/values-de/strings.xml index b27aea4..e149a4f 100644 --- a/manager/app/src/main/res/values-de/strings.xml +++ b/manager/app/src/main/res/values-de/strings.xml @@ -63,7 +63,6 @@ Die aktuelle KernelSU-Version %s ist zu alt für diese Manager-Version. Bitte auf Version %s oder höher aktualisieren! 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. - Kprobe-Hooks deaktivieren Wenn du diese Option aktivierst, kann KernelSU alle von den Modulen für diese App geänderten Dateien wiederherstellen. Domäne Regeln @@ -235,7 +234,6 @@ Falscher Dateityp! Bitte wählen Sie eine .kpm Datei. Deinstallieren Folgende KPM wird deinstalliert: %s - Deaktiviere kprobe Hooks die von KernelSU erstellt wurden und stattdessen inline Hooks verwenden, was der Nicht-GKI-Kernel-Hooking Methode ähnlich ist. Verwende zwei Finger um das Bild zu vergrößern und einen Finger um die Position anzupassen Rückzahlung @@ -309,9 +307,8 @@ Die Anwendung muss neu gestartet werden, um die neuen DPI-Einstellungen zu übernehmen, hat keine Auswirkungen auf die System-Statusleiste oder andere Anwendungen DPI wurde auf %1$dgesetzt, wirksam nach dem Neustart der Anwendung - App Sprache - Folge Systemeinstellung - Sprache geändert, Neustart um Änderungen zu übernehmen + App Sprache + Folge Systemeinstellung Kartenfinsternis Anpassung fehlercode diff --git a/manager/app/src/main/res/values-es/strings.xml b/manager/app/src/main/res/values-es/strings.xml index 967e6fc..1593c6b 100644 --- a/manager/app/src/main/res/values-es/strings.xml +++ b/manager/app/src/main/res/values-es/strings.xml @@ -63,7 +63,6 @@ La versión %s actual de KernelSU es demasiado baja para que el gestor funcione correctamente. Por favor, ¡actualice a la versión %s o superior! 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. - Desactivar kprobe hooks Activar esta opción permitirá a KernelSU restaurar cualquier archivo modificado por los módulos para esta aplicación. Dominio Reglas @@ -233,7 +232,6 @@ ¡Tipo de archivo incorrecto! Por favor seleccione el archivo .kpm. Desinstalar El siguiente KPM será desinstalado: %s - Deshabilita los ganchos kprobe creados por KernelSU, usando ganchos en línea en su lugar, que es similar al método de enganche del núcleo no GKI. Usa dos dedos para acercar la imagen, y un dedo para arrastrarla para ajustar la posición Reaprovisionamiento @@ -307,9 +305,8 @@ La aplicación necesita reiniciarse para aplicar la nueva configuración DPI, no afecta a la barra de estado del sistema u otras aplicaciones DPI ha sido establecido a %1$d, efectivo después de reiniciar la aplicación - Idioma de la aplicación - Seguir sistema - Idioma cambiado, reiniciando para aplicar cambios + Idioma de la aplicación + Seguir sistema Ajuste de oscuridad de tarjeta código de error diff --git a/manager/app/src/main/res/values-et/strings.xml b/manager/app/src/main/res/values-et/strings.xml index f3bc289..3296fac 100644 --- a/manager/app/src/main/res/values-et/strings.xml +++ b/manager/app/src/main/res/values-et/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Haagi moodulid vaikimisi lahti Globaalne vaikeväärtus \"Lahtihaagitud moodulitele\" rakenduseprofiilis. Lubamisel eemaldab see kõik moodulite süsteemimuudatused rakendustele, millel ei ole profiili määratud. - Disable kprobe hooks Selle valiku lubamine lubab KernelSU-l taastada selle rakenduse moodulite poolt mistahes muudetud faile. Domeen Reeglid @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fa/strings.xml b/manager/app/src/main/res/values-fa/strings.xml index 760f4b9..bff3486 100644 --- a/manager/app/src/main/res/values-fa/strings.xml +++ b/manager/app/src/main/res/values-fa/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s 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. - Disable kprobe hooks Enabling this option will allow KernelSU to restore any modified files by the modules for this app. Domain Rules @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fil/strings.xml b/manager/app/src/main/res/values-fil/strings.xml index a762616..9df07b4 100644 --- a/manager/app/src/main/res/values-fil/strings.xml +++ b/manager/app/src/main/res/values-fil/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount modules by default 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. - Disable kprobe hooks Ang pagpapagana sa opsyong ito ay magbibigay-daan sa KernelSU na ibalik ang anumang binagong file ng mga modyul para sa aplikasyon na ito. Domain Mga Tuntunin @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-fr/strings.xml b/manager/app/src/main/res/values-fr/strings.xml index fb87a32..a0f027b 100644 --- a/manager/app/src/main/res/values-fr/strings.xml +++ b/manager/app/src/main/res/values-fr/strings.xml @@ -63,7 +63,6 @@ La version actuelle de KernelSU (%s) est trop ancienne pour que le gestionnaire fonctionne correctement. Veuillez passer à la version %s ou à une version supérieure ! Démonter les modules par défaut 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. - Désactiver les crochets kprobe L\'activation de cette option permettra à KernelSU de restaurer tous les fichiers modifiés par les modules pour cette application. Domaine Règles @@ -235,7 +234,6 @@ Type de fichier incorrect ! Veuillez sélectionner un fichier .kpm. Désinstaller Le KPM suivant sera désinstallé : %s - Désactivez les crochets kprobe créés par KernelSU, en utilisant des crochets en ligne à la place, ce qui est similaire à la méthode de crochet du noyau non-GKI. Utilisez deux doigts pour zoomer l\'image, et un doigt pour le faire glisser pour ajuster la position Remise à disposition @@ -309,9 +307,8 @@ L\'application doit être redémarrée pour appliquer les nouveaux paramètres de DPI, n\'affecte pas la barre d\'état du système ou d\'autres applications Le DPI a été réglé sur %1$d, effectif après le redémarrage de l\'application - Langue de l\'application - Suivre le paramètre système - Langue modifiée, redémarrage pour appliquer les modifications + Langue de l\'application + Suivre le paramètre système Ajustement de l\'obscurité de la carte code d\'erreur diff --git a/manager/app/src/main/res/values-hi/strings.xml b/manager/app/src/main/res/values-hi/strings.xml index 8803d03..e07439b 100644 --- a/manager/app/src/main/res/values-hi/strings.xml +++ b/manager/app/src/main/res/values-hi/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! डिफ़ॉल्ट रूप से मॉड्यूल अनमाउन्ट करें ऐप प्रोफाइल में \"अनमाउंट मॉड्यूल\" के लिए ग्लोबल डिफ़ॉल्ट वैल्यू। यदि चालू किया गया है, तो यह एप्लीकेशंस के लिऐ सिस्टम के सभी मॉड्यूल मोडिफिकेशन को हटा देगा जिनकी प्रोफ़ाइल सेट नहीं है। - Disable kprobe hooks इस विकल्प को चालू करने से KernelSU को इस एप्लिकेशन के लिए मॉड्यूल द्वारा किसी भी मोडिफाइड फ़ाइल को रिस्टोर करें। डोमेन नियम @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-hr/strings.xml b/manager/app/src/main/res/values-hr/strings.xml index 779dda8..1700ada 100644 --- a/manager/app/src/main/res/values-hr/strings.xml +++ b/manager/app/src/main/res/values-hr/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! 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. - Disable kprobe hooks Uključivanjem ove opcije omogućit će KernelSU-u da vrati sve izmjenute datoteke od strane modula za ovu aplikaciju. Domena Pravila @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-hu/strings.xml b/manager/app/src/main/res/values-hu/strings.xml index ba073b0..21a465a 100644 --- a/manager/app/src/main/res/values-hu/strings.xml +++ b/manager/app/src/main/res/values-hu/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Modulok leválasztása alapértelmezetten 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. - Disable kprobe hooks 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 @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-idn/strings.xml b/manager/app/src/main/res/values-idn/strings.xml new file mode 100644 index 0000000..6c9a5ee --- /dev/null +++ b/manager/app/src/main/res/values-idn/strings.xml @@ -0,0 +1,541 @@ + + + Beranda + Tidak Terpasang + Klik untuk Memasang + Berfungsi + Versi: %s + Tidak Didukung + Driver KernelSU tidak terdeteksi di kernel Anda. Mungkin Anda menggunakan kernel yang salah. + Versi Kernel + Versi SuSFS + Versi Manajer + Status SELinux + Dinonaktifkan + Ditegakkan + Permisi + Tidak Diketahui + Superuser + Gagal mengaktifkan modul: %s + Gagal menonaktifkan modul: %s + Tidak ada modul terpasang + Modul + Urutkan (Aksi Terlebih Dahulu) + Urutkan (Aktif Terlebih Dahulu) + Copot Pemasangan + Pasang + Pasang + Muat Ulang + Pengaturan + Muat Ulang Lunak + Muat Ulang ke Recovery + Muat Ulang ke Bootloader + Muat Ulang ke Mode Download + Muat Ulang ke Mode EDL + Tentang + Apakah Anda yakin ingin mencopot pemasangan modul %s? + %s telah dicopot + Gagal mencopot pemasangan: %s + Versi + Penulis + Segarkan + Tampilkan Aplikasi Sistem + Sembunyikan Aplikasi Sistem + Kirim Log + Mode Aman + Muat ulang untuk menerapkan + Modul tidak tersedia karena konflik dengan Magisk! + Pelajari tentang KernelSU + https://kernelsu.org/guide/what-is-kernelsu.html + Pelajari cara memasang KernelSU dan menggunakan modul + Dukung Kami + KernelSU bersifat gratis dan open source, sekarang dan selamanya. Namun, Anda dapat menunjukkan dukungan Anda dengan melakukan donasi. + Gabung ke saluran %2$s kami]]> + Profil Aplikasi + Bawaan + Templat + Khusus + Nama Profil + Grup + Kemampuan + Konteks SELinux + Lepas Kait Modul + Gagal memperbarui profil aplikasi untuk %s + Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manajer dengan benar. Harap perbarui ke versi %s atau yang lebih tinggi! + Lepas kait modul secara bawaan + Nilai bawaan global untuk \"Lepas Kait Modul\" dalam profil aplikasi. Jika diaktifkan, ini akan menghapus semua perubahan sistem yang dibuat oleh modul untuk aplikasi tanpa profil yang ditetapkan. + Mengaktifkan opsi ini akan memungkinkan KernelSU untuk memulihkan file yang diubah oleh modul untuk aplikasi ini. + Domain + Aturan + Perbarui + Mengunduh modul: %s + Memulai pengunduhan: %s + Versi baru %s tersedia, klik untuk memperbarui. + Jalankan + Paksa Hentikan + Jalankan Ulang + Gagal memperbarui aturan SELinux untuk %s + Catatan Perubahan + Templat Profil Aplikasi + Kelola templat profil aplikasi lokal dan daring + Buat Templat + Edit Templat + ID + ID Templat Tidak Valid + Nama + Deskripsi + Simpan + Hapus + Lihat Templat + Hanya Baca + ID Templat sudah ada! + Impor/Ekspor + Impor dari Papan Klip + Ekspor ke Papan Klip + Tidak ditemukan templat lokal untuk diekspor! + Berhasil diimpor + Sinkronkan Templat Daring + Gagal menyimpan templat + Papan klip kosong! + Gagal memuat catatan perubahan: %s + Periksa Pembaruan + Secara otomatis memeriksa pembaruan saat membuka aplikasi + Gagal memberikan hak akses root! + Aksi + Tutup + Aktifkan Debug WebView + Dapat digunakan untuk mendebug WebUI. Harap aktifkan hanya jika diperlukan. + Pemasangan Langsung (Disarankan) + Pilih Gambar untuk Dipatch + Pasang ke Slot Tidak Aktif (Setelah OTA) + Perangkat Anda akan **DIPAKSA** untuk boot ke slot tidak aktif saat ini setelah reboot! +Gunakan opsi ini hanya setelah OTA selesai. +Lanjutkan? + Lanjut + Disarankan gambar partisi %1$s + (tidak stabil) + Pilih KMI + Copot Pemasangan + Copot Pemasangan Sementara + Copot Pemasangan Permanen + Pulihkan Gambar Bawaan + Copot pemasangan KernelSU secara sementara, kembalikan ke keadaan awal setelah reboot berikutnya. + Copot pemasangan KernelSU secara lengkap dan permanen (Root dan semua modul). + Pulihkan gambar bawaan pabrik (jika cadangan tersedia), biasanya digunakan sebelum OTA; jika ingin mencopot KernelSU, gunakan \"Copot Pemasangan Permanen\". + Mem-flash + Flash Berhasil + Flash Gagal + LKM Terpilih: %s + Simpan Log + Log Disimpan + Mode SuS SU: + + Konfirmasi pemasangan modul %1$s? + modul tidak dikenal + + Konfirmasi Pemulihan Modul + Operasi ini akan menimpa semua modul yang ada. Lanjutkan? + Konfirmasi + Batal + + Pencadangan Berhasil (tar.gz) + Gagal membuat cadangan: %1$s + cadangan modul + pulihkan modul + + Modul berhasil dipulihkan, perlu reboot + Gagal memulihkan: %1$s + Muat Ulang Sekarang + Kesalahan Tidak Diketahui + + Gagal mengeksekusi perintah: %1$s + + Pencadangan daftar izin berhasil + Gagal membuat cadangan daftar izin: %1$s + Konfirmasi Pemulihan Daftar Izin + Operasi ini akan menimpa daftar izin saat ini. Lanjutkan? + Daftar izin berhasil dipulihkan + Gagal memulihkan daftar izin: %1$s + Cadangkan Daftar Izin + Pulihkan Daftar Izin + Latar Belakang Aplikasi Khusus + Pilih gambar sebagai latar belakang + Transparansi Panel Navigasi + Versi Android + Model Perangkat + Pemberian hak superuser untuk %s tidak diizinkan + Nonaktifkan Kompatibilitas su + Sementara mencegah aplikasi mana pun mendapatkan hak root melalui perintah su (proses root yang ada tidak akan terpengaruh). + Apakah Anda yakin ingin memasang %1$d modul berikut? +%2$s + Pengaturan Lainnya + SELinux + Diaktifkan + Dinonaktifkan + Mode Sederhana + Menyembunyikan kartu yang tidak perlu saat diaktifkan + Sembunyikan Versi Kernel + Menyembunyikan versi kernel + Sembunyikan Informasi Lainnya + Menyembunyikan titik merah yang menunjukkan jumlah superuser, modul, dan modul KPM di halaman navigasi bawah + Sembunyikan Status SuSFS + Menyembunyikan informasi status SuSFS di halaman beranda + Sembunyikan Kartu Tautan + Menyembunyikan informasi di kartu tautan di halaman beranda + Sembunyikan Baris Tag Modul + Menyembunyikan label nama folder dan ukuran di kartu modul + Tema + Ikuti Sistem + Terang + Gelap + Hook Manual + Warna Dinamis + Warna dinamis menggunakan tema sistem + Pilih Warna Tema + Biru + Hijau + Ungu + Oranye + Merah Muda + Abu-abu + Kuning + Pasang Anykernel3 + Flash file kernel AnyKernel3 + Diperlukan hak akses root + Pembersihan Selesai + Muat ulang sekarang? + Ya + Tidak + Gagal memuat ulang + KPM + Saat ini tidak ada modul kernel yang terpasang + Versi + Penulis + Copot Pemasangan + Berhasil dicopot + Gagal mencopot + Berhasil memuat modul kpm + Gagal memuat modul kpm + Parameter + Jalankan + Versi KPM + Tutup + Fitur modul kernel berikut dikembangkan oleh KernelPatch dan dimodifikasi untuk menyertakan fitur modul kernel SukiSU Ultra + SukiSU Ultra menantikan + Berhasil + Gagal + Ke depannya, SukiSU Ultra akan menjadi cabang KSU yang relatif independen, tetapi kami tetap berterima kasih kepada KernelSU resmi, MKSU, dan lainnya atas kontribusi mereka! + Tidak Didukung + Didukung + Kernel Belum Di-patch + Kernel Belum Diaktifkan + Pengaturan Khusus + Pemasangan KPM + Muat + Tanamkan + Silakan pilih: %1$s mode pemasangan modul +Muat: Secara sementara memuat modul +Tanamkan: Secara permanen memasang ke sistem + Gagal memeriksa keberadaan file modul + Warna Tema + Jenis file tidak valid! Harap pilih file .kpm. + Copot Pemasangan + Akan mencopot KPM berikut: %s + Gunakan dua jari untuk memperbesar gambar dan satu jari untuk menyeret, untuk menyesuaikan posisi + Provisi Ulang + + Flash Selesai + + Menyiapkan… + Membersihkan file… + Menyalin file… + Mengekstrak alat flash… + Menambal skrip flash… + Mem-flash kernel… + Flash Selesai + + Pilih Slot untuk Flash + Silakan pilih slot target untuk flashing boot + Slot A + Slot B + Slot Terpilih: %1$s + Mendapatkan slot asli + Mengatur slot target + Mengembalikan slot bawaan + Slot sistem bawaan saat ini: %1$s + + Gagal menyalin + Kesalahan Tidak Diketahui + Flash Gagal + + Pemulihan/Pemasangan LKM + Flash AnyKernel3 + Versi Kernel: %1$s + Alat patch yang digunakan: %1$s + Konfigurasi + Pengaturan Aplikasi + Alat + + Aplikasi tidak ditemukan + SELinux diaktifkan + SELinux dinonaktifkan + Gagal mengubah status SELinux + Pengaturan Lanjutan + Sesuaikan Bilah Alat + Kembali + SuSFS diaktifkan + SuSFS dinonaktifkan + Latar belakang berhasil diatur + Latar belakang khusus dihapus + Ikon Alternatif + Ubah ikon peluncur menjadi ikon KernelSU. + Ikon diubah + + Sembunyikan Fungsi KPM + Menyembunyikan informasi dan fungsi KPM di layar utama dan panel bawah + + Pilih Mesin WebUI untuk Digunakan + Pilih Otomatis + Paksa Gunakan WebUI X + Paksa Gunakan KSU WebUI + Sisipkan Eruda ke WebUI X + Sisipkan konsol debug ke WebUI X untuk memudahkan debugging. Memerlukan debugging web diaktifkan. + + DPI yang Diterapkan + Sesuaikan kepadatan layar hanya untuk aplikasi saat ini + Kecil + Sedang + Besar + Sangat Besar + Khusus + Menerapkan Pengaturan DPI + Konfirmasi Perubahan DPI + Apakah Anda yakin ingin mengubah DPI aplikasi dari %1$d menjadi %2$d? + Aplikasi perlu dijalankan ulang agar pengaturan DPI baru diterapkan; ini tidak akan mempengaruhi bilah status sistem atau aplikasi lainnya + DPI diatur ke %1$d, akan diterapkan setelah aplikasi dijalankan ulang + + Bahasa Aplikasi + Ikuti Sistem + Pengaturan Pencahayaan Kartu + + kode kesalahan + Silakan periksa log + Memasang modul %1$d/%2$d + Gagal memasang %d modul baru + Gagal mengunduh modul + Mem-flash Kernel + + Semua + Root + Khusus + Bawaan + + Nama Naik + Nama Turun + Waktu Pemasangan (Baru) + Waktu Pemasangan (Lama) + Ukuran Turun + Ukuran Naik + Frekuensi Penggunaan + + Tidak ada aplikasi dalam kategori ini + + Tolak Hak Akses + Berikan Hak Akses + Lepas Kaitan Modul + Nonaktifkan Lepas Kaitan Modul + Perluas Menu + Ciutkan Menu + Ke Atas + Ke Bawah + Terpilih + Pilih + + Opsi Menu + Urutkan Berdasarkan + Pilih Jenis Aplikasi + + Konfigurasi SuSFS + Deskripsi Konfigurasi + Fitur ini memungkinkan Anda untuk mengonfigurasi spoofing nilai uname dan waktu build SuSFS. Masukkan nilai yang diinginkan dan klik \"Terapkan\" agar berlaku. + Nilai Uname + Silakan masukkan nilai uname khusus + Spoof Waktu Build + Silakan masukkan nilai spoof waktu build + Nilai Saat Ini: %s + Waktu Build Saat Ini: %s + Atur Ulang ke Bawaan + Terapkan + + Konfirmasi Atur Ulang + + Gagal menemukan file ksu_susfs + Gagal mengeksekusi perintah SuSFS + Kesalahan eksekusi perintah SuSFS: %s + Nilai uname dan waktu build SuSFS berhasil diatur: %s, %s + + Konfigurasi SuSFS + + Mulai Otomatis + Secara otomatis menerapkan semua konfigurasi non-bawaan saat reboot + Perlu menambahkan konfigurasi untuk mengaktifkan + Gagal mengaktifkan mulai otomatis + Gagal menonaktifkan mulai otomatis + Kesalahan konfigurasi mulai otomatis: %s + Tidak ada konfigurasi yang tersedia untuk mulai otomatis + + Pengaturan Dasar + Jalur SUS + Kaitan SUS + Coba Lepas Kait + Pengaturan Jalur + Status Fitur Diaktifkan + + Tambah Jalur SUS + Tambah Kaitan SUS + Tambah Coba Lepas Kait + Jalur SUS berhasil ditambahkan + Kesalahan: Jalur tidak ditemukan + Jalur + Jalur Kaitan + misalnya: /system/addon.d + Tidak ada jalur SUS yang dikonfigurasi + Tidak ada kaitan SUS yang dikonfigurasi + Tidak ada coba lepas kait yang dikonfigurasi + + Mode Lepas Kait + Lepas Kait Normal (0) + Lepas Kait Terpisah (1) + Normal + Terpisah + Mode: %1$s (%2$s) + Jalur coba lepas kait berhasil ditambahkan: %s + Berhasil menyimpan jalur coba lepas kait: %s + + + Atur Ulang Jalur SUS + Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Kaitan SUS + Ini akan menghapus semua konfigurasi kaitan SUS. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Coba Lepas Kait + Ini akan menghapus semua konfigurasi coba lepas kait. Apakah Anda yakin ingin melanjutkan? + Atur Ulang Pengaturan Jalur + + Jalur Data Android + Jalur Kartu SD + Atur Jalur Data Android + Atur Jalur Kartu SD + + Menampilkan status saat ini dari fitur SuSFS yang diaktifkan + Informasi status fitur tidak ditemukan + Diaktifkan + Dinonaktifkan + + Dukungan Jalur SUS + Dukungan Kaitan SUS + Dukungan Coba Lepas Kait + Dukungan Spoof Uname + Spoof Cmdline/Bootconfig + Dukungan Open Redirect + Dukungan Logging + Kaitan Bawaan Otomatis + Kaitan Bind Otomatis + Coba Lepas Kaitan Bind Otomatis + Sembunyikan Simbol KSU SUSFS + Dukungan SUS Kstat + Fitur Toggle Mode SUS SU + + Fitur SuSFS yang Dapat Dikonfigurasi + Aktifkan Log SuSFS + Aktifkan atau nonaktifkan logging untuk SuSFS + Pengaturan Logging SuSFS + Mengaktifkan Logging SuSFS + Menonaktifkan Logging SuSFS + Perbarui JSON + URL Perbarui JSON disalin ke papan klip + + Tampilkan Informasi Modul Lebih Banyak + Tampilkan informasi modul tambahan seperti URL perbarui JSON + Lokasi Eksekusi + Lokasi Eksekusi Saat Ini: %s + Layanan + Post-FS-Data + Jalankan setelah layanan sistem dimulai + Jalankan setelah sistem file dikaitkan tetapi sebelum sistem sepenuhnya dinyalakan. Dapat menyebabkan bootloop + Informasi Slot + Lihat informasi slot boot saat ini dan salin nilainya + Slot Aktif Saat Ini: %s + Uname: %s + Waktu Build: %s + Saat Ini + Gunakan Uname + Gunakan Waktu Build + Gagal mendapatkan informasi slot + + Modul mulai otomatis SuSFS diaktifkan, jalur modul: %s + Modul mulai otomatis SuSFS dinonaktifkan + + Konfigurasi Kstat + Konfigurasi Kstat statis ditambahkan: %1$s + Konfigurasi Kstat dihapus: %1$s + Jalur Kstat ditambahkan: %1$s + Jalur Kstat dihapus: %1$s + Kstat diperbarui: %1$s + Klon Lengkap Kstat diperbarui: %1$s + Tambahkan Konfigurasi Kstat Statis + Jalur File/Direktori + Petunjuk: Anda dapat menggunakan \"default\" untuk menggunakan nilai asli + Tambah Jalur Kstat + Tambah + Atur Ulang Konfigurasi Kstat + Apakah Anda yakin ingin membersihkan semua konfigurasi Kstat? Tindakan ini tidak dapat dibatalkan. + Deskripsi Konfigurasi Kstat + • add_sus_kstat_statically: Informasi file/direktori statis + • add_sus_kstat: Tambahkan jalur sebelum bind mount, menjaga informasi asli + • update_sus_kstat: Perbarui ino target, membiarkan ukuran dan blok tidak berubah + • update_sus_kstat_full_clone: Perbarui hanya ino, membiarkan nilai asli lainnya + Konfigurasi Kstat Statis + Manajemen Jalur Kstat + Belum ada konfigurasi Kstat, klik tombol di atas untuk menambahkan + + Kontrol Penyembunyian Kaitan SUS + Kontrol perilaku penyembunyian kaitan SUS untuk proses + Sembunyikan Kaitan SUS untuk Semua Proses + Jika diaktifkan, kaitan SUS akan disembunyikan dari semua proses, termasuk proses KSU + Jika dinonaktifkan, kaitan SUS akan disembunyikan hanya dari proses non-KSU; proses KSU akan dapat melihat kaitan + Mengaktifkan penyembunyian kaitan SUS untuk semua proses + Menonaktifkan penyembunyian kaitan SUS untuk semua proses + Disarankan untuk mengatur ke nonaktif setelah layar terbuka atau pada tahap service.sh atau boot-completed.sh, karena ini seharusnya memperbaiki masalah dengan beberapa aplikasi root yang bergantung pada kaitan yang dibuat oleh proses KSU + Pengaturan Saat Ini: %s + Sembunyikan untuk Semua Proses + Sembunyikan Hanya untuk Proses Non-KSU + Mode Versi Kernel Sederhana + Aktifkan atau nonaktifkan tampilan versi kernel SukiSU sederhana + Jalur Data Android diatur ke: %s + Jalur Kartu SD diatur ke: %s + Pengaturan jalur mungkin tidak sepenuhnya berhasil, tetapi jalur SUS akan tetap ditambahkan + + Cadangan + Buat cadangan semua konfigurasi SuSFS. File cadangan akan menyertakan semua pengaturan, jalur, dan konfigurasi. + Buat Cadangan + Berhasil membuat cadangan: %s + Gagal membuat cadangan: %s + File cadangan tidak ditemukan + Format file cadangan tidak valid + Versi cadangan tidak cocok, tetapi akan dicoba untuk dipulihkan + Pulihkan + Pulihkan konfigurasi SuSFS dari file cadangan. Ini akan menimpa semua pengaturan saat ini. + Pilih File Cadangan + Konfigurasi berhasil dipulihkan dari cadangan yang dibuat %s pada perangkat: %s + Gagal memulihkan: %s + Konfirmasi Pemulihan + Ini akan menimpa semua konfigurasi SuSFS saat ini. Apakah Anda yakin ingin melanjutkan? + Pulihkan + Tanggal Cadangan: %s + Perangkat: %s + Versi: %s + Status Terkunci + Timpa properti status bootloader dalam layanan late_start + Bersihkan Sisa-sisa + Bersihkan file dan direktori sisa dari berbagai modul dan alat (dapat menyebabkan penghapusan yang tidak disengaja, kehilangan data, dan gagal boot, gunakan dengan hati-hati) + diff --git a/manager/app/src/main/res/values-in/strings.xml b/manager/app/src/main/res/values-in/strings.xml index 87b07b0..d4ba145 100644 --- a/manager/app/src/main/res/values-in/strings.xml +++ b/manager/app/src/main/res/values-in/strings.xml @@ -63,7 +63,6 @@ Versi KernelSU saat ini %s terlalu rendah untuk menjalankan manager dengan baik. Harap tingkatkan ke versi %s atau yang lebih tinggi! Melepas Modul secara bawaan Menggunakan \"Umount Modul\" secara universal pada Profil Aplikasi. Jika diaktifkan, akan menghapus semua modifikasi sistem untuk aplikasi yang tidak memiliki set profil. - Nonaktifkan kprobe hooks Aktifkan opsi ini agar KernelSU dapat memulihkan kembali berkas termodifikasi oleh modul pada aplikasi ini. Domain Aturan @@ -112,6 +111,8 @@ \nHANYA gunakan setelah proses OTA selesai. \nLanjutkan? Selanjutnya + Gunakan berkas LKM lokal + Hanya berkas .ko yang didukung %1$s image partisi terekomendasi (tidak stabil) Pilih KMI @@ -166,6 +167,13 @@ Memberikan hak superuser kepada %s tidak diizinkan Nonaktifkan kompatibilitas SU Nonaktifkan sementara kemampuan aplikasi untuk mendapatkan hak akses root melalui perintah ⁠su (proses root yang sedang berjalan tidak akan terpengaruh) + Nonaktifkan pelepasan (unmount) kernel + Nonaktifkan perilaku unmount pada level kernel yang digunakan oleh KernelSU. + Aktifkan keamanan yang ditingkatkan + Aktifkan kebijakan keamanan yang lebih ketat. + Bawaan + Aktifkan sementara + Aktifkan secara permanen Apakah Anda yakin ingin menginstal %1$d modul berikut?\n\n%2$s Setelan lainnya Selinux @@ -240,7 +248,6 @@ Format file tidak sesuai. Silakan pilih file dengan format .kpm. Menghapus instalan KPM berikut akan diuninstall: %s - Nonaktifkan kprobe hooks yang dibuat oleh KernelSU, gunakan inline hooks sebagai gantinya (metode ini mirip dengan hooking kernel non-GKI). Gunakan dua jari untuk memperbesar gambar, dan satu jari untuk menggeser mengatur posisi Reprovisi @@ -314,9 +321,8 @@ Aplikasi membutuhkan restar untuk menerapkan opsi DPI ini, perubahan ini tidak mengganggu DPI sistem DPI telah di rubah ke %1$d, efektif setelah aplikasi di restar - Bahasa Aplikasi - Mengikuti sistem - Bahasa dirubah, mulai ulang aplikasi untuk menerapkan + Bahasa Aplikasi + Mengikuti sistem Penyesuaian Kegelapan Kartu Kode error @@ -413,8 +419,6 @@ Jalur coba umount berhasil ditambahkan: %s Jalur coba umount berhasil disimpan: %s - Konfirmasi Jalankan Coba Umount - Ini akan segera mengeksekusi semua operasi umount yang dikonfigurasi. Apakah Anda yakin ingin melanjutkan? Setel Ulang Jalur SUS Ini akan menghapus semua konfigurasi jalur SUS. Apakah Anda yakin ingin melanjutkan? @@ -445,7 +449,6 @@ Pemasangan Bind Otomatis Coba Umount Bind Mount Otomatis Sembunyikan Simbol KSU SUSFS - Dukungan Pemasangan Ajaib Dukungan SUS Kstat Fungsi pengalihan mode SUS SU @@ -513,7 +516,6 @@ Pengaturan saat ini: %s Sembunyikan untuk semua proses Sembunyikan hanya untuk proses non-KSU - Jalankan Mode Ringkas Versi Kernel Aktifkan atau nonaktifkan mode bersih yang ditampilkan oleh versi kernel SukiSU Jalur Data Android telah diatur ke: %s @@ -564,6 +566,8 @@ Lainnya Aplikasi Tambahkan Jalur Aplikasi + Versi pustaka SuSFS tidak cocok, kernel: %1$s vs manajer: %2$s. Disarankan untuk memperbarui kernel atau manajer + Peringatan Cari Aplikasi %1$d aplikasi dipilih %1$d aplikasi sudah ditambahkan @@ -607,6 +611,14 @@ Jalur Loop SUS Konfigurasi Jalur Loop Jalur loop ditandai ulang sebagai SUS_PATH pada setiap startup aplikasi pengguna non-root atau layanan terisolasi. Ini membantu mengatasi masalah di mana jalur yang ditambahkan mungkin memiliki status inode direset atau inode dibuat ulang di kernel. + Palsukan log AVC + Palsukan log AVC telah diaktifkan + Palsukan log AVC telah dinonaktifkan + Dinonaktifkan: Nonaktifkan pemalsuan sus tcontext dari \'su\' yang ditampilkan di avc log di kernel\n +Diaktifkan: Aktifkan pemalsuan sus tcontext dari \'su\' dengan \'kernel\' yang ditampilkan di avc log in kernel + Catatan Penting:\n +- Secara default pada kernel nilai ini disetel ke \'0\'\n +- Mengaktifkan ini terkadang membuat pengembang lebih sulit mengidentifikasi penyebab saat melakukan debugging terkait izin atau masalah SELinux, sehingga disarankan agar pengguna menonaktifkannya saat sedang melakukan debugging Tervalidasi Tanda tangan modul tervalidasi @@ -615,4 +627,77 @@ Penerbit tidak dikenal Modul yang tidak ditandatangani mungkin tidak lengkap. Untuk melindungi perangkat Anda, pemasangan modul ini diblokir. Modul yang tidak ditandatangani mungkin tidak lengkap. Apakah Anda ingin mengizinkan modul berikut dari penerbit tidak dikenal untuk dipasang di perangkat ini? + Jenis hook + + Patch KPM + Untuk menambahkan fitur KPM tambahan + Patch KPM + Terapkan patch KPM ke image kernel sebelum melakukan flashing + Batalkan Patch KPM + Batalkan patch KPM yang telah diterapkan sebelumnya + Patch KPM aktif + Pembatalan patch KPM diaktifkan + Mode Patch KPM + Mode Pembatalan Patch KPM + + Sedang menyiapkan Alat KPM + Menerapkan patch KPM + Membatalkan patch KPM + Menemukan berkas Image: %s + KPM berhasil diterapkan + Patch KPM berhasil dibatalkan + File berhasil direpack + + Gagal mengekstrak berkas zip + Berkas Image tidak ditemukan + Patch KPM gagal + Pembatalan patch KPM gagal + Operasi patch KPM gagal: %s + + Ikuti kernel + Gunakan kernel apa adanya tanpa perubahan dari KPM + + Daftar aplikasi pemindaian pada mode pengguna + Mengaktifkan opsi ini akan menggunakan pemindaian mode pengguna untuk daftar aplikasi, sehingga meningkatkan kestabilan. (Jika Anda mengalami masalah seperti hang saat kernel memindai daftar aplikasi, Anda dapat mencoba mengaktifkan opsi ini.) + Pemindaian Aplikasi Multi-Pengguna + Ketika diaktifkan, fitur ini akan memindai aplikasi untuk semua pengguna, termasuk profil kerja + Gagal mengatur, silakan periksa perizinan + Gagal mengatur: %s + Bersihkan Lingkungan Runtime + Bersihkan berkas runtime dan hentikan layanan pemindai + Apakah Anda yakin ingin membersihkan lingkungan runtime? Tindakan ini akan menghentikan layanan pemindai dan menghapus berkas yang terkait. + Lingkungan runtime berhasil dibersihkan + Gagal membersihkan lingkungan runtime + + Konfirmasi Instalasi + Konfirmasi Instalasi (Berkas %d) + Instal + Modul + Kernel + Tidak diketahui + Kernel tidak diketahui + Berkas tidak diketahui + Versi + Pembuat + Deskripsi + Perangkat yang didukung + + Peta SUS + Jalur Pustaka + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Tambahkan Peta SUS + Sunting Peta SUS + Peta SUS berhasil ditambahkan: %1$s + Peta SUS telah dihapus: %1$s + Peta SUS telah diperbarui: %1$s -> %2$s + Tidak ada peta SUS yang dikonfigurasi + Atur ulang Peta SUS + Tindakan ini akan menghapus semua peta SUS yang telah dikonfigurasi. Tindakan ini tidak dapat dibatalkan. + Penyembunyian Peta Memori + Sembunyikan berkas nyata yang di-mmapped dari berbagai peta di /proc/self/ + + Cari + Bersihkan Log + Apakah Anda yakin ingin mengosongkan berkas log yang dipilih? Tindakan ini tidak dapat dibatalkan. + diff --git a/manager/app/src/main/res/values-it/strings.xml b/manager/app/src/main/res/values-it/strings.xml index d9a9d6f..c664395 100644 --- a/manager/app/src/main/res/values-it/strings.xml +++ b/manager/app/src/main/res/values-it/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Scollega moduli da default 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. - Disable kprobe hooks Attivando questa opzione permetterai a KernelSU di ripristinare ogni file modificato dai moduli per questa app. Dominio Regole @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ja/strings.xml b/manager/app/src/main/res/values-ja/strings.xml index 6a9c511..ff9e96e 100644 --- a/manager/app/src/main/res/values-ja/strings.xml +++ b/manager/app/src/main/res/values-ja/strings.xml @@ -63,7 +63,6 @@ 現在の KernelSU のバージョン「%s」は低すぎるため、マネージャーは正常に動作しません。バージョン「%s」以上に更新してください! デフォルトでモジュールのマウントを解除する アプリプロファイルの「モジュールのアンマウント」の共通となるデフォルト値です。 有効にすると、プロファイルセットを持たないアプリのシステムに対するすべてのモジュールの変更が削除されます。 - kprobe フックを無効化 このオプションを有効にすると、KernelSU はこのアプリのモジュールによって変更されたファイルを復元できるようになります。 ドメイン ルール @@ -240,7 +239,6 @@ ファイルの種類が間違っています!.kpm ファイルを選択してください。 アンインストール 次の KPM がアンインストールされます: %s - KernelSU によって作成された kprobe フックを無効化して、代替となるインラインフックを使用します。これは、非 GKI カーネルのフック方式に似た物になります。 2 本の指で画像を拡大、1 本の指でドラッグで位置を調整します。 再プロビジョニング @@ -314,9 +312,8 @@ 変更した DPI 設定を適用するにはアプリを再起動する必要がありますが、システムステータスバーや他のアプリには影響しません DPI は %1$d に変更されました。アプリの再起動後に適用されます。 - アプリの言語 - システムに従う - 言語の変更を適用するために再起動しています + アプリの言語 + システムに従う カードの暗さを調整 エラーコード @@ -413,8 +410,6 @@ 追加されたパスのアンマウントに成功しました: %s アンマウントのパスの保存に成功しました: %s - 実行を確認してアンマウントを試す - 設定されたすべてのアンマウントの試行操作が直ちに実行されます。続行してもよろしいですか? SUS パスをリセット すべての SUS パスの構成が消去されます。続行してもよろしいですか? @@ -445,7 +440,6 @@ 自動でバインドマウント 自動でバインドマウントのアンマウントを試す KSU SUSFS シンボルを非表示 - Magic Mount の対応 SUS Kstat の対応 SUS SU モード切り替え機能 @@ -513,7 +507,6 @@ 現在の設定: %s すべてのプロセスを非表示 非 KSU プロセスのみ非表示 - 実行 簡潔モードなカーネル バージョン SukiSU のカーネル バージョンによって表示されるクリーンモードを有効または無効します。 Android データパスが設定されました: %s diff --git a/manager/app/src/main/res/values-kn/strings.xml b/manager/app/src/main/res/values-kn/strings.xml index 6bbee86..b0e75f1 100644 --- a/manager/app/src/main/res/values-kn/strings.xml +++ b/manager/app/src/main/res/values-kn/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! ಡೀಫಾಲ್ಟ್ ಆಗಿ Umount ಮಾಡ್ಯೂಲ್ ಅಪ್ಲಿಕೇಶನ್ ಪ್ರೊಫೈಲ್‌ಗಳಲ್ಲಿ \"Umount ಮಾಡ್ಯೂಲ್\" ಗಾಗಿ ಜಾಗತಿಕ ಡೀಫಾಲ್ಟ್ ಮೌಲ್ಯ. ಸಕ್ರಿಯಗೊಳಿಸಿದರೆ, ಪ್ರೊಫೈಲ್ ಸೆಟ್ ಅನ್ನು ಹೊಂದಿರದ ಅಪ್ಲಿಕೇಶನ್‌ಗಳಿಗಾಗಿ ಸಿಸ್ಟಮ್‌ಗೆ ಎಲ್ಲಾ ಮಾಡ್ಯೂಲ್ ಮಾರ್ಪಾಡುಗಳನ್ನು ಇದು ತೆಗೆದುಹಾಕುತ್ತದೆ. - Disable kprobe hooks ಈ ಆಯ್ಕೆಯನ್ನು ಸಕ್ರಿಯಗೊಳಿಸುವುದರಿಂದ ಈ ಅಪ್ಲಿಕೇಶನ್‌ಗಾಗಿ ಮಾಡ್ಯೂಲ್‌ಗಳ ಮೂಲಕ ಯಾವುದೇ ಮಾರ್ಪಡಿಸಿದ ಫೈಲ್‌ಗಳನ್ನು ಮರುಸ್ಥಾಪಿಸಲು KernelSU ಗೆ ಅನುಮತಿಸುತ್ತದೆ. ಡೊಮೇನ್ ನಿಯಮಗಳು @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ko/strings.xml b/manager/app/src/main/res/values-ko/strings.xml index 38e0ee1..387a14f 100644 --- a/manager/app/src/main/res/values-ko/strings.xml +++ b/manager/app/src/main/res/values-ko/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! 기본값으로 모듈 사용 해제 앱 프로필 메뉴의 \"모듈 마운트 해제\" 설정에 대한 전역 기본값을 설정합니다. 활성화 시, 개별 프로필이 설정되지 않은 앱은 시스템에 대한 모듈의 모든 수정사항이 적용되지 않습니다. - Disable kprobe hooks 이 옵션이 활성화되면, KernelSU는 이 앱에 대한 모듈의 모든 수정사항을 복구합니다. 도메인 규칙 @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-lt/strings.xml b/manager/app/src/main/res/values-lt/strings.xml index 6987bed..4b9c8b4 100644 --- a/manager/app/src/main/res/values-lt/strings.xml +++ b/manager/app/src/main/res/values-lt/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Atjungti modulius pagal numatytuosius parametrus Visuotinė numatytoji „Modulių atjungimo“ reikšmė programų profiliuose. Jei įjungta, ji pašalins visus sistemos modulio pakeitimus programoms, kurios neturi profilio. - Disable kprobe hooks Įjungus šią parinktį, KernelSU galės atkurti visus modulių modifikuotus failus šiai programai. Domenas Taisyklės @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-lv/strings.xml b/manager/app/src/main/res/values-lv/strings.xml index 3e70c16..4bdb803 100644 --- a/manager/app/src/main/res/values-lv/strings.xml +++ b/manager/app/src/main/res/values-lv/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! 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. - Disable kprobe hooks Iespējojot šo opciju, KernelSU varēs atjaunot visus moduļos šīs lietojumprogrammas modificētos failus. Domēns Noteikumi @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-mr/strings.xml b/manager/app/src/main/res/values-mr/strings.xml index 97473dc..9f455df 100644 --- a/manager/app/src/main/res/values-mr/strings.xml +++ b/manager/app/src/main/res/values-mr/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! डीफॉल्टनुसार मॉड्यूल्स उमाउंट करा अॅप प्रोफाइलमधील \"उमाउंट मॉड्यूल्स\" साठी जागतिक डीफॉल्ट मूल्य. सक्षम असल्यास, ते प्रोफाइल सेट नसलेल्या ॲप्लिकेशनचे सिस्टममधील सर्व मॉड्यूल बदल काढून टाकेल. - Disable kprobe hooks हा पर्याय सक्षम केल्याने KernelSU ला या ऍप्लिकेशनसाठी मॉड्यूल्सद्वारे कोणत्याही सुधारित फाइल्स पुनर्संचयित करण्यास अनुमती मिळेल. डोमेन नियम @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ms/strings.xml b/manager/app/src/main/res/values-ms/strings.xml index 314575c..ae3faf7 100644 --- a/manager/app/src/main/res/values-ms/strings.xml +++ b/manager/app/src/main/res/values-ms/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s 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. - Disable kprobe hooks Enabling this option will allow KernelSU to restore any modified files by the modules for this app. Domain Rules @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-nl/strings.xml b/manager/app/src/main/res/values-nl/strings.xml index 14dd895..5932c21 100644 --- a/manager/app/src/main/res/values-nl/strings.xml +++ b/manager/app/src/main/res/values-nl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Ontkoppel standaard de modules De globale standaardwaarde voor \"Umount modules\" in App Profile. Als dit is ingeschakeld, worden alle modulewijzigingen in het systeem verwijderd voor apps waarvoor geen profiel is ingesteld. - Disable kprobe hooks Met deze optie ingeschakeld zal KernelSU toelaten om alle gewijzigde bestanden door de modules voor deze app te herstellen. Domein Regels @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-pl/strings.xml b/manager/app/src/main/res/values-pl/strings.xml index 9346f91..6a5f99b 100644 --- a/manager/app/src/main/res/values-pl/strings.xml +++ b/manager/app/src/main/res/values-pl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Domyślnie odmontuj moduły Globalna wartość domyślna opcji \"Odmontuj moduły\" w profilu aplikacji. Jeśli jest włączona, wycofuje wszystkie modyfikacje dokonane przez moduły dla aplikacji, które nie mają ustawionego profilu. - Disable kprobe hooks Włączenie tej opcji umożliwi KernelSU przywrócenie wszelkich zmodyfikowanych plików przez moduły dla tej aplikacji. Domena Reguły @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-pt/strings.xml b/manager/app/src/main/res/values-pt/strings.xml index 9a8406d..8791beb 100644 --- a/manager/app/src/main/res/values-pt/strings.xml +++ b/manager/app/src/main/res/values-pt/strings.xml @@ -63,7 +63,6 @@ A versão atual do KernelSU %s é muito baixa para o gerenciador funcionar corretamente. Atualize para a versão %s ou superior! Módulos desativados por padrão O valor padrão global para \"Módulos Umount\" em Perfis de Aplicativos. Se ativado, removerá todas as modificações de módulo do sistema para aplicativos que não possuem um Perfil definido. - Desabilitar ganchos de \"Kprobe\" Ativar esta opção permitirá que o KernelSU restaure quaisquer arquivos modificados pelos módulos para este aplicativo. Domínio Regras @@ -233,7 +232,6 @@ Tipo de arquivo incorreto! Selecione o arquivo .kpm. Desinstalar O seguinte KPM será desinstalado: %s - Desative os hooks kprobe criados pelo KernelSU, usando ganchos embutidos em vez disso, o que é semelhante ao método de gancho do kernel GKI. Use dois dedos para ampliar a imagem e um dedo para arrastá-la para ajustar a posição Restituição @@ -307,9 +305,8 @@ O aplicativo precisa ser reiniciado para aplicar as novas configurações DPI, não afeta a barra de status do sistema ou outras aplicações DPI foi definido para %1$d, efetivo após reiniciar o aplicativo - Língua do aplicativo - Padrão do sistema - Idioma alterado, reiniciando para aplicar as alterações + Língua do aplicativo + Padrão do sistema Ajuste da escuridão do cartão Código de erro diff --git a/manager/app/src/main/res/values-ro/strings.xml b/manager/app/src/main/res/values-ro/strings.xml index 8e6affd..f768506 100644 --- a/manager/app/src/main/res/values-ro/strings.xml +++ b/manager/app/src/main/res/values-ro/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! U-montează modulele în mod implicit Valoarea implicită globală pentru „Module u-montate” în Profilurile aplicațiilor. Dacă este activat, va elimina toate modificările modulelor aduse sistemului pentru aplicațiile care nu au un profil setat. - Disable kprobe hooks Activarea acestei opțiuni va permite KernelSU să restaureze orice fișiere modificate de către modulele pentru această aplicație. Domeniu Reguli @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-ru/strings.xml b/manager/app/src/main/res/values-ru/strings.xml index b62f290..685a5c7 100644 --- a/manager/app/src/main/res/values-ru/strings.xml +++ b/manager/app/src/main/res/values-ru/strings.xml @@ -63,7 +63,6 @@ Текущая версия KernelSU %s слишком низкая для правильной работы менеджера. Пожалуйста, обновите до версии %s или выше! Размонтировать модули по умолчанию Глобальное значение по умолчанию для \"Размонтировать модули\" в профиле приложения. При включении будут удалены все модификации модулей в системе для приложений, у которых не задан профиль - Отключить kprobe хуки Включение этой опции позволит KernelSU восстанавливать любые измененные модулями файлы для данного приложения. Домен Правила @@ -112,6 +111,9 @@ \n Используйте эту опцию только после завершения OTA. \n Продолжить? Далее + Выбрать раздел + Использовать локальный файл LKM + Поддерживаются только файлы .ko Образ раздела %1$s рекомендуется (нестабильный) Выбрать KMI @@ -166,6 +168,13 @@ Предоставление рут-доступа %s запрещено Отключить su совместимость Временно отключить все приложения от получения рут-доступа через su команду (запущенные процессы с рут-доступом не будут затронуты). + Отключить размонтирование ядра + Временное отключение умного уровня ядра KernelSU. + Включить повышенную безопасность + Включить более строгие политики безопасности. + По умолчанию + Временно включены + Постоянно включены Уверены, что хотите установить следующие %1$d модули? \n\n%2$s Доп. настройки SELinux @@ -240,7 +249,6 @@ Неверный тип файла! Пожалуйста, выберите .kpm файл. Удалить Следующие KPM будут удалены: %s - Отключите хуки kprobe, созданные KernelSU, используя встроенные хуки, которые похожи на метод расширения ядра вне GKI. Используйте два пальца для увеличения изображения, и один палец для изменения положения Реализация @@ -314,9 +322,8 @@ Приложение нужно перезапустить, чтобы применить новые настройки DPI. Это не влияет на системную строку состояния или другие приложения DPI был установлен в %1$d, действующий после перезапуска приложения - Язык приложения - Как в системе - Язык изменён, перезапуск для применения изменений + Язык приложения + Как в системе Затемнение карточек код ошибки @@ -413,8 +420,6 @@ Попытка размонтировать путь успешно добавлена: %s Попытка размонтировать путь успешно сохранена: %s - Подтверждение запуска попробовать размонтировать - Это немедленно выполнит все настроенные операции размонтирования. Вы уверены, что хотите продолжить? Сбросить SUS пути Это очистит все конфигурации пути SUS. Вы уверены, что хотите продолжить? @@ -445,7 +450,6 @@ Автоматическое бинд монтирование Автоматически попробовать размонтировать привязать монтировать Скрытие KSU SUSFS Symbols - Поддержка Magic Mount Поддержка SUS Kstat Функция переключения режима SUS SU @@ -513,7 +517,6 @@ Текущие настройки: %s Скрыть для всех процессов Скрыть только для процессов, не связанных с KSU - Запустить Скрыть доп. информацию о ядре Включить или отключить чистый режим, отображаемой версии ядра SukiSU Путь к данным Android был установлен на: %s @@ -564,6 +567,8 @@ Другое Приложение Добавить путь приложения + Несоответствие версий библиотеки SuSFS! Ядро: %1$s, менеджер: %2$s. Рекомендуется обновить ядро или менеджер + Внимание Поиск приложений Выбрано %1$d приложений %1$d приложений уже добавлено @@ -623,4 +628,127 @@ Неподписанные модули могут быть неполными. Для защиты устройства установка этого модуля была заблокирована. Неподписанные модули могут быть неполными. Вы хотите разрешить установку на этом устройстве следующего модуля от неизвестного издателя? Типы хуков + + Патч KPM + Добавление дополнительных функций KPM + Патч KPM + Применить патч KPM к образу ядра перед прошивкой + Патч для отмены KPM + Отменить ранее применённый патч KPM + Патч KPM включен + Патч для отмены KPM включен + Режим патча KPM + Режим отмены патча KPM + + Подготовка инструментов KPM + Применение патча KPM + Отмена патча KPM + Найден файл образа: %s + Патч KPM успешно применен + Патч KPM успешно отменён + Файл успешно переупакован + + Не удалось извлечь zip-файл + Файл образа не найден + Ошибка патча KPM + Ошибка патча отмены KPM + Ошибка патча KPM: %s + + Следовать за ядром + Использовать ядро без каких-либо изменений KPM + + Пользовательский режим сканирования списка приложений + Включение этой опции позволит использовать сканирование пользовательского режима для списка приложений, улучшая стабильность. (Если вы столкнетесь с такими проблемами, как замораживание во время сканирования ядра списка приложений, вы можете включить эту опцию.) + Поиск многопользовательских приложений + Когда включено, сканирует приложения для всех пользователей, включая рабочие профили + Не удалось установить, проверьте права доступа + Не удалось установить: %s + Очистить среду Runtime + Очистить среду Runtime и остановить службу сканирования + Вы уверены, что хотите очистить среду Runtime? Это остановит службу сканирования и удалит связанные с ней файлы. + Runtime окружение успешно очищено + Не удалось очистить среду runtime + + Подтвердите установку + Подтвердите установку (%d файлов) + Установить + Модули + Ядро + Неизвестно + Неизвестное ядро + Неизвестный файл + Версия + Автор + Описание + Поддерживаемые устройства + + SUS Maps + Путь к библиотеке + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Добавить SUS Map + Изменить SUS Map + SUS map успешно добавлен: %1$s + SUS map удален: %1$s + SUS map обновлен: %1$s -> %2$s + SUS maps не настроены + Сбросить SUS Maps + Это действие удалит все настроенные SUS maps. Это действие нельзя отменить. + Скрытие физической памяти + Скрыть настоящий файл mmapped с разных карт в /proc/self/ + Скрыть реальные пути к папкам памяти из /proc/self/[maps|smaps_rollup|map_files|mem|pagemap]. Обратите внимание: эта функция не поддерживает скрытие анонимных карт и может скрыть встроенные хуки или PLT хуки вызванные инъекцией самой библиотеки. + Важное уведомление: для приложений с хорошо реализованными механизмами обнаружений эта функция может работать не эффективно . + Сначала найдите PID и UID целевого приложения, используя ps -enf, затем проверьте соответствующие пути в /proc/<pid>/maps и сравните номера устройства с номерами в /proc/1/mountinfo. Функция скрытия будет работать должным образом только если номера устройств совпадают. + + Просмотр логов + Назад + Поиск + Очистить логи + Вы уверены, что хотите удалить выбранный файл журнала? Это действие нельзя отменить. + Логи успешно очищены + Выберите файл журнала + Текущий лог + Старый лог + Фильтрация по типу + Все типы + Показаны записи %1$d из %2$d + Логи не найдены + Совпадающих журналов не найдено + Обновить + Необработанный лог + Поиск по UID, команде или деталям… + Очистить поиск + Просмотр логов использования + Просмотр журналов доступа к KernelSU + Исключить подтипы + Текущее приложение + Страница: %1$d/%2$d | Всего журналов: %3$d + Слишком много журналов, отображаются только последние записи %1$d + Загрузить больше логов + Все логи отображаются + + Подтвердите удаление SukiSU Manager? + Удаление не повлияет на функциональность вашего root-доступа. Он продолжит работать от этого менеджера. + Текущий менеджер несовместим с этим ядром! Пожалуйста, обновите ядро до версии %2$d или выше (сейчас %1$d) + + Управление путями размонтирования + Управления путями размонтирования ядра + Для применения изменений необходима перезагрузка. Система применит новый конфиг при следующем запуске. + Добавить путь размонтирования + Смонтировать путь + Проверить тип монтирования + Проверить, является ли оверлеем + Флаги размонтирования + 0=Обычное размонтирование, 8=MNT_DETACH, -1=Автоматически + Флаги + Подтвердите удаление + Вы уверены, что хотите удалить путь размонтирования%s? + Путь добавлен, изменения вступят в силу после перезагрузки + Путь удален, изменения вступят в силу после перезагрузки + Неудача + Подтвердите действие + Вы уверены, что хотите удалить все кастомные пути? (Стандартные пути не будут удалены) + Кастомные пути удалены + Удалить кастомные пути + Применить конфигурацию + Конфигурация применена к ядру diff --git a/manager/app/src/main/res/values-sl/strings.xml b/manager/app/src/main/res/values-sl/strings.xml index 85d14e8..83886e1 100644 --- a/manager/app/src/main/res/values-sl/strings.xml +++ b/manager/app/src/main/res/values-sl/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Po privzetem izvrzi module Globalno privzeta vrednost za \"Izvrzi module\" v aplikacijskih profilih. Če je omogočena, bo to odstranilo vse sistemske modifikacije modulov za aplikacije, ki nimajo nastavljenega profila. - Disable kprobe hooks Omogočanje te opcije bo dovolilo KernelSU, da obnovi vse zaradi modulov spremenjene datoteke za to aplikacijo. Domena Pravila @@ -233,7 +232,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -307,9 +305,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-th/strings.xml b/manager/app/src/main/res/values-th/strings.xml index dafa582..edd1486 100644 --- a/manager/app/src/main/res/values-th/strings.xml +++ b/manager/app/src/main/res/values-th/strings.xml @@ -63,7 +63,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s or higher! Umount โมดูลตามค่าเริ่มต้น หากเปิดใช้งานค่าเริ่มต้นโดยทั่วไปสำหรับ \"Umount โมดูล\" ในโปรไฟล์แอป จะเป็นการลบการแก้ไขโมดูลทั้งหมดในระบบสำหรับแอปพลิเคชันที่ไม่มีการตั้งค่าโปรไฟล์ - Disable kprobe hooks การเปิดใช้งานตัวเลือกนี้จะทำให้ KernelSU สามารถกู้คืนไฟล์ที่แก้ไขโดยโมดูลสำหรับแอปนี้ได้ โดเมน กฎ @@ -235,7 +234,6 @@ Incorrect file type! Please select .kpm file. Uninstall The following KPM will be uninstalled: %s - Disable kprobe hooks created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method. Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -309,9 +307,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code diff --git a/manager/app/src/main/res/values-tr/strings.xml b/manager/app/src/main/res/values-tr/strings.xml index 4f36cc1..e3dae4e 100644 --- a/manager/app/src/main/res/values-tr/strings.xml +++ b/manager/app/src/main/res/values-tr/strings.xml @@ -63,7 +63,6 @@ Mevcut KernelSU sürümü %s, yöneticinin düzgün çalışması için çok düşük. Lütfen sürüm %s veya daha yüksek bir sürüme yükseltin! Modülleri varsayılan olarak bağlamayı kaldır Uygulama Profilindeki \"Modülleri bağlamayı kaldır\" için küresel varsayılan değer. Etkinleştirilirse, profil ayarlanmamış uygulamalar için sistemdeki tüm modül değişikliklerini kaldırır. - Kprobe kancalarını devre dışı bırak Bu seçeneği etkinleştirmek, KernelSU\'nun bu uygulama için modüller tarafından değiştirilen dosyaları geri yüklemesine izin verecektir. Etki alanı Kurallar @@ -110,6 +109,8 @@ Etkin olmayan yuvaya yükle (OTA sonrası) Cihazınız, yeniden başlatma sonrasında **ZORUNLU** olarak mevcut etkin olmayan yuvaya önyükleme yapacaktır!\nSadece OTA tamamlandıktan sonra bu seçeneği kullanın.\nDevam etmek istiyor musunuz? İleri + Yerel LKM dosyası kullan + Yalnızca .ko dosyaları desteklenir %1$s bölüm görüntüsü önerilir (kararsız) KMI seçin @@ -164,6 +165,13 @@ %s için süper kullanıcı yetkisi verilemiyor Su uyumluluğunu devre dışı bırak Geçici olarak herhangi bir uygulamanın su komutu aracılığıyla root ayrıcalıkları elde etmesini devre dışı bırakır (mevcut root işlemleri etkilenmez). + Çekirdek ayırmasını devre dışı bırak + KernelSU tarafından kontrol edilen çekirdek seviyesindeki ayırma davranışını devre dışı bırakın. + Gelişmiş güvenliği etkinleştir + Daha katı güvenlik politikalarını etkinleştirin. + Varsayılan + Geçici olarak etkinleştir + Kalıcı olarak etkinleştir Aşağıdaki %1$d modülü yüklemek istediğinizden emin misiniz? \n\n%2$s Daha fazla ayar SELinux @@ -238,7 +246,6 @@ Yanlış dosya türü! Lütfen .kpm dosyasını seçin. Kaldır Aşağıdaki KPM kaldırılacak: %s - KernelSU tarafından oluşturulan kprobe kancalarını devre dışı bırakın, bunun yerine inline kancalar kullanın, bu da Non-GKI çekirdek kanca yöntemine benzer. Görüntüyü yaklaştırmak için iki parmağınızı kullanın ve bir parmağınızla sürükleyerek konumu ayarlayın Yeniden sağla @@ -312,9 +319,8 @@ Yeni DPI ayarlarını uygulamak için uygulamanın yeniden başlatılması gerekir, sistem durum çubuğunu veya diğer uygulamaları etkilemez DPI %1$d olarak ayarlandı, uygulama yeniden başlatıldıktan sonra geçerli olur - Uygulama Dili - Sistemi takip et - Dil değiştirildi, değişiklikleri uygulamak için yeniden başlatılıyor + Uygulama Dili + Sistemi takip et Kart Karanlığını Ayarlama hata kodu @@ -411,8 +417,6 @@ Bağlamayı kaldırmayı dene yolu başarıyla eklendi: %s Bağlamayı kaldırmayı dene yolu kaydetme başarılı: %s - Bağlamayı Kaldırmayı Dene Çalıştırmayı Onayla - Bu, yapılandırılmış tüm bağlamayı kaldırmayı dene işlemlerini hemen çalıştıracaktır. Devam etmek istiyor musunuz? SUS Yollarını Sıfırla Bu, tüm SUS yol yapılandırmalarını temizleyecektir. Devam etmek istiyor musunuz? @@ -443,7 +447,6 @@ Otomatik Bağlama Noktası Bağlama Otomatik Bağlamayı Kaldırmayı Dene Bağlama KSU SUSFS Sembollerini Gizle - Magic Mount Desteği SUS Kstat Desteği SUS SU mod değiştirme işlevi @@ -511,7 +514,6 @@ Mevcut ayar: %s Tüm işlemler için gizle Sadece KSU dışı işlemler için gizle - Çalıştır Çekirdek Sürümü Özet Modu SukiSU çekirdek sürümünün gösterdiği sade modu etkinleştirin veya devre dışı bırakın Android Veri yolu şuna ayarlandı: %s @@ -562,6 +564,8 @@ Diğer Uygulama Uygulama Yolu Ekle + SuSFS kütüphane sürümü uyuşmazlığı, çekirdek: %1$s vs yönetici: %2$s, Çekirdeği veya yöneticiyi güncellemeniz önerilir + Uyarı Uygulama Ara %1$d uygulama seçildi %1$d uygulama zaten eklendi @@ -621,4 +625,106 @@ etkin: Çekirdekteki AVC günlük kaydında, \'su\' komutuna ait tcontext\'i \'k Bilinmeyen yayıncı İmzasız modüller eksik veya değiştirilmiş olabilir. Cihazınızı korumak için bu modülün kurulumu engellenmiştir. İmzasız modüller eksik veya değiştirilmiş olabilir. Bilinmeyen bir yayıncıdan gelen aşağıdaki modülün bu cihaza kurulmasına izin vermek istiyor musunuz? + Hook tipi + + KPM Yaması + Ek KPM özellikleri eklemek için + KPM Yaması + Yüklemeden önce çekirdek imajına KPM yamasını uygula + KPM Yamasını Geri Al + Daha önce uygulanan KPM yamasını geri al + KPM yaması etkinleştirildi + KPM yaması geri alma etkinleştirildi + KPM Yama Modu + KPM Yamasını Geri Alma Modu + + KPM araçları hazırlanıyor + KPM yaması uygulanıyor + KPM yaması geri alınıyor + İmaj dosyası bulundu: %s + KPM yaması başarıyla uygulandı + KPM yaması başarıyla geri alındı + Dosya başarıyla yeniden paketlendi + + Zip dosyası çıkarılamadı + İmaj dosyası bulunamadı + KPM yaması başarısız oldu + KPM yamasını geri alma başarısız oldu + KPM yama işlemi başarısız oldu: %s + + Çekirdeği Takip Et + Çekirdeği herhangi bir KPM değişikliği olmadan olduğu gibi kullan + + Kullanıcı modu uygulama listesi taraması + Bu seçeneği etkinleştirmek, uygulama listesi için kullanıcı modu taramasını kullanarak kararlılığı artıracaktır. (Uygulama listesinin çekirdek tarafından taranması sırasında donma gibi sorunlar yaşıyorsanız, bu seçeneği etkinleştirmeyi deneyebilirsiniz.) + Çok Kullanıcılı Uygulama Taraması + Etkinleştirildiğinde, iş profilleri de dahil olmak üzere tüm kullanıcıların uygulamalarını tarar. + Ayar başarısız oldu, lütfen izinleri kontrol edin + Ayar başarısız oldu: %s + Çalışma Zamanı Ortamını Temizle + Çalışma zamanı dosyalarını temizleyin ve tarayıcı hizmetini durdurun + Çalışma zamanı ortamını temizlemek istediğinizden emin misiniz? Bu işlem tarayıcı hizmetini durduracak ve ilgili dosyaları kaldıracaktır. + Çalışma zamanı ortamı başarıyla temizlendi + Çalışma zamanı ortamı temizlenemedi + + Kurulumu Onayla + Kurulumu Onayla (%d dosya) + Yükle + Modül + Kernel + Bilinmiyor + Bilinmeyen Kernel + Bilinmeyen Dosya + Sürüm + Geliştirici + Açıklama + Desteklenen Cihazlar + + SUS Eşlemeleri + Kütüphane Yolu + /data/adb/modules/my_module/zygisk/arm64-v8a.so + SUS Eşlemesi Ekle + SUS Eşlemesini Düzenle + SUS eşlemesi başarıyla eklendi: %1$s + SUS eşlemesi kaldırıldı: %1$s + SUS eşlemesi güncellendi: %1$s -> %2$s + Yapılandırılmış SUS eşlemesi yok + SUS Eşlemelerini Sıfırla + Bu, yapılandırılmış tüm SUS eşlemelerini kaldıracaktır. Bu işlem geri alınamaz. + Bellek Eşlemesi Gizleme + /proc/self/ içindeki çeşitli eşlemelerden mmap\'lenmiş gerçek dosyayı gizle + Bellek eşlemelerinin gerçek dosya yollarını /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] konumundan gizleyin. Lütfen dikkat: Bu özellik, anonim bellek eşlemelerini gizlemeyi desteklemez ve enjekte edilen kütüphanenin kendisinin neden olduğu satır içi kancaları veya PLT kancalarını da gizleyemez. + Önemli Not: İyi uygulanmış enjeksiyon tespit mekanizmalarına sahip uygulamalar için bu özellik, tespiti etkili bir şekilde atlatamayabilir. + Öncelikle, ps -enf kullanarak hedef uygulamanın PID ve UID\'sini bulun, ardından /proc/<pid>/maps içindeki ilgili yolları kontrol edin ve tutarlılığı sağlamak için cihaz numaralarını /proc/1/mountinfo\'dakilerle karşılaştırın. Yalnızca cihaz numaraları eşleştiğinde harita gizleme işlevi düzgün çalışabilir. + + Günlük Görüntüleyici + Geri + Ara + Günlükleri Temizle + Seçili günlük dosyasını temizlemek istediğinizden emin misiniz? Bu işlem geri alınamaz. + Günlükler başarıyla temizlendi + Günlük Dosyası Seç + Mevcut Günlük + Eski Günlük + Türe Göre Filtrele + Tüm Türler + %2$d girişten %1$d tanesi gösteriliyor + Günlük bulunamadı + Eşleşen günlük bulunamadı + Yenile + Ham Günlük + UID, komut veya ayrıntılara göre ara… + Aramayı temizle + Kullanım Günlüklerini Görüntüle + KernelSU süper kullanıcı erişim günlüklerini görüntüle + Alt türleri hariç tut + Mevcut Uygulama + Sayfa: %1$d/%2$d | Toplam günlük: %3$d + Çok fazla günlük var, yalnızca son %1$d giriş gösteriliyor + Daha Fazla Günlük Yükle + Tüm günlükler görüntülendi + + SukiSU Yöneticisi Kaldırılsın mı? + Kaldırma işlemine devam etmek, root erişiminizin temel işlevselliğini etkilemeyecektir. Root, bu yöneticiden bağımsız olarak çalışacak şekilde tasarlanmıştır. + Mevcut yönetici bu çekirdekle uyumsuz! Lütfen çekirdeği %2$d veya daha yüksek bir sürüme yükseltin (mevcut sürüm %1$d) diff --git a/manager/app/src/main/res/values-uk/strings.xml b/manager/app/src/main/res/values-uk/strings.xml index 39216a2..3d6905b 100644 --- a/manager/app/src/main/res/values-uk/strings.xml +++ b/manager/app/src/main/res/values-uk/strings.xml @@ -1,6 +1,5 @@ - SukiSU Ultra Головна Не встановлено Натисніть, щоб встановити @@ -52,7 +51,7 @@ Підтримати нас KernelSU є, і завжди буде, безкоштовним та з відкритим вихідним кодом. Однак ви можете показати нам свою підтримку, зробивши пожертву. Приєднуйтесь до нашого каналу %2$s]]> - Профіль додатку + Профіль додатку За замовчуванням Шаблон Власний @@ -65,7 +64,6 @@ Поточна версія KernelSU %s занадто низька для коректної роботи менеджера. Будь ласка, оновіться до версії %s або вище! Відмонтовувати модулі за замовчуванням Глобальне значення за замовчуванням для "Відмонтувати модулі" у профілі додатку. Якщо ввімкнено, це видалить усі зміни системи, зроблені модулями, для додатків без встановленого профілю. - Вимкнути хуки kprobe Увімкнення цієї опції дозволить KernelSU відновити будь-які змінені модулями файли для цього додатку. Домен Правила @@ -238,7 +236,6 @@ Неправильний тип файлу! Будь ласка, виберіть файл .kpm. Видалити Буде видалено наступний KPM: %s - Вимкнути хуки kprobe, створені KernelSU, використовуючи натомість інлайн-хуки, що схоже на метод хуків для ядер без GKI. Використовуйте два пальці для масштабування зображення та один палець для перетягування, щоб налаштувати положення Переналаштувати @@ -312,9 +309,8 @@ Додаток потрібно перезапустити, щоб застосувати нові налаштування DPI; це не вплине на системний рядок стану або інші додатки DPI встановлено на %1$d, набуде чинності після перезапуску додатку - Мова додатку - Як у системі - Мову змінено, перезапуск для застосування змін + Мова додатку + Як у системі Налаштування затемнення карток код помилки @@ -411,8 +407,6 @@ Шлях для спроби відмонтування успішно додано: %s Спроба збереження шляху відмонтування успішна: %s - Підтвердити виконання спроби відмонтування - Це негайно виконає всі налаштовані операції спроби відмонтування. Ви впевнені, що хочете продовжити? Скинути шляхи SUS Це видалить усі конфігурації шляхів SUS. Ви впевнені, що хочете продовжити? @@ -443,7 +437,6 @@ Автоматичне прив\'язане монтування Автоматична спроба відмонтування прив\'язаного монтування Приховати символи KSU SUSFS - Підтримка Magic Mount Підтримка SUS Kstat Функція перемикання режиму SUS SU @@ -511,7 +504,6 @@ Поточне налаштування: %s Приховувати для всіх процесів Приховувати лише для процесів, що не належать KSU - Запустити Спрощений режим версії ядра Увімкнути або вимкнути спрощене відображення версії ядра SukiSU Шлях до Android Data встановлено на: %s diff --git a/manager/app/src/main/res/values-vi/strings.xml b/manager/app/src/main/res/values-vi/strings.xml index c3c9788..704533c 100644 --- a/manager/app/src/main/res/values-vi/strings.xml +++ b/manager/app/src/main/res/values-vi/strings.xml @@ -6,10 +6,10 @@ Đang hoạt động Phiên bản: %s Không được hỗ trợ - Không phát hiện được trình điều khiển SukiSU Ultra trên Kernel của bạn, Kernel sai? + Không phát hiện được Trình điều khiển SukiSU Ultra trên Kernel của bạn, Kernel sai? Phiên bản Kernel Phiên bản SuSFS - Phiên bản trình quản lý + Phiên bản Trình quản lý Trạng thái SELinux Vô hiệu hoá Enforcing @@ -41,7 +41,7 @@ Làm mới Hiển thị ứng dụng hệ thống Ẩn ứng dụng hệ thống - Gửi logs + Gửi nhật ký Chế độ an toàn Khởi động lại để có hiệu lực Các module không khả dụng do xung đột với Magisk! @@ -60,10 +60,9 @@ Bối cảnh SELinux Umount modules Cập nhật Hồ sơ ứng dụng cho %s thất bại - Phiên bản SukiSU Ultra hiện tại %s quá thấp để trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %s hoặc cao hơn! + Phiên bản SukiSU Ultra hiện tại %s quá thấp để Trình quản lý hoạt động bình thường. Vui lòng cập nhật lên phiên bản %s hoặc cao hơn! Umount modules Giá trị mặc định chung cho \"Umount modules\" trong Hồ sơ ứng dụng. Nếu được bật, mọi thay đổi hệ thống do các module gây ra sẽ bị gỡ bỏ khỏi hệ thống và các ứng dụng chưa thiết lập hồ sơ - Vô hiệu hoá kprobes hook Bật tùy chọn này sẽ cho phép SukiSU Ultra khôi phục mọi file đã được các module sửa đổi trong ứng dụng này Tên miền Quy tắc @@ -124,8 +123,8 @@ Flash thành công Flash thất bại Đã chọn LKM: %s - Lưu logs - Logs đã được lưu + Lưu nhật ký + Nhật ký đã được lưu Chế độ SuS SU: Xác nhận cài đặt module %1$s? @@ -238,7 +237,6 @@ Loại file không đúng! Vui lòng chọn file .kpm Gỡ cài đặt KPM sau đây sẽ được gỡ cài đặt: %s - Vô hiệu hoá kprobes hook được tạo bởi SukiSU Ultra, thay vào đó sử dụng inlines hook, tương tự như phương pháp hook của Kernel non-GKI Sử dụng hai ngón tay để phóng to hình ảnh và một ngón tay kéo thả để điều chỉnh vị trí Chọn lại @@ -312,13 +310,12 @@ Ứng dụng cần được khởi động lại để áp dụng cài đặt DPI mới, không ảnh hưởng đến thanh trạng thái hệ thống hoặc các ứng dụng khác DPI đã được đặt thành %1$d, có hiệu lực sau khi khởi động lại ứng dụng - Ngôn ngữ ứng dụng - Mặc định theo hệ thống - Ngôn ngữ đã thay đổi, khởi động lại để áp dụng thay đổi + Ngôn ngữ ứng dụng + Mặc định theo hệ thống Độ trong suốt của thẻ Error code - Vui lòng kiểm tra logs + Vui lòng kiểm tra nhật ký Đang cài đặt module %1$d/%2$d Cài đặt module %d thất bại Tải xuống module thất bại @@ -366,7 +363,7 @@ Reset về Default Áp dụng - Xác nhận khôi phục + Xác nhận Đặt lại Không tìm thấy file ksu_susfs Thực thi lệnh SuSFS thất bại @@ -411,14 +408,12 @@ Đường dẫn Try Umount đã thêm thành công: %s Đường dẫn Try Umount đã lưu thành công: %s - Xác nhận chạy Try Umount - Thao tác này sẽ áp dụng ngay lập tức tất cả các thiết lập Try Umount đã cấu hình. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục Đường dẫn SuS + Đặt lại Đường dẫn SuS Thao tác này sẽ xóa tất cả các cấu hình Đường dẫn SuS. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục SuS Mount + Đặt lại SuS Mount Thao tác này sẽ xóa tất cả các cấu hình SuS Mount. Bạn có chắc chắn muốn tiếp tục không? - Khôi phục Try Umount + Đặt lại Try Umount Thao tác này sẽ xóa tất cả các cấu hình Try Umount. Bạn có chắc chắn muốn tiếp tục không? Reset Cài đặt Đường dẫn @@ -438,21 +433,20 @@ Hỗ trợ giả mạo Uname Giả mạo Cmdline/Bootconfig Mở hỗ trợ chuyển hướng - Hỗ trợ ghi logs + Hỗ trợ ghi nhật ký Tự động Mount mặc định Tự động Bind Mount Tự động Try Umount Bind Mount Ẩn biểu tượng KSU SuSFS - Hỗ trợ Magic Mount Hỗ trợ SuS Kstat Chức năng chuyển đổi chế độ SuS SU - Nhấn để bật/tắt ghi logs - Kích hoạt logs SuSFS - Bật hoặc tắt ghi logs cho SuSFS - Cấu hình ghi logs SuSFS - Bật ghi logs SuSFS - Tắt ghi logs SuSFS + Nhấn để bật/tắt ghi nhật ký + Kích hoạt nhật ký SuSFS + Bật hoặc tắt ghi nhật ký cho SuSFS + Cấu hình ghi nhật ký SuSFS + Bật ghi nhật ký SuSFS + Tắt ghi nhật ký SuSFS JSON cập nhật JSON URL cập nhật đã được sao chép vào clipboard @@ -489,7 +483,7 @@ Gợi ý: Bạn có thể sử dụng \"default\" để thiết lập giá trị ban đầu Thêm Đường dẫn Kstat Thêm - Khôi phục Cấu hình Kstat + Đặt lại Cấu hình Kstat Bạn có chắc chắn muốn xóa tất cả cấu hình Kstat không? Không thể hoàn tác hành động này Mô tả cấu hình Kstat • add_sus_kstat_statically: Thông tin thống kê cấu hình tĩnh của các File/Folder @@ -511,7 +505,6 @@ Cài đặt hiện tại: %s Ẩn khỏi tất cả các tiến trình Chỉ ẩn đối với các tiến trình không phải KSU - Chạy Hiển thị tóm tắt \"Phiên bản Kernel\" Tóm tắt hiển thị phiên bản Kernel cho ngắn gọn Đường dẫn Android Data đã được đặt thành: %s @@ -562,6 +555,8 @@ Khác Ứng dụng Thêm Đường dẫn ứng dụng + Phiên bản thư viện SuSFS không khớp (Kernel: %1$s & Trình quản lý: %2$s). Nên cập nhật Kernel hoặc Trình quản lý + Cảnh báo Tìm kiếm ứng dụng %1$d ứng dụng đã chọn %1$d ứng dụng đã thêm @@ -591,9 +586,9 @@ Chỉnh sửa Đường dẫn Vòng lặp SuS Đường dẫn Vòng lặp SuS đã thêm thành công: %1$s Đã xoá Đường dẫn Vòng lặp SuS: %1$s - Đã cập nhật Đường dẫn Vòng lặp SuS: %1$s -> %2$s + Đã cập nhật Đường dẫn Vòng lặp SuS: %1$s → %2$s Không có Đường dẫn Vòng lặp SuS nào được cấu hình - Khôi phục Đường dẫn Vòng lặp SuS + Đặt lại Đường dẫn Vòng lặp SuS Bạn có chắc chắn muốn xóa tất cả các Đường dẫn Vòng lặp SuS không? Thao tác này không thể hoàn tác Đường dẫn Vòng lặp /data/example/path @@ -622,4 +617,104 @@ Bật: Kích hoạt tính năng giả mạo sus tcontext của \'su\' thành \'k Các module chưa được ký có thể chưa hoàn chỉnh. Để bảo vệ thiết bị của bạn, module này đã bị chặn cài đặt Các module chưa được ký có thể chưa hoàn chỉnh. Bạn có muốn cài đặt module này từ một tác giả chưa xác định không? Chế độ Hook + + Vá KPM + Thêm tính năng KPM + Vá KPM + Thực hiện Vá KPM vào kernel trước khi flash + Hoàn tác Vá KPM + Hoàn tác Vá KPM đã áp dụng trước đó + Vá KPM đã bật + Hoàn tác Vá KPM đã bật + Chế độ Vá KPM + Chế độ hoàn tác Vá KPM + + Chuẩn bị công cụ Vá KPM + Áp dụng Vá KPM + Hoàn tác Vá KPM + Đã tìm thấy file image: %s + Đã vá KPM thành công + Hoàn tác vá KPM thành công + Nén lại file thành công + + Giải nén file thất bại + Không tìm thấy file image + Vá KPM thất bại + Hoàn tác Vá KPM thất bại + Quá trình Vá KPM thất bại: %s + + Mặc định theo file Kernel + Sử dụng file kernel mặc định mà không có bất kỳ sửa đổi nào về KPM + + Chế độ quét danh sách ứng dụng người dùng + Bật tuỳ chọn này thì chế độ người dùng sẽ được sử dụng để quét danh sách ứng dụng nhằm cải thiện tính ổn định (Nếu danh sách ứng dụng quét kernel bị kẹt và xảy ra các sự cố khác, bạn có thể thử bật tùy chọn này) + Quét ứng dụng nhiều người dùng + Khi được bật, tất cả ứng dụng của người dùng sẽ được quét, bao gồm cả dữ liệu công việc, v.v + Thiết lập thất bại, vui lòng kiểm tra quyền + Thiết lập thất bại: %s + Dọn dẹp môi trường hoạt động + Dọn dẹp các file hoạt động và dừng quét các dịch vụ + Bạn có chắc chắn muốn dọn dẹp môi trường hoạt động không? Thao tác này sẽ dừng dịch vụ quét và xóa các file liên quan + Dọn dẹp môi trường hoạt động thành công + Dọn dẹp môi trường hoạt động thất bại + + Xác nhận cài đặt + Xác nhận cài đặt (%d files) + Cài đặt + Module + Kernel + Không xác định + Kernel không xác định + Tệp không xác định + Phiên bản + Tác giả + Mô tả + Thiết bị được hỗ trợ + + SuS Maps + Đường dẫn thư viện + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Thêm SuS Map + Chỉnh sửa SuS Map + Đã thêm SuS Map thành công: %1$s + Đã xoá SuS Map: %1$s + Đã cập nhật SuS Map: %1$s → %2$s + Không có SuS Map nào được cấu hình + Đặt lại SuS Map + Thao tác này sẽ xóa tất cả các SuS Map đã cấu hình. Không thể hoàn tác thao tác này + Ẩn Bộ nhớ Map + Ẩn tệp mmapp khỏi các map khác nhau trong /proc/self/ + Ẩn đường dẫn tệp của bộ nhớ map khỏi /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Lưu ý: Tính năng này không hỗ trợ ẩn bộ nhớ map ẩn danh, cũng như không thể ẩn các hook nội tuyến hoặc hook PLT do chính thư viện được đưa vào gây ra + Quan trọng: Đối với các ứng dụng có cơ chế phát hiện tấn công mạnh mẽ, tính năng này có thể không hiệu quả trong việc bỏ qua khả năng phát hiện + Trước tiên, hãy tìm PID và UID của ứng dụng đích bằng lệnh ps -enf. Sau đó kiểm tra các đường dẫn liên quan trong /proc/<pid>/maps và so sánh chúng với số thiết bị trong /proc/1/mountinfo để đảm bảo tính nhất quán. Chỉ khi số thiết bị khớp nhau thì chức năng ẩn map mới hoạt động bình thường + + Trình xem nhật ký + Trở lại + Tìm kiếm + Xoá nhật ký + Bạn có chắc chắn muốn xóa tệp nhật ký đã chọn không? Thao tác này không thể hoàn tác + Đã xoá nhật ký thành công + Chọn tệp nhật ký + Nhật ký hiện tại + Nhật ký cũ + Lọc theo loại + Tất cả các loại + Hiển thị %1$d trong tổng %2$d nhật ký + Không tìm thấy nhật ký nào + Không tìm thấy nhật ký nào phù hợp + Làm mới + Raw Log + Tìm kiếm theo UID, lệnh hoặc chi tiết… + Xoá tìm kiếm + Xem nhật ký sử dụng + Xem nhật ký truy cập Superuser + Loại trừ các phân loại + Ứng dụng hiện tại + Trang: %1$d/%2$d | Tổng nhật ký: %3$d + Quá nhiều nhật ký, chỉ hiển thị %1$d nhật ký mới nhất + Đang load thêm nhật ký + Tất cả nhật ký đã được hiển thị + + Bạn muốn xoá tôi đi sao 😭 + Hừm, được rồi, tôi sẽ bị bạn gỡ cài đặt. Chức năng root sẽ không ngừng hoạt động chỉ vì bạn mất một Trình quản lý. Đừng lo chỉ Gỡ cài đặt Trình quản lý thôi thì không thể mất quyền truy cập root được đâu, zako~❤️ diff --git a/manager/app/src/main/res/values-zh-rCN/strings.xml b/manager/app/src/main/res/values-zh-rCN/strings.xml index 2141334..7cba39f 100644 --- a/manager/app/src/main/res/values-zh-rCN/strings.xml +++ b/manager/app/src/main/res/values-zh-rCN/strings.xml @@ -63,7 +63,6 @@ 当前 KernelSU 版本 %s 过低,管理器无法正常工作,请将内核 KernelSU 版本升级至 %s 或以上! 默认卸载模块 App Profile 中\"卸载模块\"的全局默认值,如果启用,将会为没有设置 Profile 的应用移除所有模块针对系统的修改。 - 禁用 kprobe 钩子 启用该选项后将允许 KernelSU 为本应用还原被模块修改过的文件。 规则 @@ -108,8 +107,11 @@ 直接安装(推荐) 选择一个需要修补的镜像 安装到未使用的槽位(OTA 后) - 将在重启后强制切换到另一个槽位!\n注意只能在 OTA 更新完成后的重启之前使用。\n确认? + 将在重启后强制切换到另一个槽位!注意只能在 OTA 更新完成后的重启之前使用。 下一步 + 选择分区 + 使用本地 LKM 文件 + 仅支持选择 .ko 文件 建议选择 %1$s 分区镜像 (实验性的) 选择 KMI @@ -123,7 +125,7 @@ 刷写中 刷写完成 刷写失败 - 选择的 LKM:%s + 已选择的 LKM:%s 保存日志 日志已保存 SuS SU 模式: @@ -163,7 +165,14 @@ 设备 不允许授予 %s 超级用户权限 禁用 su 兼容性 - 临时禁止任何应用程序通过 su 命令获取 Root 权限(现有的 Root 进程不受影响) + 禁止任何应用通过 su 命令获取 root 权限(已运行的 root 进程不受影响)。 + 关闭内核 umount + 关闭 KernelSU 控制的内核级 umount 行为。 + 增强安全性 + 使用更严格的安全策略。 + 默认 + 临时启用 + 始终启用 确定要安装以下 %1$d 个模块吗?\n\n%2$s 更多设置 SELinux @@ -238,7 +247,6 @@ 文件类型不正确,请选择 .kpm 文件 卸载 将卸载以下 KPM 模块:\n%s - 禁用由 KernelSU 创建的 kprobe 钩子,并使用非 kprobe 内联钩子代替,实现方式类似于不支持 kprobe 的非 GKI 内核。 使用双指缩放图片,单指拖动调整位置 重置 @@ -312,9 +320,8 @@ 应用需要重启以应用新的 DPI 设置,不会影响系统状态栏或其他应用 DPI 已设置为 %1$d,重启应用后生效 - 应用语言 - 跟随系统 - 语言已更改,重启应用以应用更改 + 应用语言 + 跟随系统 卡片暗度调节 错误代码 @@ -411,8 +418,6 @@ 尝试 umount 路径添加成功: %s 尝试 umount 路径保存成功: %s - 确认运行尝试卸载 - 这将立即执行所有已配置的尝试卸载操作,确定要继续吗? 重置 SuS 路径 这将清除所有 SuS 路径配置,确定要继续吗? @@ -443,7 +448,6 @@ 自动绑定挂载 自动尝试卸载绑定挂载 隐藏 KSU SuSFS 符号 - 魔法坐骑支持 SuS Kstat 支持 SuS SU 模式切换功能 @@ -511,7 +515,6 @@ 当前设置: %s 对所有进程隐藏 仅对非 KSU 进程隐藏 - 运行 内核版本简洁模式 启用或禁用 SukiSU 内核版本显示的简洁模式 Android Data 路径已设置为: %s @@ -562,6 +565,8 @@ 其他 应用 添加应用路径 + SuSFS 库版本不匹配,内核:%1$s vs 管理器:%2$s,建议更新内核或管理器 + 警告 搜索应用 %1$d 个已选应用 %1$d 个已添加应用 @@ -583,6 +588,7 @@ 未知 活跃管理器 无活跃管理器 + SukiSU Zygisk 实现 SuS 循环路径 @@ -595,6 +601,7 @@ 重置循环路径 确定要清空所有 SuS 循环路径吗?此操作无法撤销。 循环路径 + /data/example/path 注意:只有不在 /storage/ 和 /sdcard/ 内的路径才能通过循环路径添加。 错误:循环路径不能位于 /storage/ 或 /sdcard/ 目录内 循环路径 @@ -620,7 +627,7 @@ 未经签名的模块可能不完整。为了对设备进行保护,已阻止安装此模块。 未经签名的模块可能不完整。你想安装来自未知发布者的模块吗? 钩子类型 - + KPM 修补 用于添加附加的 KPM 功能 KPM 修补 @@ -631,26 +638,21 @@ KPM 撤销修补已启用 KPM 修补模式 KPM 撤销修补模式 - + 准备 KPM 修补工具 正在应用 KPM 修补 正在撤销 KPM 修补 - KPM 工具准备完成 找到 Image 文件: %s KPM 修补成功 KPM 撤销修补成功 文件重新打包完成 - - 提取 kptools 工具失败 - 提取 kpimg 文件失败 - 准备 KPM 工具失败: %s + 解压压缩包失败 未找到 Image 文件 KPM 修补失败 KPM 撤销修补失败 - 重新打包压缩文件失败 KPM 修补操作失败: %s - + 跟随内核 原样使用内核,不进行任何 KPM 修改 @@ -665,5 +667,88 @@ 您确定要清理运行环境吗?这将停止扫描服务并删除相关文件 运行环境清理成功 运行环境清理失败 - 清理运行环境时出错:%s + + 确认安装 + 确认安装(%d 个文件) + 确认安装 + 模块 + 内核 + 未知类型 + 未知内核 + 未知文件 + 版本 + 作者 + 描述 + 支持设备 + + SUS映射 + 库文件路径 + /data/adb/modules/my_module/zygisk/arm64-v8a.so + 添加SUS映射 + 编辑SUS映射 + SUS映射添加成功: %1$s + SUS映射已移除: %1$s + SUS映射已更新: %1$s -> %2$s + 未配置SUS映射 + 重置SUS映射 + 这将移除所有已配置的SUS映射。此操作无法撤销。 + 内存映射隐藏 + 隐藏/proc/self/中各种映射中的mmap真实文件 + 从 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隐藏内存映射的真实文件路径。请注意:此功能不支持隐藏匿名内存映射,也无法隐藏由注入库本身产生的内联钩子或 PLT 钩子。 + 重要提示:对于具备完善注入检测机制的应用,此功能可能无法有效绕过检测。 + 首先通过 ps -enf 查找目标应用的 PID 和 UID,然后检查 /proc/<pid>/maps 中的相关路径,并与 /proc/1/mountinfo 中的设备号进行比对以确保一致性。只有当设备号一致时,隐藏映射才能正常工作。 + + 日志查看器 + 返回 + 搜索 + 清空日志 + 确定要清空选中的日志文件吗?此操作无法撤销。 + 日志清空成功 + 选择日志文件 + 当前日志 + 旧日志 + 按类型筛选 + 所有类型 + 显示 %1$d / %2$d 条记录 + 未找到日志 + 未找到匹配的日志 + 刷新 + 原始日志 + 按 UID、命令或详情搜索… + 清除搜索 + 查看使用日志 + 查看 KernelSU 超级用户访问日志 + 排除子类型 + 当前应用 + 页面: %1$d/%2$d | 总日志: %3$d + 日志过多,仅显示最新 %1$d 条 + 加载更多日志 + 已显示所有日志 + + 真要走? + 哼,卸就卸。Root 功能可不会因为失去区区一个管理器就停止运作。别担心,zakozako 只卸载管理器可干不掉 Root 呢,zako~❤️ + 当前管理器与此内核不兼容!请将内核升级至版本 %2$d 或以上(当前 %1$d) + Umount 路径管理 + 管理内核卸载路径 + 添加或删除路径后需要重启设备才能生效。系统会在下次启动时应用新的配置。 + 添加 Umount 路径 + 挂载路径 + 检查挂载类型 + 检查是否为 overlay 类型 + 卸载标志 + 0=正常卸载, 8=MNT_DETACH, -1=自动 + 标志 + 默认条目 + 确认删除 + 确定要删除路径 %s 吗? + 路径已添加,重启后生效 + 路径已删除,重启后生效 + 操作失败 + 确认操作 + 确定要清除所有自定义路径吗?(默认路径将保留) + 自定义路径已清除 + 清除自定义 + 应用配置 + 配置已应用到内核 + 包含 %1$d 个应用 diff --git a/manager/app/src/main/res/values-zh-rHK/strings.xml b/manager/app/src/main/res/values-zh-rHK/strings.xml index 9101fc7..2db3160 100644 --- a/manager/app/src/main/res/values-zh-rHK/strings.xml +++ b/manager/app/src/main/res/values-zh-rHK/strings.xml @@ -63,7 +63,6 @@ 當前 KernelSU 版本 %s 過低,管理器無法正常工作,請將核心 KernelSU 版本升級至 %s 或以上! 默認卸載模組 App Profile 中\"卸載模組\"嘅全局默認值,如果啟用,將會為冇設定 Profile 嘅應用移除所有模組針對系統嘅修改。 - 禁用 Kprobe Hook 啟用該選項後將允許 KernelSU 為本應用還原被模組修改過嘅文件。 規則 @@ -235,7 +234,6 @@ 文件類型唔正確,請選擇 .kpm 文件 卸載 將卸載以下 KPM 模組:\n%s - 禁用由 KernelSU 創建嘅 Kprobe Hook,並使用非 Kprobe 內嘅聯鈎子代替,實現方式類似於唔支援 Kprobe 嘅非 GKI 核心。 使用雙指縮放圖片,單指拖動調整位置 重置 @@ -309,9 +307,8 @@ 應用需要重新啟動以應用新嘅 DPI 配置,唔會影響系統狀態欄或其他應用 DPI 已設定為 %1$d,重新啟動應用後生效 - 應用語言 - 跟隨系統 - 語言已更改,重新啟動應用以應用更改 + 應用語言 + 跟隨系統 卡片暗度調節 錯誤代碼 @@ -408,8 +405,6 @@ 嘗試 umount 路徑添加成功: %s 嘗試 umount 路徑存儲成功: %s - 確認運行嘗試卸載 - 這將立即執行所有已配置嘅嘗試卸載操作,確定要繼續嗎? 重置 SuS 路徑 這將清除所有 SuS 路徑配置,確定要繼續嗎? @@ -440,7 +435,6 @@ 自動綁定掛載 自動嘗試卸載綁定掛載 隱藏 KSU SuSFS 符號 - 魔法坐騎支援 SuS Kstat 支援 SuS SU 模式切換功能 @@ -508,7 +502,6 @@ 而家嘅配置: %s 對所有程序隱藏 淨係對非 KS> 程序隱藏 - 執行 核心版本簡潔模式 啟用或者禁用 SukiSU 核心版本顯示嘅簡潔模式 Android Data 路徑已配置為: %s @@ -602,4 +595,13 @@ 未知發布者 未經簽名嘅模組可能唔完整。為咗保護裝置,已經阻止安裝呢個模組。 未經簽名嘅模組可能唔完整。你想唔想安裝嚟自未知發布者嘅模組? + + + + + + + + + diff --git a/manager/app/src/main/res/values-zh-rTW/strings.xml b/manager/app/src/main/res/values-zh-rTW/strings.xml index 2fd6d0f..fae212c 100644 --- a/manager/app/src/main/res/values-zh-rTW/strings.xml +++ b/manager/app/src/main/res/values-zh-rTW/strings.xml @@ -6,7 +6,7 @@ 運作中 版本:%s 不支援 - 未在內核上偵測到 KernelSU 驅動程式,內核錯誤? + 內核上未偵測到 KernelSU 驅動程式,內核錯誤? 內核版本 SuSFS 版本 管理器版本 @@ -63,7 +63,6 @@ 目前 KernelSU 版本 %s 過低,管理器無法正常運作,請將內核 KernelSU 版本升級至 %s 或以上! 預設卸載模組 應用程式設定檔中\"卸載模組\"\的全域預設值,若啟用,將為未設定設定檔的應用程式移除所有模組對系統的修改。 - 禁用 kprobe 切換 啟用此選項後,將允許 KernelSU 為此應用程式還原被模組修改的檔案。 規則 @@ -238,7 +237,6 @@ 檔案類型不正確,請選擇 .kpm 檔案 解除安裝 將解除安裝以下 KPM 模組:\n%s - 禁用由 KernelSU 建立的 kprobe 掛鉤,並使用非 kprobe 內聯掛鉤代替,實現方式類似於不支援 kprobe 的非 GKI 內核。 使用雙指縮放圖片,單指拖曳調整位置 重置 @@ -312,9 +310,8 @@ 應用程式需重新啟動以套用新的 DPI 設定,不會影響系統狀態列或其他應用程式 DPI 已設為 %1$d,重新啟動應用程式後生效 - 應用程式語言 - 跟隨系統 - 語言已更改,重新啟動應用程式以套用變更 + 應用程式語言 + 跟隨系統 卡片暗度調整 錯誤代碼 @@ -411,8 +408,6 @@ 嘗試 umount 路徑新增成功: %s 嘗試 umount 路徑儲存成功: %s - 確認執行嘗試卸載 - 這將立即執行所有已設定的嘗試卸載操作,確定要繼續嗎? 重設 SuS 路徑 這將清除所有 SuS 路徑設定,確定要繼續嗎? @@ -443,7 +438,6 @@ 自動綁定掛載 自動嘗試卸載綁定掛載 隱藏 KSU SuSFS 符號 - 魔法掛載支援 SuS 內核統計支援 SuS SU 模式切換功能 @@ -511,7 +505,6 @@ 目前設定: %s 對所有程序隱藏 僅對非 KSU 程序隱藏 - 執行 內核版本簡潔模式 啟用或停用 SukiSU 內核版本顯示的簡潔模式 Android Data 路徑已設定為: %s @@ -584,7 +577,6 @@ 活躍管理器 無活躍管理器 Zygisk 實現 - SukiSU SuS 循環路徑 新增 SuS 循環路徑 @@ -598,9 +590,9 @@ 循環路徑 注意:只有不在 /storage/ 和 /sdcard/ 內的路徑才能透過循環路徑新增。 錯誤:循環路徑不能位於 /storage/ 或 /sdcard/ 目錄內 - 循環路徑 新增循環路徑 + SuS 循環路徑 循環路徑設定 循環路徑會在每次非 root 使用者應用程式或隔離服務啟動時重新標記為 SUS_PATH。這有助於解決新增的路徑可能因 inode 狀態重設或內核中 inode 重新建立而失效的問題 @@ -621,7 +613,7 @@ 未經簽名的模組可能不完整。為了對設備進行保護,已阻止安裝此模組。 未經簽名的模組可能不完整。你想安裝來自未知發布者的模組嗎? 鉤子類型 - + KPM 修補 用於新增附加的 KPM 功能 KPM 修補 @@ -632,26 +624,64 @@ KPM 撤銷修補已啟用 KPM 修補模式 KPM 撤銷修補模式 - + 準備 KPM 修補工具 - 正在套用 KPM 修補 + 正在應用 KPM 修補 正在撤銷 KPM 修補 - KPM 工具準備完成 找到 Image 檔案: %s KPM 修補成功 KPM 撤銷修補成功 檔案重新打包完成 - 提取 kptools 工具失敗 - - 提取 kpimg 檔案失敗 - 準備 KPM 工具失敗: %s - 解壓縮檔失敗 + + 解壓壓縮檔失敗 未找到 Image 檔案 KPM 修補失敗 KPM 撤銷修補失敗 - 重新打包壓縮檔失敗 KPM 修補操作失敗: %s - + 跟隨內核 原樣使用內核,不進行任何 KPM 修改 + + 使用者態掃描應用列表 + 開啟後將使用使用者態掃描應用列表,提高穩定性 (因內核掃描應用列表出現卡死等問題可以嘗試開啟此選項) + 多使用者應用掃描 + 開啟後將掃描所有使用者的應用,包括工作資料等 + 設定失敗,請檢查許可權 + 設定失敗: %s + 清理執行環境 + 清理執行時檔案並停止掃描服務 + 您確定要清理執行環境嗎?這將停止掃描服務並刪除相關檔案 + 執行環境清理成功 + 執行環境清理失敗 + + 確認安裝 + 確認安裝 (%d 個檔案) + 確認安裝 + 模組 + 核心 + 未知型別 + 未知內核 + 未知檔案 + 版本 + 作者 + 描述 + 支援的裝置 + + SUS 映射 + 庫檔案路徑 + 新增 SUS 映射 + 編輯 SUS 映射 + SUS 映射新增成功: %1$s + SUS 映射已移除: %1$s + SUS 映射已更新: %1$s -> %2$s + 未設定 SUS 映射 + 重設 SUS 映射 + 這將移除所有已設定的 SUS 映射。此操作無法復原。 + 記憶體映射隱藏 + 隱藏 /proc/self/ 中各種映射裡的 mmap 真實檔案 + 從 /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap] 中隱藏記憶體映射的真實檔案路徑。請注意:此功能不支援隱藏匿名記憶體映射,也無法隱藏由注入庫本身產生的內聯勾子 (Inline Hooks) 或 PLT 勾子 (PLT Hooks)。 + 重要提示:對於具備完善注入偵測機制的應用程式,此功能可能無法有效繞過偵測。 + 首先透過 ps -enf 尋找目標應用程式的 PID 和 UID,然後檢查 /proc/<pid>/maps 中的相關路徑,並與 /proc/1/mountinfo 中的裝置號碼進行比對以確保一致性。只有當裝置號碼一致時,隱藏映射才能正常運作。 + + diff --git a/manager/app/src/main/res/values/strings.xml b/manager/app/src/main/res/values/strings.xml index f796ada..89836d7 100644 --- a/manager/app/src/main/res/values/strings.xml +++ b/manager/app/src/main/res/values/strings.xml @@ -65,7 +65,6 @@ The current KernelSU version %s is too low for the manager to work properly. Please upgrade to version %s 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 - Disable kprobes hook Enabling this option will allow KernelSU to restore any modified files by the modules for this app Domain Rules @@ -112,6 +111,9 @@ 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? Next + Select partition + Use local LKM file + Only .ko files are supported %1$s partition image is recommended (Unstable) Select KMI @@ -165,7 +167,14 @@ Device model Granting superuser to %s is not allowed Disable su compatibility - Temporarily disable any applications from obtaining root privileges via the ⁠su command (Existing root processes will not be affected) + Disable the ability of any app to gain root privileges via the ⁠su command (existing root processes won\'t be affected). + Disable kernel umount + Disable kernel-level umount behavior controlled by KernelSU. + Enable enhanced security + Enable stricter security policies. + Default + Temporarily enable + Permanently enable Sure you want to install the following %1$d modules? \n\n%2$s More settings SELinux @@ -190,6 +199,7 @@ Light Dark Manual Hook + Inline Hook Dynamic colours Dynamic colours using system themes Choose a theme colour @@ -240,7 +250,6 @@ Incorrect file type! Please select .kpm file Uninstall The following KPM will be uninstalled: %s - Disable kprobes hook created by KernelSU, using inline hooks instead, which is similar to non-GKI kernel hooking method Use two fingers to zoom the image, and one finger to drag it to adjust the position Reprovision @@ -314,9 +323,8 @@ Application needs to be restarted to apply the new DPI settings, does not affect the system status bar or other applications DPI has been set to %1$d, effective after restarting the application - App Language - Follow System - Language changed, restarting to apply changes + App Language + Follow System Card Darkness Adjustment error code @@ -413,8 +421,6 @@ Try to umount path added successfully: %s Attempted umount path save succeeded: %s - Confirm Run Try Umount - This will immediately execute all configured try umount operations. Are you sure you want to continue? Reset SUS Paths This will clear all SUS path configurations. Are you sure you want to continue? @@ -445,7 +451,6 @@ Auto Bind Mount Auto Try Umount Bind Mount Hide KSU SUSFS Symbols - Magic Mount Support SUS Kstat Support SUS SU mode switching function @@ -513,7 +518,6 @@ Current setting: %s Hide for all processes Hide only for non-KSU processes - Run Kernel Version Concise Mode Enable or disable the clean mode displayed by the SukiSU kernel version Android Data path has been set to: %s @@ -564,6 +568,8 @@ Other App Add App Path + SuSFS library version mismatch, kernel: %1$s vs manager: %2$s, It is recommended to update the kernel or manager + Warning Search Apps %1$d apps selected %1$d apps already added @@ -643,20 +649,15 @@ Important Note:\n Preparing KPM tools Applying KPM patch Undoing KPM patch - KPM tools prepared Found Image file: %s KPM patch applied successfully KPM patch undone successfully File repacked successfully - Failed to extract kptools - Failed to extract kpimg file - Failed to prepare KPM tools: %s Failed to extract zip file Image file not found KPM patch failed KPM undo patch failed - Failed to repack zip file KPM patch operation failed: %s Follow Kernel @@ -673,5 +674,91 @@ Important Note:\n Are you sure you want to clean the runtime environment? This will stop the scanner service and remove related files. Runtime environment cleaned successfully Failed to clean runtime environment - Error cleaning runtime environment: %s - \ No newline at end of file + + Confirm Installation + Confirm Installation (%d files) + Install + Module + Kernel + Unknown + Unknown Kernel + Unknown File + Version + Author + Description + Supported Devices + + SUS Maps + Library Path + /data/adb/modules/my_module/zygisk/arm64-v8a.so + Add SUS Map + Edit SUS Map + SUS map added successfully: %1$s + SUS map removed: %1$s + SUS map updated: %1$s -> %2$s + No SUS maps configured + Reset SUS Maps + This will remove all configured SUS maps. This action cannot be undone. + Memory Map Hiding + Hide the mmapped real file from various maps in /proc/self/ + Hide the real file paths of memory mappings from /proc/self/[maps|smaps|smaps_rollup|map_files|mem|pagemap]. Please note: This feature does not support hiding anonymous memory mappings, nor can it hide inline hooks or PLT hooks caused by the injected library itself. + Important Notice: For applications with well-implemented injection detection mechanisms, this feature may not effectively bypass detection. + First, find the target application\'s PID and UID using ps -enf, then check the relevant paths in /proc/<pid>/maps and compare the device numbers with those in /proc/1/mountinfo to ensure consistency. Only when the device numbers match can the map hiding function work properly. + + Log Viewer + Back + Search + Clear Logs + Are you sure you want to clear the selected log file? This action cannot be undone. + Logs cleared successfully + Select Log File + Current Log + Old Log + Filter by Type + All Types + Showing %1$d of %2$d entries + No logs found + No matching logs found + Refresh + Raw Log + Search by UID, command, or details… + Clear search + View Usage Logs + View KernelSU superuser access logs + Exclude sub-types + Current App + Page: %1$d/%2$d | Total logs: %3$d + Too many logs, showing only the latest %1$d entries + Load More Logs + All logs displayed + + Confirm Uninstallation SukiSU Manager? + Proceeding with the uninstallation will not affect the core functionality of your root access. The root is designed to operate independently of this manager. + + The current manager is incompatible with this kernel! Please upgrade the kernel to version %2$d or higher (currently %1$d) + + Umount Path Management + Manage kernel unmount paths + A reboot is required for changes to take effect. The system will apply the new configuration on the next boot. + Add Umount Path + Mount Path + Check Mount Type + Check if it is an overlay type + Unmount Flags + 0=Normal unmount, 8=MNT_DETACH, -1=Auto + Flags + Default Entry + Confirm Delete + Are you sure you want to delete the path %s? + Path added, will take effect after reboot + Path removed, will take effect after reboot + Operation failed + Confirm Action + Are you sure you want to clear all custom paths? (Default paths will be preserved) + Custom paths cleared + Clear Custom Paths + Apply Configuration + Configuration applied to kernel + MNT_DETACH + Contains %d apps + diff --git a/manager/build.gradle.kts b/manager/build.gradle.kts index 72ec98b..396caec 100644 --- a/manager/build.gradle.kts +++ b/manager/build.gradle.kts @@ -29,10 +29,12 @@ cmaker { val androidMinSdkVersion = 26 val androidTargetSdkVersion = 36 val androidCompileSdkVersion = 36 -val androidCompileNdkVersion = "28.0.13004108" +val androidBuildToolsVersion = "36.1.0" +val androidCompileNdkVersion by extra(libs.versions.ndk.get()) +val androidCmakeVersion by extra("3.22.0+") val androidSourceCompatibility = JavaVersion.VERSION_21 val androidTargetCompatibility = JavaVersion.VERSION_21 -val managerVersionCode by extra(1 * 10000 + getGitCommitCount() + 700) +val managerVersionCode by extra(4 * 10000 + getGitCommitCount() - 2815) val managerVersionName by extra(getGitDescribe()) fun getGitCommitCount(): Int { @@ -52,6 +54,7 @@ subprojects { extensions.configure(CommonExtension::class.java) { compileSdk = androidCompileSdkVersion ndkVersion = androidCompileNdkVersion + buildToolsVersion = androidBuildToolsVersion defaultConfig { minSdk = androidMinSdkVersion @@ -76,4 +79,4 @@ subprojects { } } } -} \ No newline at end of file +} diff --git a/manager/gradle/libs.versions.toml b/manager/gradle/libs.versions.toml index d846e97..d9e7dd2 100644 --- a/manager/gradle/libs.versions.toml +++ b/manager/gradle/libs.versions.toml @@ -1,16 +1,16 @@ [versions] accompanist-drawablepainter = "0.37.3" -agp = "8.13.0" +agp = "8.13.1" gson = "2.13.2" -kotlin = "2.2.20" -ksp = "2.2.20-2.0.2" -compose-bom = "2025.09.00" +kotlin = "2.2.21" +ksp = "2.2.21-2.0.4" +compose-bom = "2025.11.00" lifecycle = "2.9.4" -navigation = "2.9.4" +navigation = "2.9.6" activity-compose = "1.11.0" kotlinx-coroutines = "1.10.2" coil-compose = "2.7.0" -compose-destination = "2.2.0" +compose-destination = "2.3.0" sheets-compose-dialogs = "1.3.0" markdown = "4.6.2" webkit = "1.14.0" @@ -19,11 +19,13 @@ parcelablelist = "2.0.1" libsu = "6.0.0" apksign = "1.4" cmaker = "1.2" -compose-material = "1.9.1" -compose-material3 = "1.3.2" -compose-ui = "1.9.1" +compose-material = "1.9.4" +compose-material3 = "1.4.0" +compose-ui = "1.9.4" documentfile = "1.1.0" mmrl = "2bb00b3c2b" +ndk = "29.0.13599879-beta2" +foundation = "1.9.4" [plugins] agp-app = { id = "com.android.application", version.ref = "agp" } @@ -81,11 +83,12 @@ sheet-compose-dialogs-input = { group = "com.maxkeppeler.sheets-compose-dialogs" markdown = { group = "io.noties.markwon", name = "core", version.ref = "markdown" } -lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version = "29.0.13599879-beta2" } +lsposed-cxx = { module = "org.lsposed.libcxx:libcxx", version.ref = "ndk" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "documentfile" } mmrl-webui = { group = "com.github.MMRLApp.MMRL", name = "webui", version.ref = "mmrl" } mmrl-platform = { group = "com.github.MMRLApp.MMRL", name = "platform", version.ref = "mmrl" } mmrl-ui = { group = "com.github.MMRLApp.MMRL", name = "ui", version.ref = "mmrl" } -mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } \ No newline at end of file +mmrl-hidden-api = { group = "com.github.MMRLApp.MMRL", name = "hidden-api", version.ref = "mmrl" } +androidx-compose-foundation = { group = "androidx.compose.foundation", name = "foundation", version.ref = "foundation" } \ No newline at end of file diff --git a/manager/gradle/wrapper/gradle-wrapper.properties b/manager/gradle/wrapper/gradle-wrapper.properties index 2a84e18..bad7c24 100644 --- a/manager/gradle/wrapper/gradle-wrapper.properties +++ b/manager/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.0.0-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.0-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/manager/gradlew b/manager/gradlew index 23d15a9..c27469b 100755 --- a/manager/gradlew +++ b/manager/gradlew @@ -114,8 +114,6 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH="\\\"\\\"" - # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then @@ -172,7 +170,6 @@ fi # For Cygwin or MSYS, switch paths to Windows format before running java if "$cygwin" || "$msys" ; then APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) JAVACMD=$( cygpath --unix "$JAVACMD" ) @@ -212,7 +209,6 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" diff --git a/manager/gradlew.bat b/manager/gradlew.bat index 5eed7ee..8747509 100644 --- a/manager/gradlew.bat +++ b/manager/gradlew.bat @@ -70,11 +70,9 @@ goto fail :execute @rem Setup the command line -set CLASSPATH= - @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/manager/randomizer b/manager/randomizer new file mode 100644 index 0000000..2cd4e4b --- /dev/null +++ b/manager/randomizer @@ -0,0 +1,31 @@ +#! /usr/bin/env bash + +# Generate 3 random lowercase words (6 letters each) +word1=$(tr -dc 'a-z' -#include -#include -#include -#include -#include - -#define KERNEL_SU_OPTION 0xDEADBEEF -#define KSU_OPTIONS 0xdeadbeef - -// KPM控制代码 -#define CMD_KPM_CONTROL 28 -#define CMD_KPM_CONTROL_MAX 7 - -// 控制代码 -// prctl(xxx, 28, "PATH", "ARGS") -// success return 0, error return -N -#define SUKISU_KPM_LOAD 28 - -// prctl(xxx, 29, "NAME") -// success return 0, error return -N -#define SUKISU_KPM_UNLOAD 29 - -// num = prctl(xxx, 30) -// error return -N -// success return +num or 0 -#define SUKISU_KPM_NUM 30 - -// prctl(xxx, 31, Buffer, BufferSize) -// success return +out, error return -N -#define SUKISU_KPM_LIST 31 - -// prctl(xxx, 32, "NAME", Buffer[256]) -// success return +out, error return -N -#define SUKISU_KPM_INFO 32 - -// prctl(xxx, 33, "NAME", "ARGS") -// success return KPM's result value -// error return -N -#define SUKISU_KPM_CONTROL 33 - -// prctl(xxx, 34, buffer, bufferSize) -// success return KPM's result value -// error return -N -#define SUKISU_KPM_VERSION 34 - -#define CONTROL_CODE(n) (n) - -void print_usage(const char *prog) { - printf("Usage: %s [args]\n", prog); - printf("Commands:\n"); - printf(" load Load a KPM module\n"); - printf(" unload Unload a KPM module\n"); - printf(" num Get number of loaded modules\n"); - printf(" list List loaded KPM modules\n"); - printf(" info Get info of a KPM module\n"); - printf(" control Send control command to a KPM module\n"); - printf(" version Print KPM Loader version\n"); -} - -int main(int argc, char *argv[]) { - if (argc < 2) { - print_usage(argv[0]); - return 1; - } - - int ret = -1; - int out = -1; // 存储返回值 - - if (strcmp(argv[1], "load") == 0 && argc >= 3) { - // 加载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LOAD), argv[2], (argc > 3 ? argv[3] : NULL), &out); - if(out > 0) { - printf("Success"); - } - } else if (strcmp(argv[1], "unload") == 0 && argc >= 3) { - // 卸载 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_UNLOAD), argv[2], NULL, &out); - } else if (strcmp(argv[1], "num") == 0) { - // 获取加载的 KPM 数量 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_NUM), NULL, NULL, &out); - printf("%d", out); - return 0; - } else if (strcmp(argv[1], "list") == 0) { - // 获取模块列表 - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_LIST), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else if (strcmp(argv[1], "info") == 0 && argc >= 3) { - // 获取指定模块信息 - char buffer[256] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_INFO), argv[2], buffer, &out); - if (out >= 0) { - printf("%s\n", buffer); - } - } else if (strcmp(argv[1], "control") == 0 && argc >= 4) { - // 控制 KPM 模块 - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_CONTROL), argv[2], argv[3], &out); - } else if (strcmp(argv[1], "version") == 0) { - char buffer[1024] = {0}; - ret = prctl(KSU_OPTIONS, CONTROL_CODE(SUKISU_KPM_VERSION), buffer, sizeof(buffer), &out); - if (out >= 0) { - printf("%s", buffer); - } - } else { - print_usage(argv[0]); - return 1; - } - - if (out < 0) { - printf("Error: %s\n", strerror(-out)); - return -1; - } - - return 0; -} diff --git a/userspace/ksud/Cargo.lock b/userspace/ksud/Cargo.lock index 67aaca6..83061a4 100644 --- a/userspace/ksud/Cargo.lock +++ b/userspace/ksud/Cargo.lock @@ -594,16 +594,6 @@ dependencies = [ "miniz_oxide", ] -[[package]] -name = "fs4" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8640e34b88f7652208ce9e88b1a37a2ae95227d84abec377ccd3c5cfeb141ed4" -dependencies = [ - "rustix 1.0.7", - "windows-sys 0.59.0", -] - [[package]] name = "fsevent-sys" version = "4.1.0" @@ -835,6 +825,42 @@ dependencies = [ "libc", ] +[[package]] +name = "ksud" +version = "0.1.0" +dependencies = [ + "android-properties", + "android_logger", + "anyhow", + "chrono", + "clap", + "const_format", + "derive-new", + "encoding_rs", + "env_logger", + "extattr", + "getopts", + "humansize", + "is_executable", + "java-properties", + "jwalk", + "libc", + "log", + "nom", + "notify", + "regex-lite", + "rust-embed", + "rustix 0.38.34", + "serde", + "serde_json", + "sha1", + "sha256", + "tempfile", + "which", + "zip", + "zip-extensions", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -1048,31 +1074,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "procfs" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "flate2", - "hex", - "procfs-core", - "rustix 0.38.44", -] - -[[package]] -name = "procfs-core" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" -dependencies = [ - "bitflags 2.8.0", - "chrono", - "hex", -] - [[package]] name = "quote" version = "1.0.40" @@ -1184,19 +1185,6 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "rustix" -version = "0.38.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" -dependencies = [ - "bitflags 2.8.0", - "errno 0.3.10", - "libc", - "linux-raw-sys 0.4.15", - "windows-sys 0.59.0", -] - [[package]] name = "rustix" version = "1.0.7" @@ -1837,43 +1825,6 @@ dependencies = [ "lzma-sys", ] -[[package]] -name = "zakozako" -version = "0.1.0" -dependencies = [ - "android-properties", - "android_logger", - "anyhow", - "chrono", - "clap", - "const_format", - "derive-new", - "encoding_rs", - "env_logger", - "extattr", - "fs4", - "getopts", - "humansize", - "is_executable", - "java-properties", - "jwalk", - "libc", - "log", - "nom", - "notify", - "procfs", - "regex-lite", - "rust-embed", - "rustix 0.38.34", - "serde_json", - "sha1", - "sha256", - "tempfile", - "which", - "zip", - "zip-extensions", -] - [[package]] name = "zerocopy" version = "0.8.25" diff --git a/userspace/ksud/Cargo.toml b/userspace/ksud/Cargo.toml index e9abb9c..d056720 100644 --- a/userspace/ksud/Cargo.toml +++ b/userspace/ksud/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "zakozako" +name = "ksud" version = "0.1.0" edition = "2024" @@ -45,7 +45,7 @@ sha1 = "0.10" tempfile = "3" chrono = "0.4" regex-lite = "0.1" -fs4 = "0.13" +serde = { version = "1.0", features = ["derive"] } [target.'cfg(any(target_os = "android", target_os = "linux"))'.dependencies] rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", features = [ @@ -53,7 +53,6 @@ rustix = { git = "https://github.com/Kernel-SU/rustix.git", branch = "main", fea ] } # some android specific dependencies which compiles under unix are also listed here for convenience of coding android-properties = { version = "0.2", features = ["bionic-deprecated"] } -procfs = "0.17" [target.'cfg(target_os = "android")'.dependencies] android_logger = { version = "0.14", default-features = false } @@ -64,3 +63,4 @@ codegen-units = 1 lto = "fat" opt-level = 3 strip = true +split-debuginfo = "unpacked" diff --git a/userspace/ksud/bin/aarch64/ksuinit b/userspace/ksud/bin/aarch64/ksuinit index 8d3ba057f9de3933e8e4e507d9b6a3de6e653fa5..df67742fb53313e0405a6d7aa2ce31b4e208514c 100755 GIT binary patch literal 421792 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

&ok=R}NPB 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 z zSJ$^0TO?lX-<9vKe9}2DrMOzE1)XY=(|P}T^L8$w^L^)9ul1fW{eJ#izCSWDa{vBY z_lMk%`-k@{k6ZUM`8^(F?oY%`<;4B_?QB{|Y{>rocj4)d=Kc6f|K?*AvP2}$+|S?c z-~Y0lca~BFU06lG(R7``Z>VqrGP>FS5SxFCvt;>Qal39_8#L?oiT3WVNDuc9)B0Cd z>=@I#wRzF~#d&F2Nl!gAV>Y$}W7 z_i~yL;UgF;7vi);c1m`(ke!_!7tgY@2Mz0+JtQLyxBc_-BPMiD%+3z$nJtbPh$p|{ zCuV1d1WD1^IkDN<`t0o9h1uF{Ll!T{pN5^Wz^3*8obu|6TeJ3r57_*{Q$N0QdDGMn z>K;wqp#Nyz?8oY-{qRZPOMeeLmV0aAzW1%)=QJ#~ztnBZyOVDG^Wt~D?O9Vd_3aM_ zKT|xhe&9Yw%KG*{)f@@SO>Ft@r;Qo6=T!XI_U#8ZR!rUS=Bf)%CoZo`37)v+&O1zZ zt$p~!=U>IgF3}dHzOYLB^?%~((*E}MsPl_wk1E?TJAR%?zur*3vFtA!c6~GS;oZAp zYHsa&``2sx1$Un~?-twLuM}PvH6v-@z56H6T^jfO=lUBg&9k2#z0~5}nb`ul1w zxa!T8xl#rG)H}@KTs75}YWp0i+-bJYu}L<& zRjRgIZH-dH<1K*+#OkC(T9clg|?ZoQ1Vj(m9Y_KJ5x zdJg`};JqLBy^tNVv+eYcFMWL7s%^bn7FXNnn`^4Al53&8-rTs<+g@{p#cHek_q#j( zx$dUFuOGDat{Wy=>z}N>>EMTP(@t9&bGOW#*Kaj!6*~ST-=Dc&inrP-8s?a4%@(I4 z-nsyR-`6j!v$^Pg_OiwEGoS9>^Y-kuudNte5>;C7tgf5pv{hC&7C4;_r^R8fw>8$g z91Tv3t){xdX?8A5FxS*LED0{BC84siI>BOc)jQ*zF4o2C+Io7CQ01t#B{&=GcAGOH z-r3-)Z>USCcQ|S+Rpx5DD*=;MQDYm~n2}~mOBvZ%wl_46oMUfDK*7lE zs;4W}N^yF~8{?|C@`soROorW(;ILb436%|K_Wya^7KhWuE3d9c&j4}N^{xbpo9i8X zNd8ZI?wtlq0HFEG?pn;wIgSLUZB8{(ERFwz*G7@v{Ivs899i~pOVZ3JV!u;Up~RGLuhuy*;5Rxifg;jB+^RhgYO z06;AUgC2C5zQJB?aae5$br>ZMYF(zPGb7ntWgJ!L^0Ws7m{8+zxfW7h4uOFFjHJwT zTXL$=oRVlUrX**YQ_|8a@Jq``&rHn7NK8quOiipv_Q0xl#w?DS8k?oQ+5zydsCU|I z3AN_B1e+bx^nX=>9Yf$LJHcwJb0y5FuC1vanHZm(6faM;(`L3Nm@6urw)s41hlTrF zrrZU*Q&WQ~jA|8iRL9i{pHp3*V5zcM<~ka%lIA!nfCNU`Qmi(M*=n-^m1iaz%?ZZj z3~NfFyzFf?l?g6+Bu!N)+1ACnlNR8qM@=vtq^t}PSawiD{iPM6I@R5fT8Q!VHR zmkE>(r+Pa#f{0^>iwO0EiiJe^`H<_srcRXWGFfX(GL369)m2&L{q{Fbr_ajskg%QG z{__JUj|HfJ-`piBG;+ANX|NEjq;h5&z zKz7Jj>$eu@nHhUSZNdVlxvoy-UI~r-9FpZinyw(GDHMUwb&&+Fd z+Ugseb~My%mz$0j+ZyY@h}o<)3$M9lwgsjdn?1&07-w#9p|pyHSq4LCCmEih)H9P3 z1`rk7lpaIm>c>V3f*|6bFd;CpN0?yq6ZN7fXa@@e``s9r6%Ziw)YX#X?$W40>wN7*VdDFj_N2C-{#Mg2a>njG0yxOd3&(VG;f)X#9Kdv6bf` z#80CeCt!#O95h;VE<~K}Po3;VBP~A%tei%R^|oA0r$6)e1TmAc+4G3|g(Q z4)tpV$q=pQbEg+I@t6YUhhY**dZKA4W04-kV-^5^R1qAa1d1s-ou(Ihs}E&C(2?mz z`7vRv_Bucl`W`R#Vmgi9-(U0_sJ&0a(zQwcLWs~)hi}mN3E{j{omJR`JfpQt`xEM` z^<(AkGb}MN(U_Q&n4FlBn3|ZDn4XxCm}yKj8jVTDWMhgk)tF{XH)a?!lM<7RNl8h` zNhwLGNoh&xNsv<|Cng({laiB@Q<77Y(~{GZ!6m08rWjL_Qj$|rQc_dWQqog0QZiE$ zQ;n%fsmZA+si~=Hsp+X1shMesX~wjqwB)puwA8e;wDh!$w9NEGsEL!(lhaeuQ`6I+ z9)`#(BQe96k(7~~k&=;`0U20EMn+~PnwW{|A!bBDnMjnG8E1$#NQ(^^tp87cgy;Ud zA3^)of1TBfTKf4^cw>Bg{7n@Q&f2By(Od@+%_v4c(knm$V{w5=n3-ErTvS{*HAWgO zRmf6vXSFdYqXEHHRb4BYT~cjBy#!H}Bvrd4Y2+BG-q~QQbU3q3rdh?be?YEjorHjM ziLA1rrly<{+8y%@3))sUq0Kz@V4RT_#;DQ%To zBd6v$T@q9g7blH~P24oXv86mte4;IG;E!!1*-uj1L+#~hmxU8) zf*~q>NbrgDZ6W5&gx)7I6Z@Z-`2D~W#W^XPO9q-xl%Bc8TsGBlqU?{any0h(%rk69 zHqT(6M9pMvo3DTBWYny$KR68m=c8DZH?4A>5sU)1^eiT?fpqCpoZhG@fp zsv>mV`ga$4h`q!J6B=*!rY5M7eoDA(G zU4qu&7vPs84c7(*q-nBref_jSe$$H5lY^7};{6T!VVVK{qWf@3m?k~WKelJzVg0-J z3MfWZ`N4hs4EhQF!vh)u^Tx#LN9hdu>-2&yOrs0D!CxFuF>r#v!M)|?K?Q*ZeQ>ud zy+Pl-f1I|L`=xAaY0!iKLt#O`3I3(QMSceN^MgVR`a;71&G@2pO$chu^fNT2_3;~} z88BT4O$uJIp|T;+{l;CBEx{`j!+IL-*`QlKzU8H?+uvHA=@+Y=tsiD6G(_vVH9eE% ze(OftByFZ&cn(3%1Lyr$ei&nT_@7P5p~3)th}OSp&7Io0x?oL!U)Vk6?u+`Sos-80 z)MvZ@ZE*S5^(b7_J*a!o%z!@bJDSF8Zp#bpv2t3ZUhn>3glu>?hi+3g{C(5e_|$U4O($Uc>d(k?!DQ1K|5X7FGXw$iPKtxW*FR0Wef<8(+2p7 zA$oVqijTG7nqbWWtw|rG6+(lw8R&1cf1Fkv(^M8TKx5El`1RKZ__;5x)PoAr>GXQh zPw(#+5N_xn*e9rOa9BuCs5VR!5z#H6r_f8=Tj-(@`{FGlv1G$S-41LK86tx-%8 z9@lOaTeaK#FN#0vE{i{D+5@&XE?j;0!-+GmU%h5s|G$NVPMZAVrTB!gvu`q;TzU7} z_03zKefgC)-+Jf3-#`DNooV?3NY5HIx@h7}E7#)X&X-?#>%gJ+KL3Jwmd7YsA2(ZV zE7x!M<2#4m3l5LT8dXp<^TydXo2<6G*Kb9ZH{Ut_`4?w`!wZV6HuuUMufD$PgOARf zU2)s$N4LDb>&>^{`{dIJ55D&2w+_8mR6KR&4L6(aTzk*6&%dy1&s%SQ6yCGfjkEv# zpP$;@we$XdJS5WY=s&=;^wy`Ip7P48J$ns|EF51vl@`gZ%bt7l{SW?n=IpOjkdobK;y6R(}0NA1Zj1eK)n#sLs#tA&u=D}gI+=uvOKaS$ZTe)GUyx4c-V)aIFF!$w)#L5mTKDUkewxr=63y4v)nglztktv$ zf}fHW*_;la7ubL-4J1mI>Z@#$6S800ubk3?YHYSOt0PjwQ6CJkqEM7QvMIt3)=ud~&@zPX<{UxTgNMUTJm6`HyD9)Wy zG)^L_M~Ze-R=RBUl1ecVtw_WckyS)V>F}d>e1E0R$PGbdKCSlb{Pk7XiY;b4(LAy& zV0I%iU&$9=`Gi-(*q3yTkH3<)wOQxb;;U@cbE@j4(W7N1T)7=78Lvzay-7UA29ym_ zTV3_?&6Vl8Dwiv9y@jhnfrk3Z3^iUbxe!x>;-oT%L*kloWk6%35fX_gq-aHBD#;pC zmpTRcWv1eSg8YJfN!CbZWkH~jm8IxFe_084t$Hl4QZ-Jhgy_PxNTR{Tuk+}4zL!&B z(wV#MTGFfbF=~3>K>|%y4S=v=r1EpiawRf~k*1hyK-$%UMuV&ak{U03))Jj7L1a~7 zb7o12jd@%LL%jR0XLf*vA?e2Wf6XB2AuFmf}-+TMP+9)qgEz~v(C7Nwn zo#67UnCY?wjZ8)oEzeT{AAoR|XFN-WkSvJ^Q)LjmJp~wyFP?6ikXt$-3#$t{&U%*# z9Um=0IYq*yYCAZQ8V6DCSgE5GRj~?9rCh@c!Iiv>3F?%k43O`P3;2*pgJrEm2fQ!= zQgVq)L`mw?QEYZomNcoLqMbpZ?+f zk4&$^-P@TTm)!UDum>Jb|8dOTQ?`jWJ^XpW&Owp)-XzQlUAy=|_;Wqma@%arcAt}R z`|JNPeZBfW#{(ZfU6C=YeQ?uPd_htaQKj@rtAKXPF*2uwu$b&y9cc!u|D@A+1Ya%u86Fzx%O2)fl!7 z?f3om(B7fiHQnFnvpz2TNah>gADdyhxjcH@$;n&0oplWP!8s`*IHJ^Eb6?)dIUDD? z8b~Fal|_GjRL==k+x&#NwXQk*&zAln$*VIDeO8~^^OYxl+Irwa{nxW;pxYZiA^Dm%1Vnzir*gGQ-UOyfM7{t1r(9{_*^f z8E?K2+?+n{pIL1WR=@he)Xo2TKKq5=zkNc=b?qKdx%ZW0ul}&=)JKlL4((h1eCpHN z_PUeqOu8I+{@G1QQq>)Q34Y~F(T}GartMhp>g^v~+Ph@dFzMidk?rw&&0B6>@cWMk zy}0A;Lm&N6v+dLQ`z}}g6x#Q|C!a*Ec)Xx>?R$Uw_eay-8uL-42XmBin->{9V4ao{ zX83sL^u@=2*S&t#M^lqu_)FDKy`vu}ZhpJpU$%@o_J@X73ht9P8c>8qJ* z8-F;z^`5DzrCXfco;?2jdv||v*NYm@49g1l4yyN$_8h%w-1x6wG=H=r@%{(TuN?Ts zi#I&8vfmNwvKM#mn)2kkseiw;?&*6U9kO%g&bhOs-Mx=*-E!NrrN(h3k3F4xxL`+* z_XXXhZ8Hvh`sm9>KF$_w^b#z`8c5s=HX1Tthyx@W5TBJ8>*DE?L0CYGI0U`RtA^^> z4l%$~zFQ+wWQT?tFjq7cc#2Gd=@%eh;Kon>sn1nB_dKKryAbvxl=(4s4A-0Ik6+OL zY}w|7=!bJ!;6po>)WRm z78aD0OfAVT$eTXCtR#0_0mQ11i|Zj&_D72iERY4`v_UxQ#_&xv7Rd&)5wPYR#m2Ex zW??S2f<40avCkMzB9UZI3=5B7;0pBMkhMXYU@-(DJ$faV>xVLfQCc4s1?T0t%*>kD z9wrD)BtIn4svd&VAR$bEj9N$(a)feFJZRCClmVzIQ=P*F#F{K+6Ky3+P_9NuM9YE| zHA{>%jOT$70mt944t34FmU6t+8m}dJ$7+rLgKCq0eYHu~SS^vaS8Ps+*Q%bm)^w0@ z)jPAYxX_DewrFVqgmm4I8N{WV*NgCH+cbO|~(jaMMr9HaRT;sAWmKICWQiviG z8_Qv&nU%%ugv(t-0X5V>g#$vo7QpGMCfyaW6-4_(w8aw_g0|uoMCHTBNb&Kbr6p37 z9rDQdnwt5w@ryad(6N=il}I#jQh6!5IGMU)vliCY=USl?vN_B7P+F^9FnR-)s%x+> zB*Ew60&qkN6#_TE8b*NA94?jpD7MW}qqD&6Oe2emB6~gWk*AV^#wsYMP*)Bo5EXi_ zqIwR=CW+vyFk4juhoaSl|0F3II1>~i?92&^*4do3E}%XY-zjJi*g^iOSM|BX*eQx< z8L|+)G9P>=)8i|Cl9EpvNm69|vK4b`I?#h(=lVO}%c;C{>36$c<3s^$EMaI#HYN!T zE?C0C5}ONn6XGixs%xwxtE~y0U&sPrvusT2H9fa9m^h;rFTbz0SNa;JbLFcyF_}qr zxT}FBGdEmUvcs^wQwdJkGIdIX72{J1?o2X0>RV8~+AJ8HLremrc{F7%(%Q_m;EJtW zSg!1aL|Eg=^s>T{8D;_j0%_uixdr$yt4jG11mak+nr+USg_N~pO|n9l@B!GP8k|{h zU@gf;U*1>TIIhFHsjgaZGv+xq4#DF9wJ^LV6Kk25;eFsZgut;HOB6T(TtT(Xik{gO z5kHLoNf+DU3Hb0T2hf6#TH0=_U*K@g^?l@%#ascR$7UsNTX|};66;CK7WffVpx$vM z#C#~QN!P6wYK18%>_K5%Db-ci*_7s+YcLzug}|3I!47mn)&eY$5f7P~$SMtMH3|0} z&V^X4B-2ptJP|$+Bq~`*1&(ysp$Fi6C_l^r1pubQ;lwyfU<9ihYJJ{;>#3szfMH&& z7p+Fy;^)K@FGhePQMwo?0WfcBr>GR*ce}O5rm~?ju7H}tTx$akQy*K+q-G~36u48K zW|GF(!DcG$a)BYTxooz%q&EPPMvU7EP#yCcpvJ%;S!^~SLS8Fzs5Kzzu!$&2mWl?~ zLR%wt2ARX+spy#%1407{8rJ24ZQy(uk6_gqV4c&!JxA`IduDvx#!L~CueG?#z{vPlLAIk0$~vfTkp z03pnp<8We60MAaqUW>IkhIBHuSOAg>MhVrG3rQ0Lc@-W&!$O79%*RQ5aO0?NutEVZ zESW7BS6HIjFacPWcmb&w)j@Yy>@WWi)auOuvCJJJ9%YHCckTQR zx>Nd*VpeT}1e${eoOHDY(r!XAiUCw4iNI~d96{m;>8Z;`3mFU4l_gDv?Hdg-h7Nru zlO3421Aeid91Sj@n+hN`Nw%W1S!Dp1xXqS~(g2_`kEK|;7&>;J-%T#c^A3nSkXIcp zV>#B<{A%D3&eY9^VwB?xE-IESe9YeQGk|I!Ftd({k%g&%2Um@i$b^YEFQv6y`%GsJl>%aFSW8ns?Z+S5!q3oP#sh#q(hU5=D-r;h8=kzFA6*)2c@7V z`DiM^);J6Is{Wm$P&D*y1^*uUsg0J>M$bcJqbJ} zC(5!@7rDx{saK8izVlUGt!h5iT2sBM?>pyH%T=#e!wsGDsr9JOuU5WVp4yHeB_aIf z^PhTE>t7CsC3hg)i|`NvecXa%fD}juNP+59<$_^|(=S}UK>8;C)N6!tufA97SLKUp zezhG2CB5nQ&2AuWGsKz1kmjKGgR^@WGKdglvQ<2(uC9A}mE%i_n7b2*RHbUPsuEFc}{J{Q%dK z2;U;KBZT9_k&y_g2)PK85y}v5La0J;A-ECNAUufBim(G=Hv+zC)&42Mw+I&yv~U_9 zj?f=r7{Ukyb-p@}zxrIgs^g)KpW07#{CZqvp48{+RUKb7pIYuPC4Yu;?W0^(hbij( zsPnCkyV^cAom#GXRomHJDNmjEe#*U?PAx};H?_U$Jgd*u^3><*RqaPFtgqn+83+>* zW+B{yuoU4Qgg+qA2OM`H&<8y}L^zEA4~p%f2m=rj5oRE)MR*+Hb%X;5#}NL8@C||% zAGeG^kPt>9U`CiShmZz4l&VxG6YC5%CwY}>6tIyS|TA!Lvy{hG_`PF*V_i8`Y`BTeN zuWGsKz1pA7>pcY@$H_*RjBpb|4Z>oCyAd8jXhrxF!ajsU2p=QN#)lEU!1Wx$PY9v- zuu2p{0zv`8bqKQ&tO&ITjR-3c)*<`>;R%G75%Aru_QME&ML3P{69T@;)!rQ;3Ly?5 z4Pg|5Iu7c*sq>(YgL+lRO?|J9mpcAx{?6l~j+Z)4>b;s@El(|9osa*&&U-dK`kRkX zhG0XeM_7UIJA^F=I}lz$IEe5eLK}h=ADjFJ*NX^x`Zxwc3_?1>6oeZPtOzv-4G3<8 z)d&wFJc{rP!d`>}2p=PSj_^H#9xxP+&>tZNfnahB!Z-xa37jm+G@WyECgQwmZ6?S! zN+4at;qPc$qsdWm%jltXF>zoJtLKAhGZ{w@t+5)|)icF_7w0y$o+RdKmyPI9l?epL z3A16ZkObVfdVGa^mdMOymNe7_q4`jk3^_i})pEz%>WLaIvDM_6UAAb8>=}hxi!4`3 z6vG83p0hJHtAh4@>-EW61_8q_mhYSB*OXrf7P`Pr>K-uCh04*{=sJti1G!~g^gB-_ zdZTHeiNf{C;geqS%2?LHjWAsm+nLCV8V6A^zu z6mw0#tCfok&U#aI-RQ)|p*4-L9zs7x4|T;!H7=`JedwJU6%AaqyiV;@I_QCquG-@> zU*igMec?bFJqB7aw9DXIpHKRtX+=!CKMPKsm%&xXt*i2{3QoTSL_f3Mx~>Nx;4z5xj1Wh;KI8d| z<3S)&M`(N@siV9_(#-sta%el@GTpAAcW51-=wDNwFCch&ca=5tzbW@hnE$WK9SWsK zXB?R1#eu)3e06*~Op?5;Ow)6(K;3-7`;@P4Niv=0vm$6>I=v(!%s!ktTE zf_|C|`EUa^lv1vftG3Wq=eryFY@oa~9pH&p11TZ+4(mP1#aEBqtHj2_LK!{y!l6(7 zB{*#7u$K}Iag}Pj=zpoLetd~faw1pejHY?w$FvBgC)P9)5qQ;p7ftiY53_F%nb1`` zQD2AjUDnfKk)mF;lk#LS25eObQ$bLZ#x*z{i*8(nr))!zz1ScVUw4kg)z_itOu)0>$7u0&M{}jJa zK6L0ZU2U3HQdC@K%AHnJN^SMs2RJtb1YrW|ik4YfaKL;U00QJY!7zC%kYxc~<_BW? znf#ueMBPZbtlz`U__ksgu{R!${EB=;_trvM!8veGvab8b%O@3^H#tl~VRpv5i|b^Y z9pab<>~&Yv1IrN?M%-M36Wtw3kT=H8<17z)QSo>MFgm}_pP5@y*l`|ww~}~Um0XVr z4ycGI0MnfXLn}-;Ze%;36vco7IG#2GZcNK9z2?9uZRjFd(BRXLtCtV^654MdB5F|T zQm72Z6h~O&NU%`=GlXiJi}%Q*DmYtV7uBbpN z%2zFM$S6!6Kzgn;Q+;V9!z2X_Ggzj$Y)B-h{uOjU{sObJk{iZ4WlBIU!D+@e4QuSA z)KqEMFlmrX`0>+a*ql9^ylnqPO5j1>cFGi6$5Z`ky9)CQxTOsBOgUE1U(s`H<&Z+u zKzwJG)UH%QsD*bO;E`J@&>xuhkayTC1`{P-W!$ftV;m=M;pG_)bztaYc0!^EGa!#0 z4OG+Qt$?FD03iV!7t8={l#%Y3U}GVf6e}H_uCS#X&OKO?n-eRgbGtlQxYMz{1rsJf zWo~oIlY-NEHlka+wcyAWjL#~`XOJq*Wb*uDpo(CQM5j0b&MvZ0_S|$NV+rpwGe9qGv{0e+xiS(V910wri?bskd_$wPJ;f z=m4Kr^pmWH0r$VqLmAMN`aD@WSM+oJasiK<_|EjRL&+;r>hWgjRMF4XgF)vU!Ozw5 z+wv#*%y@_KP^(m1$-yH@+1n_kXP#i1hx48!2d%ex;YOW5^|3ELxnjOBTP8~-Txrps z27wgWvRwrpm89NgdEk&XUHpD-9F)vNX8huKD*fx6C0gFyJ^feT%gu-$s;q{t@d`68QVaigU_*G|f0hgV;+ z8&@Xin)Wst0H1oVRs@>iK_hDV+}x6J6Vg)TYT?w@tRh^cysNx7kx0i>XCW;n7Ylm! zPbHrWkMg(y=XO{se7=-)KJ-bKWQW>L1@Eg9;i-IvZu)?K(+I0s*Fw`-r!Sak*Bdht_nsAjPEl&}(eGFhwX4)`$n$5!N3*vEqaj_+rcv zCgaR+DWo-TuvMZFXMq2kf9dSM`xl7Q&Hv56nL?|$Q+Qa!r-1SEWuBEqf+J9LYzABs zozhT=5_#H4Zd;0zuGW$c(qJyn#S;1z@XPsAU#><5;zUkppcYcm$7FfbfNGl*c9fBl zdnS+wF9vOc-xLx+OJhdU0nYjuoZcjreZ0)y(%J%ht2SEUd=+YoZ&F^p1c5jm4Y>+u ztTf;k#|=(mA<^)Ro;q%wh3!80+^fC!t~d{(>N1_G<@Xg5@l!i=GMD9E;bs<=GSNeE z$b|>O+xWp_DCl55(o6|rWx3;PhfX{ZE$4I z{t|@Bv@tkhWOlQi73UVScs6nb zjteDTk9W~DRx$WCpTF=r!ezuL-qpW-p67i%F*P0KsfHw1&3~PeVhh?l29jG!_Y(fj z;IY1V8i0QqNOIeF3=e#MnjD`SMEm*PPy6t%{d4-j%R!$$eJB~Mul%pyXhB4+}hz1<>OFhQFToYC6wVHPNF7-8_E+ z}ggi6o_S_RlH9;A-0RxF_&1Z`3ox)Y{E^Nc3}j* z+PO^go>-&#M6+0U71;iJO^9mV@6(`;(I{=IcD#0mlFp%AZ_@_)F?OH!6(ru&p$y&= zJbg`jK>JVa1#NHmOe@rt>+HHUx--Iax`Vn;b>Ha%_4fz^^%?rfdNUUB6T+*)d%|CZ z75dHk7idxQ-#hwG^k3>P=-<{1^vm=s^1IpZPnth!iXFV){t#_}EH6HQLe6gr3~11B9MbHPxBs4P~DCvX^AzmQGy93N87QnRVBM38qr z%7+{IG$Zmzt_Dk$e25f#qUSua=M72VvB5Ap>!KWUW;4hcs8^86chmpJ-uuTla@F_V zGm>_9*48>&q6DQltRN0G;4FeD#SkUY?kYq9Cs7h77%-p^XQLET+{A!e+@fAwaEsfz zU2UQe?V=W!y2WkXR&CwZZQWv81HQ$*afkv+5a8art$XX|)_w4<<(>Qf&WxndSi1?& zbN@uIm;IjkobUOb-)GK@W=6&ZZF!){h$+tkbFuWKJljehnUeigv;#Y=P|{2+Rds;a%ep6J5XyS ztBTDoP;bQB8y+@SfYm*Z+<5fFK^~t7S!=uy6z2>ilJcw*P zSZ?$W<2hhCWt8Wu>n{Z3No!VI^dI9vd$V~Z=OR%e^XW*QQLR6EtQ?o?#f`xd<3XO( zuE~{ACssovv|-ah@?26#LOq%Wxzhb#VfvO_>dID7-;o!SXRXGG$#17Ihcwz&$7ZuYV7$9{-M=7aS~%y{gw_O=Hp3g71Q&I($YtNzM6jb^tb z7^|;-PAB(>K{qntv(bD=S)K|D~Tz(60)nfILID1}C98;A(n1d6r`?#u1!Cp8Ihu|hG z!0hMF>ub=PKd<-4RV4stU^iTXy)gWCRT+nCa2YD^P?h#yKd-mLLD&uFVGeG=IT-tm z^ZL|Xs?rY^U;&ok1`H-tCH(pGdIV0wez*unpm&ORzo9B|m|?#c&cY;IhSRWex2kmf z=6QX-5C3rRJ^VO5^u1SA)?pXy`~vmxKGOYd%KM zcU~|55&od})0F$0lou+c^ZFR{!me@Z8+v~cd)Nu1F#D&fGWR^?{|w0I+!Wf)^akv0`;418g>u?ZO;1KjhRAm@;z!BID$6ybfgh`l#!*Cjo z!vf5~1vm?r;UX-;B3y$Ta1$ywkzeSC+IOi(=!X%}VIQ3N9{%Av9EaXD{KEj8g&lAa zhF}p!;0BCCe*RtQg?=~$LvRxIz*(4rD{u^M!aVf-6a5pm!xh*G*I*cK!zlFLLcPHt z?1N!A0poB1_QNeW1pEId{^26b!euxE{r^nAhAFrK$DkG^AFuE0rrUwM_|{# z;0LDQ92|tpa0r&+1gyY07+@Yz{*`jWG)%!QxCs6KMt#Bj2I(=6DF1$5pMZXthaIr^ z>-3lZAe?bMvq^tAp(;zT1D0Sn?EQb}w{Y-(Grr(BoP&}7Jg;xUGVF?B|9$EOc5TzY zVK2;! zJ9^-D+8_49=oLHqC|rIy`I7yscJxg+4-?G$Gq2dur(ypqck~t5cZ7J%^J6dt`+__A zDjb08uF84g0U%(R-ls8p;8~Kf0smV6t;ZUxeA$?&urP|GFLBe+T0W zMxgigJNf_|gA=fD9qGXEQSyH$<$_&s>kZU991P(XX5UCTVBhsSdXVEzJiMbPVYr+6 zhDqq9N*E?FbQX11rA~7jZ^RJ_d+gCC`sstV=x3aVK3~vo%VtYFbhL(-_hq`9+qL? z&K=#?OFxETI0oZz4i3P;T|4?X+=5duo}hkV4i@1u+=3P8yN`CdoBYEhjKfJd02g5z zuD}c|!D+Y-=iwGyfm8R;Zg2)F_v8Ow>IJU%Qh%`WPRb9XeT)Z~fLYk_t{r^=7GMd^ z!%bL(o_ErYKTr8!=u`B6xcF(}!~8hoN%rA9?Dz%h1&%@w>w%eHr2XIuj6%<6cJzMO z1xH{QPQZSchug3SwF$}reNcH9>B4q61-sz_?19TL31@zp{J`L^;0K0b0Y=~wjKUI( z!%esh7w88)3)K6&=|3<8D=-05e?vV%?MviO_Tdc7zy&x1SK-hi{TEKb_V*AUMxgR# z{J6eXtC3Q0Zqrg+aIu!?5cslneI2QP>YB;Rc+7!LO1JI19^g z0dB!EjK7!m`Wo{9oL;8Cz-2fMeg8l|5go3{{tD^855KSzM*org!(Nz%DVTvva2k64 z2ki_yVG-tF1y*42{j~ej^fR~(Q?LR@VQ7_hfhjmI`@96A2#4SX9Q_94FG>DkCmenj zKX3&0!6{gwy|T|SKBgE~-(ns>kHc}8ggH0{XT=^a!V)Y(twcI743!5cAN0c!*a>qm z0vBKcuE7E5d!Bv@yWj-ugLya#3vdoD!6GcdZMX@&FVO!#Ks*?Lao7d>VH6I)UYLR@ zI0#4K)HeQLwnD#rka|!mdK{KvKb&<{^a(iXt>{azbXi5Oz`&IiJvu=7U@r{5yrO5} z99)E}S5@>)xOsI&Pdr4wdSyi)fHN=ym#?Yl8!&QhMejIC{MS_UZs_Z(=xNw-9qGaG zqZK{yF!_1|`GFmwiarj9-dNG+;n?*Ry*-7$A0r+deRD;hgTZdnhg)x{=%Gg_M}+ur z@TQ7B4AXENF2Wq_x}~D8!6@8@ebD>k*-aLGi z_JDC1_$%58J^nZNhZA3={OHlIRrEZ}{sZj{=l-#x_e|6Oo+ceQxLVPZkI~=0LHn>j zRwAFj&2gzhx^Q%tb|8F1+101n5ANI5E3gcszk{FsyZV-d!;WG4Q_HSCD7Kl^ojk~(%W7HQ6z+_}spMbHO zclA7soY>V9Gt4J%r+n-eVR%H2&%62nTzr^xeiuEptIx8ZePmZ(gs~yY59c1;)wQ3Z zpFXy$hv4u>clA-Y4JYB^DCrQt@=5Z;{@~A3A3seyeQH;a!0yjbUbq3X!Y6k1BFts+ z`*HgFle>C5?EEb0;iu;*(qUi8QEoU7C!zPuu098|umXK&clEAO+6_kGJRF0?&+qCz zzsGp{;;vq1e`tpMvET8#v7228=_Kc+vxu|L_>OR)RT zcJr?c5n+$62GuWx!CXhGVKnvzk}!tP@BbmiTd~icCZT$e1-g>`@c&2 z!!@`8yT3+1`vZ=T%lO024`<-gKhQtm$k*wAgiox{J~01}w0oL#|HrQ04Si2jkAFzH z;1JBMl27!$XYdE7i^M|@Jh!Vai~S4KBm3)Xl<()r-#<~`F!itWmp>xk|9e-TXJ7d* z>H#i8|9>O?xm~><_MXQt%)kXW4OijN1@iSt@~iLa!!WH}(5GNgy`V3`oa=&Kgj4&` zNhi^ILGK=;{N4+C0&0gY=v@WuF1w(Qvp)lKFy*_TFUvkuexCkz_=4_-!OJh`op1q0 zg#HWqF!2*t;E(;tl^67R_5&}!ppVHuoPi0r00-bI9ER&~6jopw`aVTJfZN0`Uv)w6 zV1M>UF6e!*`<0~k$FvU|V}Ao?;c)PRz6QtOFyS4qx}eX&{#WA%eFRq6@9ems$Nq%+ zy_Wbe^_mNM{?o*R1(=0PaP3Dg=q0!bH(>>O#;K>y3wi+hVH}3x0F1&#{B6Os*uD0G zo`G?=2zy}>CgBDgfyyt?4`iQs1Fxe#*-yg+9GCq+rQTkTfA$k_1P;InxCR%X=Q_#< zgRmd_O&I${+Uw{AeG~@XNO_>=O_U${q3<))1MGki*bPXVZ=qe-&%y#+fRpICW2DD^ z-|-8&_m{{wETfO#dO;7fKMmt>8xFzH+h}i?f(vjGcKtd2Z=*l4pMdKU4l6JXeZNe) zumjG(Znz8+unY&_CLDqOIQ0uVVID?d0Vd%R9D*e{3OC_6^!y6r5e8r$cEJLS!Zp|n zH(?5P-cGx~D4c`?a0+JO9Grp6Z~>O#D%^rwP@81_c>4vt9k#;|48s_V!ampo2jKur z!(o_#V{jT~;5?jyE3g39;1b-1tI+$a^h+3oTQCfL@1PxEFYJdYI0Q%G7@UMzn1wTN z3NFAcxC)gX+6DSx1qPw-*Jw}J0i&=RCSVWjhe?=%!*Cdm!!ekHlW-R1;36!*B3y(U za1|=2sDJ2(Td)&)?qL3aKA3=UH~${yS-Z*a0VD2u{H$oP%+=43n@7Q*aB8K$;WAu= zWw;Et;40LzF z56r?OEWly749DRb%)ttrh5maPFR%+1VGM4-eyBXfID>vT0XyLojKF!AfJ<-ymf;B8 zh7-`&OZ&r4Sb$-;1f#G7d*CMQgPt7i3j=TzcEK?ig_E!s=3oj=!%8R@gG!!!LO+~`op1$4Uu?EHpzjgN`&aaT7>0S6fa`D& zW`CS=!6KZ4i4W4>;1XPi+K2H2Cm$u=UlV_rdWUl#qn@GiQ-s4J>{_6_AE$j__bB}s zX5lK_hRWZN&ri}nVR4M~VCqx!ef}N!zd%2NKJK>7!_-gm{y{hm zw_)s2UGMyR{KGz2f@!z~XQB5o?BO`nmgr~0yw?;iz&^MMhhXp{x;_pQFb~sk4$i_A zScdD+_fg(+`W4Cv18@k2U>?Td)K3u)W_|{{ud3u)y;51{hR1bR=a@%Xba$pb%7!~E z_Z1N4Q&wq{Rdry=rOdfPXt^8Iqsjz#B2Mg|R11fd@`1qQVP*S3n%hFReOm6Yl0F<$ zl*Pl^>S1;HFgLV*qI!>lo;#>|y0mW9Qt;odY?Ah-E3oWVCftD~w=(T+XM54jJ2Q;* zE2Ljh16h}nG1Ax7`%5lu;h?hS>Qa%?M13v3=##t(W+2bJu>K=Gh5doXE~*C^W>H~p?%44KItnYmARzGMcw73#wFd=B^+o!$a!+*?peE{%TE#HlwOvp#KqVN$p*4<4l)oEW12xuDPa=$33z=;bAl@OZ)sw zZOY7k??RhW-0w-ZDdqisLZ=TTP*yc9-=>r_?quH50tgcaX}bA?o{2VP;ovm!*IPtR zw{}n|t)4L=woZ`A!Xc424*76iIn+nXw`pr_YN?I8i#OUlTW#uQ8*ewQwqNd5>MOe9 zw9u@o(NE^o1i5r+S(iHLvNb$)aD_6!`((6P+E*>BH1N0^!K|Ody@44Qx_8Q@6jatC zhE5%60dGW!cw*IBGU{@f@MX1~s$N(7rQUM;)F<|lHZ86C@((Lps%P<}vaL>f$jpB4 z%)?4%Kizq0e|Pp_C4FGVttc}sp6!#$q}Q`>QptEXQO10pm4}pZA1hQPci21murhsE z>S^U=9=2CsM!}{om(A(Rd#F@nbNh1CCBLZ4{$6hOy+SspuApG!SIXwZm2=#0zW55+ zUV23Vca?VU6veSA~)DuWl#%8?W`0 zPb!2BZB!^&K@^zq7DjP192Dkqisx5g>NjT(UyH^#{KjV0Ie!^+If849;> zOj{)f$0C>za<3-tA z9*{;T${Tzp9@4T8shNjT)WJjA>`8U(B;zM{(ldQh&7UOw*^}D*Np~F&KGA1<5^2$wCJi&6hZOP?_HxlAN@AwwQzSg?(#kGol2IeSt*ffW zSEJ_o9^dduCdh2Q{{54@h<^T41u3tV`%r zOE>1FmJSkW^|Cq9I%KAu+jgn{=~9A<5?pXk-lF99ExIRfrt}<>lo_u!elvyg%-*Dw z+hhXH`D8oqBi+nlZ6d;~L3OMj_AEw}(&e7gjY|3QIUWnizRI_Hqf)xYvvQ*{(-A?h zbOeewD#dGk>o+Rv*On;mVy7p4lQR2SPxfY|@LHLImR{#sjVOyMKN)jiZw|tb~?bD>o?{VIKjNuynAs zAM=!M;p)F{`WCMFYs)t(vp0G+Bg)*3-lZFr#T%I$(h+U>CUrcbF*igsZnnxs7+F&h zjr*urUq7I%9Pq9kP)Y|F0(niF)l~X!Es`$BDWywMy0j4` zELZg+)Z?f&uBu}y1+N~&_1sme-?WPSt@ef8%=Y>hVp4 zmFX7m`t?eoWu2aqYx9=F%1WDOKFq;$f%!k<_bh~!tbdWmZc5ksif>Z#ukkH~mBMSh z)7Nu7wzqJ-QhaT2I;<35=Usl2vfeeg`6gxc4Zgygl$o$L7gm?US}ClqhABb%22b_| zb?gRaCad0b)oWzg6IDXgI^$dOX}wB-Zq4?nvO(i-sQz?_QANpFW%x*uKWMc~q%f<6Aqb%wFrCc!N^D*1LUF zS$mCl_NcP{I&a~qvh_MyK$foa=kRu&cjBlrb~J-R`iyweTvUDIE>@9r1+;5Ol0M-LUNGT)0wM@oL*wD6@xR zY_mkETp1!<80eQQQY$RVmQ}T2Gmtzr^f%JK$`sO^JxULoS3OIr zx=_yoaeSzI>(i-`ueOk9h?@(hRdrmYJ6DG$<&?2$1a`=vAEgatIgwGk~a5ZF{R?ouxq!* zr+3N2l2wlH>kLYdQgJ6uKQZs9l89P)KugNS^OO-?(($p}WinE=X%}Zn5$vdkm$Qtf zqpN<_YD?-W+c~#)#$8=T8~q`tYAceFuwwXboi+}+vo7(B-!@+2r;PY$&T*A#$I!ey z$g1>u2b5&Hv4617Pg4w1NHg4Lbwh22(|a{)*i?__Ud|sUT!V&oSdnsWa?0SQ%-d|6 z@eeCobn-1%XK6nfE-{2ny*h_zV=@+_mS1yTv1{H*qZ+d66fP;lVXGRvV{TRAJ)JRYv6}9@WUGTVGaDS1{^iuv-Z`W zzDMzlyi$FI_lTV7@Rn=N;it~&!xDabo9~S4tQvpe`L^ozG1=zBnUE6WFZ7$|bo0zX z)cPw!#IHJP`jxcAZvJTF{{lpamS-2%68Kae-IS99kYtacBj!ZTh@DFPvK{n+{E`tFAk=O|-H@^KhD3 zv}w{m186H~>rw+r@72Dut}{|^5*8*bN1kRSjJ{P3lX^-J7W%?DJtARzL>oZsL+daU zQyW1`q1nqYftGY=c`L5nu7H+s*vS>@xI>fE=crASyvsK|-Du6mq?ZCpSWxn58{ZPI zld#A|!eWF;I?duG35#7MY?Ls`vooI5S(dOK!tCXqv3%LJ1vJT*UE@-W5_V|oXk8Ai zg4W^Cd|a(BkMa?1)Y4>IKGIfvAHZ`==k#e2Vr>ay8nU`qi)+MtBw$7_(iNhOS&@3zcz^2*aVs1jhL@<-?N z1l#rbN(PIpu*|yc$7TwfPO;%5^)0X2pFH7;zgc>TAI7%*kLztFS{$td zt?EI zR@f%n&H9e`+{PyQw~hVK%hy8PX!f*&Xq`4YNjr=dL<_QQKek9%g0K$4jJ(>`P?F}L z6-Ls=N3=9ryUo97@|C9_&7O~GG!L505Auog$M;u8TC%=muAEf+FVUtgZod9If{it4 z6rcO%w@St|YRtPDXNi$7pVP-#ALA=(pLg9UVn(fbmtZMpx7>*IPOJWG$1f>2#~o$l z@6YLdrmwPUYac1!FyXUbY3wKCXao2Q5>EP|Y7|Lpim()6MxENy3K7PwYRWLarp|`#%VZav@l0jj~TJnczp-k4*o)XP3ZmVIbF`L?D=#r zs7$IHY)GV^pg!#8zs51ztb?ITHjQi5dt_)&!?d^q_}r@-<75v1>;IkOp5%j1l)q)P z_}9s=gLVHm-$cup4`tW0aJSkMxZNwA_8?^q2^?z9C^H z!WIcjm|;WJvPjrAVcQo8^K+L*7j@u_M-(MQScN=viBCQu{$gnU)pPo)2z_nRpNwf& z)^dvWO6}^NRu^TLZY5pN$oB|wq@JZelJ96Wj^r&vSh$+LZ4Q_8X9*i6t?C>i@#U_P zVYDT)T^iSe9KK~m=;mAJ`iih<q~CX>wyF7N8Z;=<%t~M{YgVA-8vCCsOLLO+xdb;%8YJq-l15!vmk4FWDYfym(eEN8F@o(9 zwpROJG`*Bmgd;V{?;P<9-{rj3%ay%qQy}AcuF5*~+j|x6QYs_bCQ|9EJ zB3mw8a}1pZ59WLu;bp#M;%}J8-%HGg{q81HpD_=l@sYj9I@fZ{Upc}ug!P&EYnZ?0 z2wNdc#=m|3kny`pSa`iYM~SwM)`M0Qdp-gFxMfON#D}aUr2N%6s9nN&$MPm&{bpF& zehinm-Gq1DTRToft8pc)m#`RNmfzYjM8bv$n<31Yzp9bYC5+p)l-xzarU{#9;#11D zNLZY*8GV6$nGcF+TmNF6d)Vr=rKf&Ok#fpQsm0F9tFL9GO^LN~0?tSL1PO2d*Gv16 z@g2A9ta)nXlH=R8?LC4;Z44PJG3pFIllam8oj%1jZSo?1W}5nuV^a}7Q?%cx>8E}! zXqGqtY-g~|iQi`PnoCYzZ5L56BZ{)XzuCKE!@d!ki09q#%DGY%lOyyo49O*f|U zkvi)@3s>Wj-|F{1LBgVh^$?b1Tk6Ak4leNruo=guWZ6{XOI@W2i|;hQt|@JpCw#OT z&c667pv|L&*j5~K$O>UA7YW-SZ0#ap+ACNW5GH-mc}~|sn6J`2y(nQFggN6$-unrQ z5LWGfq79*i9oiULmqW{1eyee%+%stH*v+zS@4Mo6iLhD1GzsG)_9e6_w4fX#Ik;8( zy7c{R!lrjw14vjyyIBiHWEsOj8Yy>y>nGC3IEiVx;3r7((1Y!m%DW}mmXBz1pI`w^ zw&f#f52MYZt%^Xt>V2l3w&XvDUBBzRJ}Gts_56#kIl@*5tCm-^Wwb>!V0>hZl-XHE z%enN8Y;$%M&K`A$3sAyR<_uVKao;+a_hV$Hpi>S^>M9 z_^_2x+ zllqg7Xq{+-Xw~r;M2lEnK3FFr>J8zIckwtPgJw6tnD zM4Lhz#Lgo|d_qo2FNnV%H5@=&$WVBb~_c9xVhfkh2uc=n^YvoqrdxPYM@E(bu zs`h=eG!ouL_!QxT67GmE_1#1GCgFLh59+j9R*9P;Y&d20XGdHq&p6=)!ez|LN3>j! zGNFlY`2_i!<=+Zgb$r(5j$D0QtYS0th}CaNt7c<U72IYEUTm) zcolU_c(u+Y4_#=}Xh!`E*ZaJL#R*%#y?z|4mc4_p0m9~q7ggE0#C$h_-Rh6?E@I5= z^(Xn6CM^2F`rImYI*&F~)wpJFFN1`a2+v$3TzpmtpCi25hdR-GuV(B@9F-mNx_7=b zuY*gOBiJquTI0*s-r~EDuqDD|TRy@;%YM>O%<+*%i+;#zE1M6=e~z#|!VG&ye-?l9 zk_O?jEg#9#ill)S6v5_C!ZrxY#;rNUF`sGI(EkXFN<3SbMTZh zh46^f8F3RAxjrxX9KzQ7VQao+U;4)wT05Fi=ftn~*ZT2L;?7{#J;Xbj*_Mx#X#s5> zEhTB&)L_qNn1AcdawHlUdwu5nD?At#76QT!)6>CkJ#`D^4Etpf)*B`q3%uJ z(srX(yl$~^^wlh3Q^a4AFk63=^a_L}KEiw=VSFftvV=B@=55*sjVmEWU2kHS{HS#d zAfDvI^BU$=v<%ylk7`{@T0z1(KUOcBXkoMfT6L}#A91v)pJtuGw!I&U{QzNego&Me zL>obyN0YqEN3;pFWwf9Od_>Em714}3va|wP1+ALC*e#*?Ki;U7(7Moqgv&?lHqpA# z?CE-blq@^609v0z>q1L8v?$sLnmsSQXp?9%cgRQ5O`&BScB5#s4u6wq3ux7`AaSSA z*3p7&%SW_1v@M6WjK)jMYhzaI%9a*nTRx&~p?R^h=TBoI^E=|UTbgFXG~RmMacTjwRN7(c<=9aIFoKMpwo)h7J6rpJYBX{UjMy4NK-0cZ@H(lGfMi=7miuU&m|tJ7cYT z*cYuEZ4_-q#v32$+dXKJx9z>QC4DzVSo`OApZ-C1Vr>iR-Z`PgJ)wDz5MpKB$Uq45 zDdNsdGnbk9c3jJ8ac`3A+SME?ElQjsaf)B0AMH2No8%c*`}&~8z37qaqGnkoC3l1i zyLCaz*#0`^`#-4n6Y(2DE23?%O*xXrv{k!5BSH0Xg8>(vleOD4T(&*){Am~;=|8lN zW9GFUYq``S#zs*jOEi~~$94nTc9)S}{ktL8R{cUALH+zKV_PaWOk0<8o<4LWXwm=%4kUus%5FxN9`(`*_V1T8N%N8 zXN={2hP_-ft{--+!*Yu&<~@=y>#_bVn)slH(rk?y^D(ZL@LT*#eiuUeBi|?0n%g?$ zwYb*pUxdogGRh={@clKvL&3g$V*L5h%4oc$pt&#i3S+E#`-zb+zUi{>n|}K7GyY}f zql@N=FK6u0TIfIaeUk6tZwf!Xf5$QFqJ9P&qJAiEl+a5Fjd9`8C7~f=}$*4E`iS(~5ab~_k-I@J9eaY*| zEz~0xjZ+>&da~))NJyzo)#AVzVXT{49Z=QHl>|0~1ah`5+EjWVdGVwaU&UGcTp3{wUY;|zCGe)gc zXIZ`qmBAo$jrR?tyTWgUn0217edp2Q_A@K6Z{C;Z$F>{Wo~N;8TiV}MXfwuB0(}!* zY~&;J=zyik*piQM1TDB~t=nU5`AgQnyOS(q(%8U;?*;rspJ_~Q)zYf#E$M^nXff=v zMh0y2gK^4h+#P6*6tg}g-|a-4CXO7xg`lbgi9pWjVdlLV{B7vtvA7P))DesmGnwS$X%u_%3Rh4^#CpE2V%>2r=R zNF^Emv^O%J7dhAb0m|iCY$%42{{()9pX2wBOg};6JCEw|z{T8PeC=Tj^cFdU)pyN& zOcHCgU8u#l5trTNsT;74VL5)=Z_^oMRrRt$L(6q#l{Gjia;?rRvPp}hAyRqH!o52Jz5T6Z)W*)_CRq;KMP0KX-EJF4HTlVsCVN3%%s%~-?H zS6ut>F^!Kce$PqDCZ9Ne^Ju>BQde>?V~u&qG1TJjKX{SJO2)o+J!R+jiWb?n<{WEH z;+m>0NeGg>gs~m__xg2*4zxI08qMl=((cA-lq+MlvGh-gJA|F*f7Qz&+8A08t(|T8 zh?Yg`v}qDHgBC*L@MWD7)~`cI_%h)U!lggU_d(9RZ zrio8JqJ_~?4lRz>kLEY5OuK$dtHzc2Xb3HVoz$6JL#Uo>N*J$EQThn;Nk3r$Qw@{0 z=G7?5##_(n^JX8Ke37=6yA-gg_0ZbAZj$a6zGnH&rxCVk>vV0NckyK>dos&QIoogG zZ>QeQlE)BQFk-FoZO8o(VLgOZ&Rg|w3zNfh^`36#Fmn@o@s}n}>BhbO$fA-XY!iPM zpUYVHIKC5=e6JEOgU@Pz5?||R3Cc3eHf1*FC38N&N$tB>S%KAdq^^QLM!OI%Va7{V z`}GLp)P6rx9J>fARWVuVI^d zOW4cWA~$0t>I=yzCS~lkxAS{3Y-7JCuPrid*Yg@{weJxn$X^GII)D#j{oT{%JLPF{ zM|nNS(><+nPmfV2aclzoj#B~qX6eX0SWm`0E{qZ^|r9? z;gNY^0UtB(;dg&bALSRB7kE(%N3JIGg4C_|E!6E>YWH3^>UO4~Ze!R-u=hN`Z!VdA zsrhlO+NNvG+f>RhhM(nM;J2R4GUP9LeWS&#aq@*tt!LKeHYvjjzM{Xz?+>xPw|}tS zw)GDwgYs7D5c{Z^_9e?;oI^VMC4;aNH*x&To#uCq@hIkR;$&?`q3)f;_x-uxaKz(r0#%R z654xRr9#}PKjSy6#943CbGV*X%2A!LB@dw+84G{T?gr%vQ~FZ#xt?&18vWEmOiqDj~;yV zv#rE!bApjy3T(xfNym>#M}+zAub8{UUe0xLZN^*_K@a|QZOl8y%UDBSlXM5MpTd6D z^y9E^?3+<+a@cq9TWCY3{SRDLX|FOqviu&JY|AIg-xgX1ZAOHqb0SAKdmYB?yXHLH zeG_ee=S#c4O43eYzf78IGXL}G<8Kr#$8WOnaxclprN^E-@7a4$;7+x@qA|vb_}2J6 zv}H-mydJdYI+LtytCvTf>X(g#{x zYmJ@2n$H7K>XF}flV@(}2fF$aHSBQ4HH9AHQ+N{+>Q#Tn(tNNf>&{ zF!nu%E4tz}?32A!-%oIjruM8^pvB$S+HjDQ1f@?Di0{3c-zH;wZ+eaMh}6v*_7m8b z#Ga36+fk-`es|9B!M^zQ9%H@>zO-_PeFXa~_VaA-%}3)}O#0y<_8qU{x9`OMBK?Xvn>HJi?uxvh1+BiKKWhvq#gO3sMv1g8V(edk(ywrzwoc@BnY|AIgUprb6 zEhj?Ld<@py7}xLYdqj~wpTtM#)%+gcOX~CTrNrI^#>M8id3?_u<+tz5{K+-%y>r}j zWA~PEu|>SH{8pfgofy~N8ZN?HYlbo7ieL3Ap|Sdxx(S^iKEEqy)Cv0$ae*ekF<55X z@W**=<2?Z8brZRYnzt=u#c7BrW5jRgw+ZLW_bR{fE7u-$cxt_6_Z3-o_&j`c{uj^>d#Y;*H90DK<@qTC%0n ziA@Rn$nDIDRyy``B^d!mAvkhK8MIqD7svLclp%n9H})Gmla>_U$@=pK zyK8l}r*F|Z4Sh?}@V||}dtTbSX0$){UA-^OUh*`I{T%l4t_+!*T9TYA^mDEtg_LuJ zA+zZH#;!S!OcN*8$M0}@i1UAWt>n3l{5;5SJmOmVdeeDis@@}It<;BUgm^sz)a^^k zQ@`h>&TZ0%viMH(yO_ot<2zx(KVUds^?kN^`mR0!CvNa-|fur zz2|hrv|n+4Z!XvT5sfO5`P;rH`AZPL@)FaNc1~lzI?C^xN_uitrS?stIalNf_x>!u zpG&ZO`uQuM&7)~%ze_j2=3#wDz-wO}-wjH(TjI@Bd`n&X-%i?}<~LZ`mO68l+l+IH zF7z_`im5jrC)17NMA~tHc;3&{=R2vV5i|{L-t^@-7i?TR$(%8beFl5!!?kuauV2zh zt#f4C>9V9-BF+?X64r4|v`w_~1iwpb(>(7$|0U~qgpJq*&_;imHL7JNdFVph{uO?s zm~HZqsC~y?KhICpdj@Mg%#y^J_*K#~?lP)43vD-jP*|d-Z^w6J9)!KA+Pk(34f>`VCokR%}Cz0Vde9bY) z6?1CMc2e1*l zIW#5D?=1uQh_;Nj{kcZ1jJD~}w$RpXnyhEEJ6Y>jpS`o?Q^Gn3Tf{!dHf5+igDYWC z!j`JOZ1E&N{e%sY$7)%m9fm9|$~L|l>y^_QVjdPKA%B&L4^Z9zek~&!zjs9=ckFAWd z?@92FFv+{Uk4hebgiYbgnx{A2_T#*@SZB6IH&+R=T9+~2N4(A{erNloou7Cwc777V z*A%|uUwG;Bu8%lH;wZn>I4^9VRq$hPE9LIJn%~l-zse`XUniO$e=8!`)-3Wosq|Sn zsLFRu=C(N=`-zkP?Rp-hu7}Xtsq1djPqaF9N`IXo%=i2J*18#1TL~~b)%v02seoOC z-#&jS$IdABYuGPgKWX}H(vLWV+OK!o_lP5T>%52Z|3Srk-p5vcnF*_7DS}-ByZJw? z=t;Jl_VXdT^Tx~5lFlUYOT?GsjJ=-i=|~;SVK-NJX>}le*RhZB`|!QuH(|d9$YDiF zz}c*iAm6(!!D1VQ5WosHjCE(XI3Avv_-U}Lo1>UIJ6D4ltYvE`46I1$AnOR$V#*z+lu*` zGjsk!AGX;_exjBp+wu|iq77juWtER;DYUdB?x>|1{oS&gL>sH7ZD~_zlW6ud=Fle4 zyx7P`!j`SLcCCyy>ag2FlXBSYwEG(CLHxB_T1XP$BU%V;+z~g1CS^0)-LmULn?kdf zZ4hl5&0a5Qv^lh}B)~_~$e<698L0UkK2z{LX$GdM>u5J*|jmWRfk;`ZN;I@pe;MJ1+*nJdtI$s zn!Rl6XbTRzily1@eD5^d!(LY%Xc9MOl)@}WH`=DdUr!S)*+d(*H2XSY98KEDo|hb& z)L+7u?krm1&l~L)(R^t3IxV7k9ohz(=FsFKt;eDH(G-W)iB@ssFM_t^&=P3Qel>t5 zWwYmR1kKrpCeWl0+3O&WCjB5_tAhfXv%f4^cJ?$%XwLq!i6;HUZs&Oy>syBwK$AAN z=cNlx#%i}MFHtmUBYRu+qDh|ZS_)0lu=^WDn|ElFXa$Ehg*M~xH;0yYXv=6BhgL=# zcW7H^qiFU%uD#owcLTPvwWCQ~dtHSrJ9{}|XyVU)oa#fHbi^G*n?SS2O8Pq^|7Rm=)Kam#n4P^D=`b<*;iDX#Eat6)oY=*3n{)bSr2Pw4klc zef_Mp9d;dPQHR!z)@{=y?>%TCG-G^N>ju#W&?P@c+u7C=qn2)Ot4XvTZ0%*7LX-6E z+8kQgp)I3zqS^hG(K^uV^}U7G?ue_smpRlCw;j#v&_ZaM!(R+daoF{tRam>(^D>CG z?a z3fhoE^Sz(>!=ZJcr5su}+JHmrK}$NcBwD{i8%FDMXya(T4lRe4aA>n=Jq~RVE$+~Y zXfcPjffjXWN|HIiq509e9a<+^*r7$xLTK%Z`H^->ph;Z&JUxIWY1p+9G|7uyn?RF1 z+qFEJl+CUc(4-FR+7g=7m0c^LNuAoYO*E-{yXJYoY$LlCK$Et#Yh7s4=Js(EZ4$S) zN!*kb*Y0l=P5PBxn?#fTXHR3wifh;A(4=44wPiFZuU#vnIqPZ*P3p>Sr+vVzXZv_- zN0WB5+lA1iAK0}RT9-rXL+fy8gJ|szEsf@PXc;swnmsSmXo|yb9&P(i8|_xmHqh+( zTSHrO*lnW~9d_OanL8bJL9|7O7Dk(MXmPX|hrfQbDTg+MmUC!hXc>o=MVoMFGiYOI z_Ig=B8$q+riK}SCj=1Y+gAT2NHsJ8*8(@4m>^jgA4!dr&m_zG9>vqIVqJt@0<0^-@IJM6>sU4YYNKo$?UJP=}o#ZPlT5qAfe(M$i@< zS^}-$h&zBb{>h8fI|zR^*j8H)v^gyEIx4cGY7*^;oL@bfU-jMV{8&eRNy-w99Y) z#M$WJFZK@J>*ZoBocP%bx_smwpgs2;Rqvm7PdJ`tFz%p_;b(}yz#Y8LPQ};dHD8YV zsH@LT%FTlH8>y{FCynoh=fqb(@0Vn9(!TvOW}f(xBiZ!czWRIQH;I>2&G!y!-?QRz ze@FGYc!F^G4sQEN-uo+g{CdrX_XS+Se~UXLW!`)D0&^KYrMwe4-Wx1FpQ`y>Y5rY$ z_5M}Ye3KV0@{J*F$e%o$H1;{ZN7;wZ_tbn|`aaLmW_N`!kK)fu`+7FGk0^w{hb@1p zOO$zE{73fP-fn!~87FT3dfu5J<*UZ!yPJml(sJf|LAppwevfD12Hu4t_IFun*}v;~ zqQ!c{l39P#*w6DEd$$|=>uY7@nX60Wuf@H}_dM8`-xWP;dk1-#JX9pl^E{Vz0H6Ok zYW5-WaH&2d^%N!#eT(OfciCwluH|LO`Taas>xb&!WV7BOt~_&5;o0&HDR;fi@~*vO z$*ipwcbI-fc&!Y^ThxvA$5+mJ&RctZEg$v!1ZvMc(~Yb<1&sS9BIV z@(j4-vsNFr?_cG)GV>M4+!u^Z1p5N^MxTDxs=qz&ny8Ltw?a78Y^09! z_?y3SM-NFEpQ!m8+S6UETRv&op?X&_ilz+UXB|INSMBJrgZTMy&CjLh3ga=jz4yn; z@5}5}IYy99f^wJK=KJxrU#X>2?~~TOD||;~yLY9fVK0K8EPe)F%l+)eIH~!ueV1sy zNKT#%t9`>~j12lE@#Xh;re8QT$(F&UGU*uopR{7v(`SOD^>0>MOeU9_ha}#-#JksgenGpw*8dytagf_s zjPDH1n;hg=v_-tgJ9qRj<43!u7H{)Kj&Jf#0&JXRFf}Egy~f=lke-{u`>mIHphHBjn<~2^MJD zAJu%kNLd1ho0X-PJeBd+HO@1GG7i31^Vcw^RS&f-#!brR5$hYz8l#~_-e+H(=S+$9 z6Mp6Qcv7F)(c{vGK3DU5iDxR^bM&7*PgTmu*dyX!`e}|d)*j!{S7a`Iu$IP)%%$#5 z9@1$to0@%P6Q2WD@y=xyQQ9>%pD$9sQ6A;r+gBuyQT(Jk_$>yRCoZe`E5FECl1U4j zy<7<(d483{WX==n#GX!j4{&w)s-choijaZ(ua8667v!^G4WOd95{&ly@AzBd&^` zVNI%iyyiE3>2*W%A8wGvh#Uh;`0V#r%sKhZHJ`R)sQL3*EiMk|_WN;&8|JUQjq+Y; zJ}08J*5aql_X(Ld!^-0p2y|p~tq~<2w`M7`#50cF-~V`ZUDjAf)?+W-wsn;8!*Rry zwOtk;zN^gV4zw>_(uXT*>q15yJNCujI{t?6x5BpeE0^}i5wq4r&HPE-bYI5x9{fq4 z(B5D3*VvaF#|T*;4H3`#Bj$bd+M6#DkCOp=A8fem6CaYM{2q_zm0Zt}et3D!Ph%Zh zr%+Aj^(J$K_?6$|S*ZH``cF4+yC2Z6^zRgYH-bFh$F}yFi}|(2&9JQj%u(YMjN`*R zK4UyDRu-T4zKBmIbqib5-7puMIh0WDdk;@3BSBT{rQ$&9gT0oTc`e zn$OytP+e6Ic&bx^xhjqmx9erB^R7K-JU6DjzZSQ?wy&LUGKbpFFYC*DP5SpJUw7Ov#I^{Mg7PN$ZRpqwQr2LV;`vK{W5odt`?VbUHb!q)`VAK!mFPJ z8LLnAaDds>PRivGM+A9{#FN2u3p)*ExMx z5dSC5=hw6k*5lWoM>MaMbz`^nXvKI>ulCNGT|J-Xc`h?qLw7Om?Hwj=AJ0S;q>Qhx z#T}}rVf^ywQ1z%P{d1Z)^B=K}Uk7S&(tDnt=#k@b{i;)xxNF4Cvql|}w)yr)t8J5P z=$_T~HGTxH_EWA#11Is(c_qgko-^7MAHQGok>-@aQL&7DRCA;ngUx(!y+NTLH`ti# z75P1$O`c7X-xtvytfet)OQSkH$$Wrrq+-kl()P>5?@V(|UGjKiE&htTo<}Rb@l=)D z7Ts7cd0$SOR^wmxgTahp<^AJkGN(k_Y8jTe{`gnY?=TbLxpxXVuIZ^>j{sSIhG+JoEHw zn{tdLwXyCB{2b$uIE#|Vv9^5Mu`_Gx+3%^xPkmR-d`CTTx~x9_ZS~eCUQn~ot525H z&wfk&S)4rg&1Z|xe8co~pJU&6pUn_{yYjWNgwE_gyDxUi6K{Fp`KKlMI}Kt(vN>!= zupMQaJQ%jePkA!#JH^__?eWvDw?8An(r(MxFJV8&xWayR-?20IpS|z+sou=Jx1GM{ z@w=Z$Wbb%{5a z$L~Gy#68)&Pds&J?hcoD`BuqZeDnLHB!2pNzF`iZlBa{m&OEN3Jxx7k)XXXM_~~C$ zjqfDJvD?6|WZDg#X*+x9#3^s4wO-cSYNfuTwq?}QB5^0DYxPu*`;MBOR4WxmnfTkn zzKs2(nO41CPn>o=uD+c})h4{Bx@ojs*C#n9@Z80)Enc&>lRnpn?HIOWHd~|Z#C@&p z?t12!l`$W76T}(%oyKSD@@NBS5-uNM0WFE98Hzaume6|9qN4E}&KcKP_4YR9`4`xg zF| z#(H-J&rwJniSGG%#+gGm%8y<|kGREyx&9BKd3fe!Of;+f?$;n z{>1nlCFVBujPLAa$4`47Z;hR5%N(*tcJ7I5O-Sh#mHT&r! z)|N5WLZ2cY_sbNCQZ2tR7vDn!)BgR{u_kj;68kCcVVp42NS^VYZ9V?P{^O@wG6zqb z)*e65PrPTVzdP1u z%v;A#w>*9@mOb!<)}LuT<$e2;``^3z^s(Zz$DTVO{hMsJ4T;GUTK0gsecZ@#**Ja}Bga|- zya)HyzHMGlA0lprxLsVYC$E$0nU|eCeBzWZbJ?w@+a5pkWXop{KBeUj#IvnWct22l z_O=(Em!qoX19Pzy!zaAi)~d0%aGh{7vJd=T*D9$DDU3$=i3;v)^F>@q+p|IXYEVGv8Jp$bL(G zqNLtdeD+?}Bh{LJo2kCrsG$V^?`}QzB{GBIjzL5qLcefhDi4x zZOZZ1e&g6OOjgRoU47H8-nY-p*OkZ4th>(sv+MZjHP_?cbDenNyRPhanDYOpEAvmT z*pp?~XTR+_{?rSu-19EiC-7bF97z>f_D3#x-f`qrS#^$7$C)o~bX) zZnfIEEhHV{v`f~*GRCsR4|nhCgXS2kulc3o%|26K1>RTFjI>vYn|jL+9+zZ|b;3OH z)4xM~i!jLZmW=x@HpTBWdQNnnRpssG#tUV5%99KJz4D(^P4gr6!`I^Pi@U}%c-k$g z>UpAEV>I4NB5%r;uOQ^as{{M+FEKx3KOinx!?kcmh2xa`$Gh*w%Eps zLTf4k(^Egv6}oh0m9BVXE}oN~;-m-G(KA2NnH}|vZS>61^sKd+b<-)M(kIZ{i`2sk8&K5a?BF@ zPPq={&}z1z9CaDxcth+u#O^iNb%b`()|x1Ps8bpZfBMqeBVSB%_sP0>NX8=UJA<;? zOmGhC7JZ%QaW*jm-Yp~#HkWI6@xbjUhZ(zu~YDB!MUcTajad5W9{SFy;)zE!|BBSDZ{V4{!dt_ zG1t-GhsEcST=|ZkT+6(YakNU76+Pp>Ai7@BRYSL0W|YjObz#?%ZWQPd(ZBKZ2Oo}N z4XvEka`um?YX4@H^Uov=hG7@Fdx|`pva#KcJRR~nDRH|0@`Ddw$PRHvC&)hhNcN+X zr(}&tgja_DUOcuXUo@Pjnt0M68RqIgvD2;$l3YSf73sixuUFnhIYl`rGu~2vyD~_| zM_yF9?Z7FdzX24en;_| zRhqAddDarQybTHUJ@c&kCFvU&*X&xZ*7}}dJEUFSyd~OxmbZ!JNkvLl-@z))o22ig z-t0!0HzJYJqZc}o_Lt?#LE@>Y)1ze#$Z_n^yrVw}81VIw+ZBg`X;j?(1$tF-ZvpyZJ_P(k6%nah65CdF${BpsU3jXSQYCoO! zBZAAv1<@hK!I=W|kSt8ogul#k$;ZcmI)APHDa!N34!XQs(*ZGmn~zr!?;|`~})o{8H02Z7e^6p(T!5PuzKd!r32DmtNcv8^8abx=b0DxxS&awXg{cQ z?p>NIP&dncI$9Ovsl+Gbt88{}QE&Ur+mx+eOGdT6@^ke-*U0;);I}Yxjsj>{KK%b=^Wd!Y{Z0&fI@L~l)wcJ-z-YESop`&|fha8|ilxM0K)w%xg`lo1b?RAVY9vgwmn0K~{Va#A|dR&H6 zgt{!8ruDdz_Nyr`qkKE#VXX!IL|&-J6SPaoHK_1Xjbrulem@f%_;t8=Uxu%|HqhVJ z2XA+u*r2?R(Z5cvK?kG*iiN*J9P2&kDG7x2*W)+Jx{Pvo{BLAJpd22bBF3{~@b{!Y z5r6$#+RyXl8q_D{LEm`1UvR9Q9{*#(@r=deZwiKe+2h#v$^`!+^V)jy4DA=>jO~Xm zH$PWXFU=5W9c7FM&@tDqJ}h{F;7fm{ z?R=|TgYdaJ+Q;K31WyUR#kE5RWjtpAdAXZIQq-m-y=BUbTjWQat%Vi=gVt=<+~K1kRRVKQ;#z+=rm>QNgyA0pI~S! zkAF=twCDTuM{dwbmdnI?}cPfvl-%0x(%7gNQ4ljtYe_iZ>`*Daf$)Xth=4@S8dsn7Kd;WO0 z=gmoa|6Dq6)tV5;?$iFio=f*I?{E5C`p?27O%&%9IIbU|rz@A(9 zc~mL~vRPuY@rqT!($=9`%F<1NvN@ln8v;#sa)YyPb~4@0GAsX~ii&^@kPnjol;yYk z0o1!xULKz>*k$r6c(+pCW&aNEvaZVg7uLBr9QP$2zfqzM*C{H27Oyzwg&`&Z_I+8De|FpPH}=Zk{H#UBtH^WWP) zD_C4!%$plx?Dq(UdE?^%$C&i^0l}8U;139fGnTjCEg0sL#~&mvEResS8vQHnpGxu+ z`+M(nUB7JI_$K9x7^j~y#wh4%%2QN~zDHT6XCVI^;Vb`D8F0)Y&|Z0^aN#~rm;fw; z{C%9)m($G?}>Zkw0A-kenx!Qyrz9-O%S%KTQ5p=DCG-(4uWN z@N=-6h;I{)HhR+G_W9US;)S&D6pnTOSH!VifPNu&AG-v@`C-~jt;c*A3E_+jT1I@9iqVWS zW7mU+1&6=Kr%8LFP41_^tv}tAt-seWULoUSJ;l8HGumN}fV>^{0gPjhZxsx&{QUeA z!BD5e-P+zMxdvIgcHOUd-UG^2tKYB9w0rmODuU_^`Hn(uH|*)42PoV6+vVhP)=Qcv zka3F9Rf{2s49&(eM>3+nBEK>cFs-=@5fauz>7w*B0mkHz8WcaOg=g9v!xZ0%3$hxLrR zj(LitKd+bTZxOF0exq>gCmzQti8-9pq3vYtZoG@%qZE@dBxwTm-=Ti(N6J0B4Ose$O@)ywA<^ zQ^ajw^ZEQa-phWz*z;k(KkSrx;(Y4y`&fUEKPouRs2<;&fPaJWeR*Dz3N2-w!OlB} z@_gvWr#+{Ae4e(q?R$CrzL}lJw@Xayd%m2<63Tfb0sjk@KWY8$F(1AC_XWqAJq^#o6ykz|lU~ z6MY73oUS=1unF}g%-Hb0vB4~LJM4IfsbwhImQM8w-1niM(ZE|5w-c`(OLz z=g>=9zcnjYE>9J$x%swvsb$4w#U)YeAGEb^PW@qHYg_Y4~Q+tkn+72maab8TbC=Ej!xTJhV~cup_go7&nmQf+-}YyCYM$*vKtxV3Sc z1eP#6B#*`qPMddZ-`;q4`~SMRE|CZrPx81YklhdT02H$OeU;?*F?K=zG2xm1eY~IO zqksDwV?BI4+;e97+x)-_yse^|rc=5Ol2>R^{Tp#Fz@f_V1Lo^Kj} z=d!@E7})N^h8Aj%7MUTr_rFbWJd?Hj-rt^cS{U;;UM~t%(fsW`afJEJF3EqpC^GAB z_ot)uZ^DNa8RX-AUUZrMHMP3l)zoVJ4`%q{()^9z=lsv!uhyx5&y=wN%+Js-F{ahO y=X09B=X2^mY7e~W;K$!Xf}{K;)EhH@Jcr68*n%>-|a2!b_e zY-2@@ZLCqFjfxmGRkX3jib`v&RA>boHPuw9jV*1dVohzVD8J|I-6i3Vt)KdPJiZ&A z_u8GEot>SXot;1LS+2{do2;etF3=@91 znjpiwH$&*gnos+^LHpF=^(UR>af6HQUG=DGg6BY=3L{pYUo$>!-u=FK`m|@c+$)4n zhaM;6dZtwVN{u>C52hKL!lxG=*Yc3(xR!^e86JHamF_t%`YMn5j+3b+uW#^tQwJ^?Fiw<(X9<_Cs+v(C*I}6<<73F%gURY6G?$*zFmxXr6a{IQDLUdVqWwqN` z<91e+>ysQ#yQ8$CaJ##>iZy-eaKe(y2#<&r74F3Z6=f zefxds;d0vxx9eN^aaY+3o#}D$35iL`DXD3*>{|;R&LX?Z<*YJNzM#}uvyQtEW1>s9vhZBE@;?$Arh9nKnkn!dBxS+2WEOA1TMx9J5>ZAwsWzkkWDcWqkx z#LZKmiFNIAxt(RPTT9AgOUg^!nSTq|@BUrZ)Ak>Vm$nUCcdC2LXLF{tpZweB&vsu` z|5*6lyGzP<*h@p9MNXc5{fzi8n<^75!R<&p~GDq~gth;NHZ_ciXv z-QFc5W7R70)D$sa{PREjtrVH;t$-h1(w7jgig(%LugFkjjt@cQuVk{}>T#3%4fi1L zGWPPIpc%f6i!@RAc^BJoKff^F#!ww8H^Pl{9Ihv4t!cUvoV#8-?A)`8ynboonCl$z7jU z;3zJwbl2>ut*hVH(Ad=6BDO?abCn=Z1X_pLhKG#|9}_WNpAauuLiOiL#K;-^3k(y1V<(N= zZY*N-_Q?}emG!9Y5!)vT5vrQy8^flBX(mnArf4IzXf05iuBn8Ct)ZuFcV6v~Vp>6WYJkKJ`=eAL=|UN%JGX8nH#K1^Ks!@TZE= zYAndV8QjCytgpTHnnn0OJL=hIpA8k4?VhrG_wI2*Y!ScmyBg%5ljANI5BtS}{F^Ss z;)1QkyNs+Lvx8jbZ1*U?g_EyX%^{aswu{L@;ima3*NcrpB(K%hYs;_NEbKx|OP!FK znwlUsi@P4Wq4UVI$L@T!LY#Q)MIly-d$f%p|JDlOuLa5c?=KVU;>BOSaWpMR$X|8` zN9geje=e?6eayhRe-yr{ga>=3_E-cMy+@DU?K!r^8-x^bz?LYr&DfBE%OsZ@N1xhw zU&{kxzkKXO67QWHv6Z)9wYWvxA?_28h<5S3h&}J`ZDVV+@oVGB`TLN)p9()^xS}hX zUjXm?p})dx4YgXWlZ0Q$@Q{!YeJfSB49;3DZWDKjpK$hUu~AtK@^1^HVR4o z4?gNV>&Y}%Y} zFBFL(p;QcIT~+8TE!kRSui6!BFQttT>#8b@t+MaT-{CBbsiNk3;l84xlGwt0 zm%EC+cB+|GWp=k))^4dXm$>zkvdU6t85L2xQBSUNx~r?ooetezt~+Iw@Dc%*xn)oT zRcl{m+T|=QiYamBQ&p|X-zuxKL^7&;nqAKecOcmE z>N0PzF)@W?2Vz;UvQpm2^z`cTomKYAsA&Z{4NtpXu%e>eS)dc)YGoyswOg+!(v4f{N;hIig6kzN{o;F6;;7N*=!+KQELgNGcf&O4>Ab^Z z^p)OZmhG=)-UQ6a&ClGhV!`rdi*#4XHBLRMqNs@GkFIa!)G|dp5g{-mb~2sF%GKqb zR<*mB>nOCB%Vrc;jo%@@98MCux{y|r?x?Pk`M$8!X)mv?oI7{o-1FOmfx0eRiRpTv z$6c;?U3h|xrq!Iex@-ehR#g!MSlLq71Q*&BvnPJd<;tB z6Vj@Ir*4j`xTK5(<7T^c$F6c+pE-|5u-YlJ(str}B@|Vcmd48BgGJvR7#$cDc+vfl zS&943-HD5fi;qi)ON>j3OO8v4ON~p5n-w1yA0M9(pBSGMpB$eOpBkSQKPw?FAwD4? zAu%B-Avqx>AvGZ_VOC;XVtiskVq#)aVsc_iVrpVq;;f{&r1+$Sq(qus$w?_msYz)` zvy$VI<0(=TlarE@lT(sYlhcxCrNpJgrzE5#rX-~#r=+B$rlh6JN{vg6PfbWoOifBn zPEDcemzFv!EiNrSEg>y2Eh#NIEhQ~AEiG-M!=ckI|$Y_WA z@{DH>sb@6FoiZ1a^R~H*z1tq&#*t)3Z+@XUZ){M^Im;?3OLTXIH(c4Ur(EC>)a^w) z)q0t|rlhR8Os|%A@+%Lhu9QKX=hssbd!ylvQ`9uwQ9*By%!d@Rg~hted?n?clGSr= zpVv#B%Uz{k;!Lt{<^Sd4$&KISSN05j^&)HbLl>hpHso{{Jpeq*o?HpRrm zY~JeJMv=h7DZ6r_3S_67FDFrE$opZ}<@4w%!+0&t*5#|ug(p{u!%4pnIji8J!pd;H zhYvjUv+q3V=O2(Vci!4hj-N1vg{P#hU3cB1k3aEz^2y=f zz3JBbOu@rKhfj-7O3zxn?1~jD9nQR;J~KAL-x3%UHY#OSdg~+IzY9ohxV_asaQ3{S zlH0x?T9NfF2UiHV&WwdUTI`ybr@aO;nMcBs=Y$U0(N`uwb`4m^D9 z*Dd~|$4r_$Z~mW8e{}BUUz+sGCr^n=OiQOlpR;<++VvarwiFaPi?+LJuD$Mi`yYMc z;NjzsKT%%s!Vk7g+HKZMGqoa3iH)hfew-E`5@CuA7;m0o&NL01Ui+wDq$$!AWl0KJ zv1o5|g&vg*nimgytMOo<2^6|jO6WCoA+5A<7VnHo?Xv~b>ZzuD$MzpMNdvrRMC z-n@1EQcGa%fi07=f&%@5FH83eOo=v)s(p5jW3_c@K;Yu6u}dwhgO~XS)_%G;aGbVe zS*kY766iO}KX7l#X#d$-#9AdJA^3)SimHQZJHMA*7+e=;8+qHK*DtyM+3RQdPdDZH zO$l5a7-hcf`h%OCSDI$|hi1quac94!?#<}|5Bzy=Vu&)%Z?dD)Dz~A=$ zf+Yd&Ikle#x-69=7GD!)4YRHb7+w3_y-Tzk7lw?e%bDQkSNp~c^Sp^l^(mH`ajkFYg)D`(4^iFnwdSf_7`*f6w?~>*d%rDuxOLRx;C)(@w9Ql(WU@@ zb(ml6-8XcbLbYIRrzzjhYEnY1rZg@t$}(xi-dyWA;*w$+hSvc9+M|;L>->bG$&Ovs z-_PP75E>W}G}<~Q*fz`>VzOz&hhG*jQW<3mS4L}N{KqO0>V%QHHba{k6r;px@oIwd zu= zZ~y)yPyXz=mw)-ISKsaZ;G8fS89Ft6_S|JxY_7YNo$Wt+?w7Cr=C$4r=*{wG_t`SP zZ*e&5ZolW=U;XB_;LvI5v$K}1+myE@-{HLF_D4A6IKzx>T>%T}yhw_!{EO}F0m zqc-Jz`mZm~)s}th-S>t~D6fbZmw)ZIAAjPy=MIe=HGab4B`a3S{QvFio_@LOjh-_f z|Fg<QGhLr)8Ef(j-P<;-_A&E>fW2e2(H6xNYf3Wt zYl^?0e`sJ%$Z-EP{+cNwFo6Che|nuX&1y1hL4L}x5#|;CWBu3ptA3-bIi^M0Op2#a zzYuG>Y24&|z07p=kn#U{I35(+u%PkAUr^3xU2mF{l@rh@SkB`92jjPZ)ovB z(WWteL0WAayJF*)Yqk3=bF>g`j(?hEhWYw)q2ZR;(3#rAkclC+`%Ks0IXY-WeWN+n zJe!;r9#H$jB)7Hp%`sMU?KyMpd)9y4tEC3)%?qpTu+$zk2Zqnq0{zk~i!D|^chGok zqiI7x?G51(fg=O5O|{?idu+dTlqvpx)84nI_*>29+5@(||MXY%>3;0J#Z>!(HdYG> z{>lQ{TTtga>~1?{^hN%hE}8{K>1_=B++fnh?dAfp`LY%SM#~vtq;VpA{EjU;4@T`?E8W z>^WzyzW=I~6%$w8d+7dE;x+qf=ZX7Qi?=7{ij#j>^Z5Jrb*FnLZa9AG{tdeLaKjnp zx{abz_=}m;0#yE#r9p8cY>Jcnr>ctSa%KG3O+o1a0ZO<@3E)*@o}taLOb=J|R1Pp% zxKI9p>Nq7`9&WPWB~Ts1n9MBRNhUcoql{NIB}i_Y@t}mMBY9)to3Jg4zZR&DS7vjp zl_R4Fo=`P2ImTZNGD4M6;-s`|gVYFh7V-0)Wt_59F)0MDSd^<2)!%B_s;B`${>#*{ zgr_K}!xYYF4pJfmlp>SjCx?(mt0v863TD+$2~iky)5fXe`O8ohe~Y391t{cJrCOb& z?9fbVfa0gULn07ce;J}`@e5RyxC!y5IJV77RDf00xkyDzVxxwp2~}0iT=#U86p!ST2t)k&u7@Lq>l;((wFLI=9b=VUVXQjj^IN}Q9 z>}d=K#iu6P<7mMo7o`@)`X1B4j*r3$8ofp<&=tGYUFCGfmQ(-YiN3gH>U3;##uPi{ z0H8j1u5R>JcsFP2@fZ82UnPdxI+c2SJ6vwhh7sjhhqKZZyRD?Gv}9&nOhQ^roTnFt zQP^1f)~!{}9mK%bIZ*8`N?UF(l^riMMLZ*ZgDu}OIsB{Bp4;lIO4s9R7FHJ((JrBC z>aJk0uegFXk!=6a;@nncFVl-ThjTD+>?3H;5_vsWdM_hoR=cZ=s;9RlJx!lMHP&cN zb18&-UUS?CTff{Gr9QuD?(&X9dQUhcG&_TcdX(?-WDKZvm(cun5{=9SxeIhTzB$mg z&&=vOBHg`?=_VS9I<({4j zSwq+5DX670ZtjUpy62M9^&uxtHxi)CUcSrl?%GwhwW8ED(2)?W%OO(NHA7>yA}cF1 zD^uTAv9+{>{t|kx($i^}+sjG{V~Z=woUuEdC9dM?SbA2fwpDPu-LZwm&cf{^AUEGQ zYGzVWVqsk2tc1jPhMd!8CDVBv%#VZEF2gw*6?kt6C0Ur>Dzb*?}mPIMQ&bDEw&0)Bkwx!Rt=E_v!GucdUQ9 zA?xF=;CFV};(tBmxlb)2HyxjO=UwkTu;AJy^n(<+cocTTZZC6fGky+43`@Lu)^Gmk zP9FJO>z9wb`ljFMjb*R@vH6-Q<%xf7y}qJ6@L+RG_0f%gYiY z-wk%%yyV-zJyv-2)y`98z13eXuWsMmd-wA@EVG9%o^or!O$B%R<-Xy)ASZ+G#+9)W z^H{_Cxq<8Ar!OeHcIt}JpUwO@;BRfC-f8Z7EC0~J8}^*}d{5))DR(}c`g!`d@*iCG z+MCXTvoCCT=C21%OdWRZO{IJCPK|v2ZvQdYzWa8=u>+s&xGzyVQ2)~SMd9ne`_2dd zp0}m^x{v=jJ?lVn;!E9CKYaAsj)k$;XC8j&C#8XpO&%Q(o-}>+u?sLr(t&OLjEV^a% z^wnQJv7_y?FBaX^IQG4at^dB^<;7prtk>_kZQQT#xIOOVXT@7~EZG_Haqj)GQ4eI? z9rNSA@BeLSYIuycsCo9w8#7-iIJ>fQ@6VrENIy^^mKL&O)>8=c0;zHq(qZhFP2ZE# z*F!$=o>*O2#hcCZGN5E%E#Cv)9!`5Gyfqn^pu@Ld`wq->=#rx&U&j8TH`o7KcNpk9H~Qm9 z2<(1j$tSBKp7%PhrIKs%D{6S`AXLH`kY;uDuoR9!6tqGyoB}z_)CB9H8`7WyYM>vaSyUZlLMKGResDl9 zgh3+aLhXi;Es^A|G2;YM%;8mCokHXdPR~Q3#gB;|18`9xta1Hz$q&?YpU@`m> zroaIxfHh6!Cki(kq!aR5Zz71aj z_61@uEP$WGM7SU9@COKm+d*1io`86G621i=Ljc?a%ivcq4IY6K_zR4NyI=$S4ral# zunRr|X@hqoWWmcY86E;Bd;lZh4p;+kKr%cH)$mV{_IbC!a`-jOgdam0{2j0x5% z_rNB22WG=_up2%HfA}sefmdKEJPh05&oBz^gmv&1q{1_>6aEF#@Z>gF39mscJOLH( z5lE}lA3;962lL@YxDL(R~AyfhcH& zVmJli&;;wD8`7WyYM>uXPzRaN36ZcL9MB74&`0z3s(@DB)t@4*%DDolq*;cECR zjDfphBfJgi@H4mu{tbTc9as#%geh0zr#4V7dFH1VJ1O8L&V- zEQKQw1+7pFryv}fU_Eq08gxJn^n(fNAQL(v683`wdLaxNAs4zJ3EII8ePD%skPXKm z8rq-~PD2DVLmu?N95@7f;4G+cJuHM@z$AD8w!-@`9KH{$;B`oVr=SY{0fF#6xB^~< z>F_9A4S$6(a5rp(w;>&V2G_v9!4JLzi{Y0r1r9(F{0T!8@>bu_QC@AIZTB6!47|bP`Dkgf)fxAPr|q0V+eqo zU>W=hrokgn0)K(ga2ITV-@z<+7IwjBV1^qZ3tooF@DMoR0~i5!z#4c1lHqBnhJQjZ z+ycwt*Dw=)3}x_l7zg*lX81kKh3DZ~_yRPz0T#haa5+2(h44qX41NHs;kS?oKLHnf z0zq&yTnR^E20RAa;cqY&?txA44$Ow;YGL(&Uvc*hilOcI1Di2!|$E58aRk9Z&=PV1hcxgieTr{osIJ2!lq*g)T^fc5p)< zSYaPz!!d}4HYkPD5CP4Q2R$$c4#6Hc3o2X>3*i?q2_As0@IDNO@53s19TMOvsDgh$ zAbbz5fLCEUJPKFCUttW~4IANYNQa-nHSllngYUp%_$5q%15gBif|2k;SPO4L3j7py zz^5<_ZiN-_8;F6&p&UMh@o*n(fp=jZya3;ZFF}F5umFAz6XAZa!yh0NZilPj1jNIW z@GbZl0^lZC2ET%7@CcN^Utl!c1smXZFbkfAUGN#0;YP@UmtitI1Wxz>M!+4g2Ht>V zcp9qVpAZbUz;gIC%!D698T=i_!M(5h{1GmLAHZt( zEhNHEzy+T`5Znw`!cmw3kHL2M8;pf}U=zFpv*9_|4WENQd>59$D=-xvhHda?7zKC2 zI(Q3G;ThNo|AG*>4OYTy5DQO01$+b(;75=T@47NVvke z&9|CkuW~t;mAj3}OhayvG0rs8;beS(ZYEj$;b2#Y;~&PFCQOgEiHqn3-aFR90ERtSh>-^qnP4;=>87k^ya^l9 z(vmWari{60PRF(Lr_R%(^-Y`8({~q^R=DV9Q9&^+|eBd6YgE zQ`}OQbGN=**RPGMi774JQ5N&Hl9>@d(s{Rxo+SexLgGf7Nlm{mR!oukSp*@O;Pnwteag<e-jzMlM*{0KJ zEe9dD+8w^JM7dfbr>yEawU%#OaFkTUZmq!Fe5cbP3(9X$cKpY&Vf&7fZFJ%i{4&GN z^t`dqsA=E$GI+Lzm?qK@=rKW+52mAJhqN@@x=X(XL*Z|JCTYnU>!M?wi@A94TG~O* zW02dhshu)K4wOpsNV@PZ_Uj!To;X*J)A8!v^!WA}m9KKTs!J*B&NnJA*9$28yhi0l zG5B>0vLO%urH9cOw>z(!-ILLWq$e>etd>3U9*cR3cG;C*WfZu&-AHayM_O?H|B~K= zmiGTO>W-cEs(~Er8B)0PHTuRLVmz~gy_N&s4F=hL?uHdK^M#G6-G$Z68IWTZo=t`& zD!i*kl6hR^sxjrmyIE1_^HWh+THes+Kv_ zn8v>>69Wjw+vUt{#=^Ysp9{lesyKtB_G@@>V-VB$4EndT(;Wugr1IO_)^bEHRxR9bwdl?Un!!VZi4h2dR3|!XaE}oed%gCK~ zDmhl|axOWZ2Is05$r(1~PWR4=s_jE3$_(*jr-0{mD~++eu8baXhvZ-;Mjiy<+cNC6 zeN)tZfjTgWz%F9Dvs|w%sdRemXmoq2Oxazerp&n&MABnyztd9{lPT!2{&tj5kztH< z7jHFLI(WARR*dVG(*<_P0B2T|m+tbIj2ILCbxIQ7#CxoGN~+5Ya}v)!OjjzU2Odi! z*ulC|lrv^q40Hdn6Dr@XOMlZSk*O8>E=q<%m95-SIxv;r^HQy}W9;Lk`rOgVe>V=c~VaPH|!I#>AsbJhRMKD8@O)NLk}c4c=#rE3wPl=bQI7IOu-iL^blU zGhJWJRq_BbQjc|$ojQF5^<7dmTCb`u_q_ZH&MyMy%1K3xrIRvtow-+AE3h{hc=BTc z^YdlvDBqYuk#88B z=3Y;trR&>DE4Jcl)Ya^AYF3%u^A!wf3z3_>I`E?xpZDB9kv}~42)>zpmrQ-; zQpu@n3oB~o$Z1v!%6H8pGBV8_^QL;{k9nuQr3ZS_^ea;?zJPqWqok^$+^~??!Gtd3 z5i_cDo+*v?!K4ff^h}+Tx73(iWNfCV+e>%ace(P7@-J#yv~H}(NCtY}3j>GkESBYg zw80`*OG^k(cDne8LeGrG@{01Ao(XT4^n79R#Kgq%Z{X${W(^)gIi4u*W1-LV+*v(+ z(LnjW%4rnuLmil$x0G*_tQq_?$qG;5Kg4Cw{{m0ZzoNqJb@P0QWLfzdm(zP(CdKCB?t0=@*Qid;vUxr#jG^8S&kWFwk%e?k&qr`243K`)ybxi zl|fU)(%C^-XB4b!2H*8-2FjDHilUX(?v+K$jnZ_bEKS!8El1^>UzU~mRmMX_GycL{ zL-Nt4DUGErQxjX^;?Xj!1Y@NEZ7eShzDaYwgKqb|yJPfaF3(QAjQPM6Q}h30=u}7L z6xkuA;Wj6;l*=8=*E3?7u2Zwy5nJtX8!V@J(UgD1F8Mk?Z`Z(!3)7_%I!g=>GIwDb zTduF#na%2djHTh1ZZ0Nd=U45NTg3U|W)(A`tIOqHrz48DzS}v?m?n&cmD?`o3uEZX zqs~3AsCH4=*h-Z~udFC>F-_O;b(>M?-dV>~YRa9I?N~L-w=Y!@Pu0z^z`CdcdEOoM zltHqtX)n#kYKv>RWRZRG1>#v+ml{1r(&Y5YtPT0AvsUD;U64I6Yt{GK{m(^@(L9PJ zU0m`dd12yzjbs0}qhjTRb+U7{t4f-dlePGE)&*NJTdPY-9WzTDu|qO>nZ1(Y*+qvg z--(j8bFy+wr?z46C0nYYp604y03}y#-ff>=cESE&@AfdSj_~S*{ZT%5pN3p;oG)Ak zLX0KNtE0U-%B#L`1H9XTUiHP#=g)VXFJ8WQhI##4ygJsaK7YP&Mta?S@%8nm`P_Zs z`P_Z+_l4^V$CsYI>-FhyZ}`6b7had|cwhLw>-3%X!foI2zVLkE`>x9uZ=XM3xW4%M z($nYeQ(t=ej`OK6eBbfD^Z54ruG5z;zVLkNyAI#Bum8~J-u)!~lkMlu&FMdPZaL(^ zcDNR9g}cGV{s+;=p#xrq*PsVZ!DkTk*|~G0Ar@xCNy1%>u7r)?ggsCXjc|(n2hesn z46nia@OS73^S?PSi~xD<5osQ z0{IWSF5&zC?nG(isn@0JlU-EFCc6Sf{u<-c2KllpuFQGCCLOex2TzsV^YZ&0!gF+f zB`+Cgg7AL$z}SkHg%vLun)ZD2!q?BCyX>^|>E8D=oM}u|GTEZa=@~;{K89?fdqR`G zd>sV?{zM@6iK#&>!El)}ajIPZ@5^*P`1PNbfFL{se?3p0a&{@nT@1O92Z#7hgDeLc zp&2@0KeWT|xRDlm`-fpFITQ21D>3!l#dzB-nBD#eUk_t$+ROB<2qjtB%6Fx{{@;(3 zHswhM>P#1XtZj%xNGQ{)E*mC7L*@5iM2uz|B|?V(rzLzW9>xo1^oivx!IARQcoY4n z#jg~NN~7{NzpF6!d{?Z;XmX2K$)6o_*xigCe!%tq8MDeWm~Yl%{`;A@5hGTniz-13 z5RTv)Cn{5vn=x2!!svBBhPVgC3}&}o#<=7+`Afm<_M7~T;NEI6DP2#zU@CPKez)AcZyNN{?jr%3=bn0kbgGt&9VROZ?lrGctWr$;O#KYm){#L zm!|%ie8prj-4OEQDRd<`F~X;RhoPT9@t=aB1BuqV^Baf5w}SWvrTpyB{}>S$t)qO; ze1rcn1YZ*VuQB-Q;}gDKT9N|!g+=!N_59w$^S?SCUoRSTL~&P*0pI^IB|Sq019>9f zRrF1->3Q!2}`U64)K z|I5=%Ig(W_%?=znlp~&*mb5x$F9LmC?4Tz}P)KAg&Ckiom){(tM?khq&+}pQmJbv{ zgZ^bNwe(MW)Yk-+1`x8x-Lq#Q^%0rcaf8RmD#-uk98_Gk$&%u~#EA=ClbyAYk1q+n zvSf65Fu$nOxfK_>!+n`t3>J--*hwI(FABtUaUf1v^w7l58Vr{-T$z(!#AM)YMrsXw zXAO%3&%VkMC*1($jE7vjkC9A@1H zG5IMdBioW8jBhyz-}GTmLiw_^ah={Ej4_TFQHg0)rKpJ$HSwY*LDVFQnj}$^ENW6j zO{%C#6E(BgC3mww9uM*Oh{sDje&X>IkFR*V#p5ple+e=i{3YOz56IT=P2HLV{3YOz zTv${oN~?q;PB`L)gF1Pla3l#wvT&pbN2+k73CApU$=&Sdd=Ad%;Cv3w=iqz}&gbBK z4$kM`d=Ad%;Cv3w=iqz}&gbBK4$kM`e2xVCCE_nJe&9(TxKWEUvy2WXIX3S7q?V9w zJh=3SmC_$&*B33z65ur=IRDF5#*Sr~(!b2WG8n_jdt-g>@p4=&kx4jT(k_I%;a+Hg z2jKub0&VaFJOxif2ONUK&)?FIG`9xp%UCs1AAad zUG6K|mHBi9uj0ewnBrW}#2m86Xl)u_8yskq+DlxNeuYlo99M=icv=U0a>-G?PfHGj zyTs|vHZlUz>ht=YY@6 z=V?eEAU)r50-(k-5JBErLT!7cSI*PUhU!g%_&FRf!UhPdav*%KirWN=M`$o+C#ritvAIe)3l zI6r+VGtS#zdv5-D+v39QFE7}yWek45f5E~5yQoS>z|R)M4sGzsYno$u(hjab`2h08R=BXk;ItS2GA^&~F z;_Eh)yNxbG<0ru5%` zZw$M}jFG>WUe4R^>q5P>SC1=wB?F7mwQBsxE)*7ihwdNnXsq};aOH;4DQ)}&c%1ni zy8rwmFR`^V<2=XObJwk?@LC>7qbK>HT`D%fKG*2(ysmieA$hu~vQ<)@ULcrXfa!N!F;0kGU8}8fHtzf1^Pq%wrQG7+aLp zdCyqAybNQQ6FUmmE-_?i5*rx#|7OrfNtrv~OMae{|IDJQgqK5gx!pHv$bnR+<$s#N z0IM(tM~9lAaQsjM9dFQLU4adIY1+9;+W?#8u`n!+6Vcdu7axOy~RDMMV{H9k{K0JmSLL@`yr= z9%#*Zj2?valL}=otobmhb=1@pfld6k@|J81u(!N_U1s}%p-hY{*-WYlw_`$m)JN1p0rA!&(VMI!fSi6bn;FoKC zc6kFibh*dqYOu5zJm*e~W#~MVAApigOP43KfhZXlK9D2Ma~|wYSZy5S2}B++xJO2D zaBJZ3BEH!~8FIc~-?MqCokl_6O{Rf|`24tejY9?=f8X}d$Ny60OS#7mW@vHZwS1H} z+GAkm`OkZXJR06Brl=|OSlw_Ny>2pAgN`(bjjNRQ+#Vw%PYN^nq}pv1Q-f~>pKYWZ zxshKF zQct=2&C0yIcw(24iR;JqQz|MH;aR2606?N((I?~_p`M7DQ$iO zSADKISn+H1vm6TaUA1veOPZy?_$t7;<`GJ0gsoUaGb-w~)G5lI^NzI$B9v{b@f}Tk zL%C`4##v5UxHI)+V;R?TicPfR-mX}<)I-V~!!1;tA^K<3s6GqPC$3(Y&pGA zbeik%e8jI_>&z3~0paXD6VOEqs&j;;ZKF6gB8oH}VX50B&W>m&3J1qV_UDP-36@h^ z#OVq9XpZ#9S(`SCy7*kaq7<*Q+MHl%+9bO9-7H!YkE?v(qH$KMse7~NnP+9T@u_*8 z1k{!p#TmOYbI7(?Rwj}5X4!Dqzch#SzNIoZFSE6-6Ya|!JRDanW%cA0GOoQ>)+%iD zW@q4Kd9BL&{*@8T8C`i&uD7gWc)EVI^nZLc&Zk$WNguhHWUt(?#?9hvu8r?9t+BG& zyvEwLNwlr8_N)_!*4Vl@WR109tvI>HL?lkFiR8$GYb|HiD!pq>r`IaY>rDICDQDMN znl>o=*P9wQC>h zXF8mx9Ln1#lY6tJYqQe0#oD_?Y1vZad&gVMYD@b}qQPWkz1ifJSy3K)b)2o= zCh~0EHc_XAwb*zXBUtY=b#le^{sm1o(PmlSA0p}kPRjcnn0qWlbOh<-tDrbmPg`wx zX$iK~*_d2y?F|vV!R-ng%^_jzXb%bF*drmwIo3GedWNORB8s&>kW#am^<%2-NB~9Z z8MQA!booV7X8YUZUVqcc0MX;$s<2TfTe?jF@~E}|6CT?GOspRYu&~}4P(awtZJKgG zi$1M&Y!bcZK8>fN(`stlKpd=RH;Ud6Tki&nQcK@P(G|**F51VKIyZ@ZV=a9fMB~_6 zlC^QX_4EeOGQOK=^-nPMt`}z}?z8le_hwiQtrdr&<$6nuwR^2-jJ5Qx6$cY79cx8T zvZZ~k=uK|pu!dA?=UUO3YHQjc>eEbh8^r!}OT$LdmflJ;ziW}DXT9iOWNO+d`ZKM) z>qPxx8*|;87V~HwS!_MEUUV&%Wn%9V`Sf)xwKlI4bywCa9qYxhY)jobv2S^gDmSjG zY^P{gRd9BbIJ7#B^)ss@SZ~P9V7)8X+Otud&b8rEyT*EUgJ@Wj!FtykD{0WX#@f6= z^sTWH#k#fDv+G6U+6X=vaAd8eYpv41)^adUX<27Ew2mj*+Ot{dTxU7CPH9>n#`>}K zmiA4`>GhWW^-AXk%gGJO@eLN-Yd2ctyz4_$<~rZ31{iZ5_Kc@bH`f8 zTbmb)x(U`JS)yS=H@!lqW)>XG62}uQhqFX?;%T0$&UEY9Owl#RdMZ<#oKt5aEi>Y3 zmx_Z63eGMTjSC}KZ(kU}de1@|>-`IDtnXWt!TPa9ltMj=GH__gv=Z>aOlwn?IGkxE zBF8hW4OyZmvtPc+WPY2bTz*Vl-|sJun=O6*;+Vg+(L#O=JL@kFSuDpaN~dMu%_IGf zR>G1@k62B~ibU zb%bs|6Yf2J?ewoTOt9=r62~T3TarZM<<`zbQ5R)tNfgJUtVa?>UyQXqk#7-MYm-Dn zqP2s)$tLO@^~oISyBEZ7#2ibbA{-`)!w$vNrBDfr^m!nDQ9?e(#}(@lv*=Oe8~v2Y z#QGUi7;m9^b6AU6G@F+a_j8Kt*l^4KXweg9?T8kKMpzF;i`H=4>6v2R7|WrVqH#pu2I+B#>7Ly^|jnWAp8wVvOp*5lElYnp{)8>XLBx@L;L z8CEvxqOHvY6m4yd7A?`ne`m~jFBiOQ!9t80G;5<-oYt%jX8De#;5%-rRc^m zOXo1rXSTKt6Gtr8)?uQ{a*(34ZkXOXjGSmaVpCde)=rz!ZW~M{jvc3rYEo=G>@BLb zUsb3b_Nhv*I@HfO#kya$^qZ&}$>y79bdE>GulS4%F zY&0mQW(C8h!K`8I9}z9%9#>ENI~2^O1Fwj`PoFty8Sjx*I&#Agb)p)~;ZsH`vk_teg%WNID}8l57WgdK7D? zLP03wCiAb&A?9$ul(E|E{S#zM6nn-8sCcv9j2=hZ(Nky#dKT?M!*r_SXf$elkm@&T zL)W7@XenBW)}n5-5j}$*MElTA)JDqopwVa_nuJ;=(o8^gGzU#WbJ3;fdNdD}Pf;aW zgVv+PVM9-jk4=ciYIp=F7eAM~~;iG+M4r+Ro@X;`|28~4bp-E^9nu)ff zx#$tpf%c$#&_1*gwMO#bqmgJQItT4RbJ2dZ6g53Yx}!F<5sg4w(J1r~nuH!lGtgdi zAKH(0qt?m9i$YdM+tDO6^T(t=+JP3x{f`rWGy`owkDzVnz9$rM3~fPs(N?q{okO+@ zn?frKjYH>Pu9}TzqYg9=-GjQ(MzkJnLz~e~v>ok1kD-0&DbzX@d)lW+2Q(6$gJz<+ z=pM8bwf=VyKU2g>wxgdTer%^bPrhy7zPx}=)T2#kGn)2u(hEI_ zoEqIIjgbEr8 zd1yOYgZ83SVnh^1)>LeS{xOh;(cC-VvzQ=Qec6>m25=%7DaI_DNL;KN8G~!Q`gJ>mMgSMgj(5OFipU}hTF*N%W z@k6`OvuNaBxL(TTCNvuDL^IJdXdZeNtwh8AO8n3?v<1yWJJ3?}I9iYPqOIsz^bi^r z&np{^M!V1qv>(kyqyENyN0*{|P&e9uHlzE|BWMTOj~+u!e^qpbjN;Dg-m;2EcwDS|IS+te;ZupzG0Wv=%Ku>rpq_ zf$l?lP%7IZ@?XRcEk%!`t^MRrH12cKFNyU1g7~00UveDUgziW4nAC9!ZIF{YlDS`+ zD#FoPG#cH9$}brGR})uhN;5cfIC{pb|>15T1Tj& z9nD3Lp{LQ)Xdh}y<2)l(5r?*pQl-_GXhX3!qeVdc(VEfJjM3sT_@6~NfQF+z<5W?J zo}8f4TgClB+tA$0=}<$PB1x}w?(<~Q35}YfifmL&Q$;D-i`Jt@rV~%;K7)9oN$4@O z4(&w`p=Z&P393kaHT)(WR?Yu^v5x)}Zyd zs@RYAtmQZ~Y#s4O`_`+%%9r}uH*p-=m8XgVw3YwEy$*G6QAG=ylTW&#Jq1)g(ONtC zV?NivmE+L5LRI9U$BM{bs3=xN2il5uqctUT^P(A7t0F9ec$KLl6OF9kKFRGW&V$-q zq%)d<9ziS79<&8LgC0js3n)j>a5U1*{X=um3^cEr_@kX@4cd)1NZmnxk?uRWeyKI2 z8``#u>t9GbuOS??aX0xuZley=y+<{^*c^8)`GD=yXb0NzZB=xkIoA;{v;eg%BEG1O z)}m=>1Db=jpvCAxv=*(YRYene7;Qs)_Hw;w%H8ik%gn^9{fcey_F2-=1^>InyJzKQ&Ww%trRWl?_ZBY&dZXb!61!u6p^=pHl|ZA6RFHuNBR z80|#6(QfpN)LXgli;3@T_;QfdUOuj zgyx_JQ3o3RF82#Pj5eV4C%I2(Loe5b?nf=z#Pb8v7cD^Npxx+twChjg8?@;Z>4LVP z9dbW<96g1elJ0-uIa^M+e6Pkv$q1ou!)5HT6ePkd`lE55aJ}eZv&8VL64xl=tL=D=B?nArK7PM`q zCc4n(SjPO&!>Dx?`8ZA!k!UWOhBl+w=&^WB6r*7Yn%INxLz~b;=s`3mO%o^4CbSPd zh+0-t51FNj2(%qdLetW@KD2f=;h~4;GAMxN%xB;YZCXIQ(2hl#IE$t&)kJhI@wtNQ zK@VQ32{#(OTocV`^9oILqQxso5406MjV9%g9&1QXGy<)=iu0o#Xf}EVEkGky5gwX} z)}wB;8Er)mqP=J*YFkbC=p6Jk>OjR>{GnlJ7aE0Hay2mrjYD(LJk){Kp*841bRXJ{ zwxIneJ@F!ZjYe-a_Z_9jUbJuIeyk&&Xe3&&Ni)8X+>Pe3ot;N`=pnQbJ&7{PByu(r zU$hH7iT0p1>ztyh!o8z^^C9o4rJZ}b@Y|MB)d;B{41-v7COlHR1DTp(=& z1bPcmQjD6G0I3afTcQStLk6Q%tkWQIOtEShgGA}r3q(w9)KmgQ2qi#9b7Pr_zCojA zB$dnoAHtXfM7(Mf3rLJW{dY2VLxpL6a#xlIf2?|t6id7fmQ^IdDNz4qE`|2=C;`xfE~ zIt&edlK$nNsV`^{S^%wt>I>h8p;geHNG`q^+MZX#3{7&)_^qworLE6 z1NPj89%vM*FQwN)E1~Oe$6vx5C@-||Ys4kA6WRxzgbqW)cTjF!Gn?FgC&-3Kj(?&>8Tpqqb6zR)&kCp6ZN{m{~1V)v(LCy!${ zwEaH<)_Q2puaSe+LHl(5Tk7F@?0^rd=mu!}81WA+`fu7Rbp0Xxpz8_B^^f=ong=cZAIb&odx^M& z*1t^t(D5na23qne{`w4le2sPs-S;~ALmOsj575>(*mQ;lk5F&WW-Dmr)zD7Ug4Sly zlexDFZ9gMujY4~&)6m1v&?d^Ai##*}Erzy2E1@IM&ElUGv>KoV&{k+Qv>RFv9e_4L z_d(mC)6gDh)(zydENFGmPG|Ch);M&_JAzg|coVb)+5*k{4(;rnp9SCTI@46T6XLaU(>XzrcF#f3qu zp6i*5$Ojs_IB2y?4%!1PgbqSWq5Gh<&`D?=H269E0?mQutwuj|09p^-TY}%A`_@uE z@@Za2IakojY8;#kDw14s!X%?>Ke+&zpX)N5j5wcY1Vq^ z1hgL7`LQ(CkhK46(yT#fKXjM$UYo{Rk~n~d>(KwnG^+sG3Y~;M_G#?>E^+ypG^-Lk zd;@kuCvQo!I-zx2)6B2Vgl@xbu7{e^tQlziH`A=hO~^rupjF>WW6Z-p&<)UPZY(xK zD|Vz=z0i^Gq*+7c6XmvJ@Lue{i*kXtL9@R^xuE&by1Ub?3h4TK(yUtH52sl@(8dmK ztU~w2@%PQhb){J)&~PvILam>sSsl9mIr%}GenGjRMg3`3UOjd~%b|I{q&~=h0J??i z?*AYiwB|p#5em)!RhqRK{xo!g>n+^G?cjPCn)7A){U_3_VXiBG&GmiQ@f-Ak7Y(IZ z)m%41o1sNdrdd_s(ce){u1oh)&Nlo9Ef@WLn$-e-^B;&KXd84dwB(P}^H-=ZXd7}H z#_$W*wSU3C(Bc0kPN0zq_|V}0kRI9!9f7WYiE={+poO>4Kfg?zKzpJ3rJS)z@`bj% zLj6LEr$`Uo1s#Lt{gpV`f*sIt^3Qvfc;R{+x*l5e8rSzz&ph9yUw-PIrQBRkK)boN z{+4Dnab55R^$l&|`L!YN;v?7v-Efrj|BPKw{c^~zV7k=+ZAweG!aHey>FHKC*R{|A zXmcpt3VoGvDI?t~ffi<_TQ$(l3)8I@=oV-vbT4#J*IDV-476%dx|O#Tze1zXz0eKN z@{`FAS_y4~_Jv6Y9e|eZ#E++@TccdJL1SExotAD*b3G32<+@-A_QYskr>9#5&{60l zc6A z&@pH;bOzcA&AOd+Jv0LCh895wpk>fu=mzLsXf1RC+5nw_wn7Kb$3M`qccBkjQ;=@u zG*G|qPPZzc-OvW;KIkrJ=soGy2s8p6hn7OaUt=Bw&4boL%b{DK)zHX$@f)-O+7GQP z#6Qp;Xx<(4>nrgCG!MENS_F+jH$yw2P0(Rz4|E1P0*!o~xPcZytyPo{S_5r@)?GmT zH{x%oehn&iA@K1C&ertFU)F z{app|20aYzgjQTleM74&sTb_GK1RJlOFxNUq0<}j$2Td*b?ArIU5{R9Zgsk~4_XGD zgmyr4zePKQmOu+WO}U^=&_?JCv=>_QkJKx)85;gJ@c}J?4nZrR#RJq=J9__vdV=;s z+jRYF{0oi!mU?-RejHi`og7ZLs-WSgC>OL9+72Ct_COo=kPbQuoq*~H+r;vHj!sr3+v&p8!QUdA#9MpeP9u&4q^NF8~3D@-IM(71*3b%k&7aC z`dfl`rSA+$KFf#CZ%^hWdm~`^V5#LO1}{j#rK1A85In@W4(X@{%L7aG+ZG4+maQ2q z8(A;b0TxWbdcmeChw{{+e1^a#T@0Tyoiz9uxcp#bJ@{ep(RuJ}zwM(``2z6adE`sM z2UBpR-2kq-b8Qr@1yenH%hv$5D+Ox>YXi$8jSi*V<;Z!len+1d8vz?iNy}EMi4U*b zVX%o5xp08~AO*_<%fkQOw8db#9!zyx4yKq;Y*^)4XN7nA?+`>VgW$3;%j2(oU|UkKNwB&UESSca zkb>oaRi|JDU>j1fC|D&}xhG$iJ;twItOjf#MQ#h&VX$UTT9zVK{nwM2eA)pv4OS;P zPhZ*xUeQQQrQjpr9pGLcsh*gITb%sg?7Jqcj%tXomsMh zDV5a=U+1zxeT)1a1@8fO`#a^s@M(2|wMf>{wG#pHYT-q`;j?1%@S5N?%)?Wjo$$ut zDNi?l;r-yF^Wb~I_kz3e>Gp+_4(=_7&O-~BMY5c0t|{CfDcO6QDwsuS53 zgbu+ob?DS>0eCJzAGpREY%VPG21RBd#*>*(k3942@Aj82G<{<^@}=M884ARauQ2jE zLz(_Y2}+7_kiRJeT!$LI7eTkRQc}=`Rk23+xC8BT97gRC|wjSrJ_}) zz2H6I-uhJDli=Op?VKOa#t5vO3}XJ93CEJVaIO$8~)(zecWZU&(`&)U9Av<$-a@cieC$^M^&nB^#!*fnblM;`-1Y=d+n`{$qU$YB*J$R5LI^?^4ure@jJ?nfg zcqzEbivFnPs=_B79bS3a6Cya3XN4_q=7nk>@?RdjMtEGBLure_w#<`n1$g~Dcnx?R zxZ(zXC+5yE@EEwaOf6s~|8i{KwadXP9UZP;2Egl)H@e};=DlEzU^>?!nei_{ zx2}OV1g~5?4%wqO`hxLfJ-h3P7VwaZBd0QSf`!4ne(D3Wz)TsP@<=a_Hd`~~rF!-H zdcwh7pBQ=)?EqP+(jhDxYy!+%-}zvpVA3z0uJ5DZ`@p?+Rf4Ik-acJ=H-is@yY(dd zVvgQQwS)3S#=Y)X*#v-6~-TII}cn`!H zLe{NMrI`Wi2dhwePk$!e*(Vbd@YajBc~Mk#UBGDpcu_acTr!8SHY{rPAJZ02ZL?d0 zZQ};yyMLS%e=#-S}{0sR_If+-rL~*e-jeVuUx_$ zlHUxL^;2ga>yejF8l?-K{A^V&inzXN2d{T=#$nf2z2HsYS(4|Fjv=rHFy-a8QQ>0_ zZhYwJCk}%*BcF))^8)Y|aQRDzbVnW8hyZ6kCVV}32eLZ%+ENSNo`Oqv zqoc=L_ibP*qg(fq>vrS{rI$n409Y?_5dj>+_JR$78Gkz11lT?>_3Jt$r>`7MrpRT3 zg?^UoKUCg)uspCVyPry|58U=x20#3Bp8J(feC&?5Rp3?NUOz}*J@^LjeUfKfu@#1P zX5OD=>aP`kP9Jkm@ts(4=c_&Ndf=tj+Yop^xHm7IkAe4roANsKFTWgi^k{8LdmcSe zdsfYeOSjhHqv#)Zl!S{xDW(Oub-lcLP zq#Gw))-Zjen+{tt12d{Uvo2FPvX=0VcQU*_QoZMa7lMy)PWd8A>hG~r%Fa^wWAJCh z=Mc6YZ18F3{Z75f_RV0^&*;@yE=m`*`zq$zE@#&#O*e*>AlgB?&A+36vu!_qTqQflw*>Kov z$E@n6pEM=BN24xWM-hJ`(gSu_KCCJ2`f`sI%X} zjzhnZm7OY65pu1gyc1%}Rm`aZ|2{|WeiJ)Y$Zj~09EVk(bzpU1Wp&l16|gE4rOFFQQgY8NwL zd6UQP8->qAJ-FhCe9ZVM`F!vy@CqAGjFnOF))YNTyB@q3Jdb@h^i-sDb?T-w1MTU_ z+NCIp=VsD%ypFB%1Bc4q0aiDg+V*G+q!E7r{=_kSyKRa;2EY1m%(ZOaYJAEJwAfw0 z_(A?f4j*5C9{ko5@Jr!`-{jpdTfdnXlu13hHQ%U)U$c;B(K(kd68nO@tncm%%EwK} zSy?ss}bd%*|5a~023^X*$C*J_Abp|j{) zPv*IK&b{j&&CA5khhH4#y@Pr9%C8Lm0Q~&7nO{BfeW&r<|2+Cs=2rMo7RCcMzt$cp zcbc_v54>@B^VzEYX#{@lVxF;Aeau^SdzGrXvCd{qzNBk$}8^_o9_d?_}I;19w#Eh`dsC#qQ$BD-th=bg`cA8(_b$nJLJJKp`)c1!mVvb)|xUB3<8>T3@pU$%-E z<6MWZ@N&iqutI~_SRPny3RVo(l!BFmb*5ldU;|*XPlxiT1KS6t^63!P2sY`FlP}xA zLKpDPnU9Nk){1@br{UMY)|lw7xktcDi+IP!!8Ny&z6tOsxFvn_thuw!A*L?mT|A|$ zOzt--Z9cq7c=M$dzYPBHMZ6DW(#~Dai(do3mG>?WABW!vzpmKLdv1D_qZ59V_cXm_ zQY`j^mw*pRuV-u!Zy&q~czyHmX5clwZ+>3(xy0%k-gWY%bN6Hm;SIp+_VCo7$gXmD z>q~eCN<1@1vF94q;1%=WTfoca!CSzi^WeL{i|4@yzzgTW_krhw58AfU7VY)tH27Zd zGT|KZRfx_!>k`IG#fInE1(hXFy!R*Ng%yKE!3ZsLNJlwXAy}OdPhI45Sp%;TUW@df zpzn*o);A_5FHMbw^t;m%K9+*Z zPvzjF;HHo8j9Km+YBT))GGfN~F*&A*FTXUw@3`El_j28{v!Bh1!0UupcZH+7(!Nh} z7a^hfz)lJvyM~adxN@#9ly(ff9K2ffL%-)JCUMrzMfZnI+R%CA|6#@%MC>}T+e!qy z6FelGL**<2YXdWJ**nWc!$` zDV+|LdjM=0Oy}Mh*atoY9`WF^e;RxQ{CM_fqrBpyZ*Lq`o~6i7tmhqa<>SoTg>L}Q z`8e+e3wQcM6aNmrS#glk*EGYcxi%T&vbO`Q4XoV6&D{1UeFN|ZKf$*G9KL)1S!qX= z*C&r(R%-=q8@`E+x0shIFMWSya3k->a_()rDsMTw+$wa5=k5Q&YrqRkd6VPx7Vs_C zIex3P=hTXy7Vu7Ry0^ECALToMeDC%1)IWGn3a%KM2JZ%^J2Z!MgwCgJf(;2N(|mhp zQ2vX6*WsMw#Yzds!2){+FE1z2Ao9~z#6UwL2!3#|tCi@2Wo>nLP_OB<$8TMi+3%IEp&)B5AgYUwJ@S2sz&jzR4@o*_U`o>LBqcbNoX?F%y zPbJ{x;3W&Vc#GJPy_=C=|LtTMq&o&y2IjR_vaR5y;5yeKx(h4{rt;`entn%aOMnY! z4k+2Z;GN%b*34YX-U+b$JDCs2#~T(6d)7P7z5L{wLGro$>u;g&3v%%m<&!P?Mo(+& z+_K2O)!;4Q^Tm*CZGa#CZgL*3^0k8Hg4J_QpOUB&>Fa@45AU#eDdREC-5P;kc`s{c zQ_kbBvy5-wjbH9d_U|f3HrOawvGjZPfC|9*+}fP^oop=ypFzG?@}6-^wpPJwAa&(lxmf=j+Z^3FKt&V!5L4a2Jv&uLFCuM%E%yVFNjC+kjj)k-HkZ{O1ZRu5+K z@x-vcl{5yf^6QYVyTJ~FZ4lteSGtGbl{}dArQ}D!3c;Es@6?SupE?Y$!y_*p;r9}I zV1<(BkgxKyNLh2W z!P7T2j(4xH4}R0b&OF!go9rC{?*?y{&UyN?Y50*2CnlZxR^4Y&=oxU?<&BR#2TzTg z67a*wn=(3jR9BVYS^q-pvF~VApLnlFIvbE5eavY;=J_!_=cgygY#(*NukSy$FNnbJ zhc^Px_>cW7dv2cI=B^b~t}$e`>}I~L{=n(Km9M^K6@1*?Khkq+$$XVAcO~O0{1VRH zJ+CmQMd16uRaPC6Ed!edGjY8>IhI#}TLZKc<^2}Ekl*#4tiu18JHJr8O7AXs4dSV< zU%#k3V2@MDO)>WBWZwvKLxYaL=)Z)GOYalO@r0sVhrtTK_HrM5{%5&)Rp{D0l)oEjvvYv6}tD8YQT4a*CXQ{r()n)`<*@smTYYS%LXfx zEQkEk304MX<_9Tr#sPR6o|`Mb7pxh~Td%6itP6;TQO3+PF5Y4; zBRflwulaK_UttwsZD6ulhcJFH(P{_lG>9E9{AeThTN3>kVQa59n!)SAO<#z-;ecUUDigIwbbZOO=a=bQFaXlJ%v zcaaOxIqUQLX&{1K_p7J#$C#^i95;Am6x{ht~_Q4qn|nyb*Xq@KSZEJ}2Qt z()f;s(s3vsecP`P%*cD@YvM)VHNb0-uxAb^T_x});DyBFi11edHVS6$Jvq;;x_ycE zX_T%GnaTxxgN1V)5&jy%HfQm@76J3LZTWB)GS*1}JDxoA!~yUE@F;8@IsEB6gzLe~ zo?Xg#F%7S7k)sD$*%Vq$41z^C*C8FbVBKKVJ}y#p6vLZ2neUTGm_s_s!G?H&tjr*` zjw&$glw*CXetiqPN_ZWTPZ>+aZ-c)V{;qlW%C8T8^{IT5MSKon!_o=n*01!8IdVM` zDo@-e)3m~*3BHfAkjBI&n9}5cMNUiFX3wkaS(WOd7=8zQ<>}59<+BR#Ht=%cp82_W z`j+Dmy!9TQ`z*1(?-G&Jz3{_lI(>s@ej~q)z}pPZjRDE) z`<7e4R0cOM1%UDjQt{33rp22luRQqG+(>T`-|?yJl-?-3eefzhJn4;ay#ZeKS$xY! zyoBE5m@ePMkm*4tS2B)ogtvnCf|m++=Ikn4H~1L%sBjL+4uEx^Jy%xvKJb0uxe|1A zOUE?$D7fNZhp^Bk_zg_ybO_ToJcYS_6t56$3~ZEh?5c9d?K1FP%aZ=p_%9t*-~%qs zwXiy{Q7}ZzA*>N>7_7pR)lXG2}{}M5Wf$83;Z_m6aKYhL^}7u4=;CY;yS|L zBv=7hM130fVG{GVHhcCaz1i=l-JQcX*f^gr-y-;Z@Q-I#CH&HJll_*r?=a)HA#?BW>GL0G4y!TpOccd0-|^9le!cm0&UXIAz?Zh1bhNBC<(`XaiUem>XvW zV69*SU^5bW8*7n4#Ru^)^QM=5eSU=1Ejv0ngI2eylI zwI{`XM5j^hA3Jea;oi4N-kXsvHORz}Y2{o;l)o+f3%-kQenq$_<$a3dKL-~WK0EoS zl>NZ4t)m|uWmnSwsE}`=V_)(?Ugx1>dtDt|%Ut0qzDb4(yPwGP^E5I~S7q|}o&Ahi zKCA-dvMU(d?7XTH&;9Aa%LeDEJm)#C^~e@g;uFOHhkURZtnZ_9>mvrX%Yzjo*8T? zhw=-qqwRr>>3$VHOg>Ypf>oy3xyT)XA}34Z6vb(jv@QQYA8*@gKQpPf5DSc?66q;X z^O%?NuSGt;iZNaC9MaJM7P*eT&(DSFr!!62Ok%qRGI{;ca*&`F+8Ep6PzXF*@WyCWX<~4Xq$^i zJv1rm@3T)-#QkR8G(>uRBXU%0683%O#OD^rI+K*QpUk82r3;G?4N~Cf7a9AUI`pRV z*Cj+Vs}iTwA?Zp;ms`iT743A%_k;Bu{=THJ>bM5k!JFP*oa7c;;jIZ9=Y3~@aP}0@wkbl|$OoyrTN#^zT$J+6zIk}Gj$pE7 zR0pM!`5NCShU?6~Sbeq9BslWov+ z>83v!A$>_B->j5E^=Gn~W`iY8hweVPZu>p>Az~AGv#*-s_dss4-Kf3`khuhIuC?A9Q3G7Y(ZD{x3JS`cW&qg@{FvK&FMRLA>aOOXPhFv;-DX_ zw%M7BrPyx2cW=jx;$Q;V31nFUCF{vvoU5bv+8&rpm}}>gPvHmds z<;y|zw6;;OOjEY*ly(@{8*-8$A4Oya`I_(1Ce-*!7o8x+G6Pm-GCOzfp?EI2oObxb z+T>3}B8^Cip*CE+yuqH6Y1Z!CHoV8@Lu3#<#E*Tx75ie%?VH+<7 z?{sB7cm;Sz3a&EMfVYDO6%%H!!@k3#d>i1E|2*jf<=YBY1{UI+`bpT>4c-qP70#jl zX8>#hY`p+vHRZ`PZ%$CzrPSSid;T?nTy9@-{$+e}CFKS)^~E~h%$InyUTLH|0>2Rc zw5_|!ZKM7+#RU4nC0B;rVdR|tIQfiw%Uroy2Xu?}{HGkf$f_ObQ2HFO z(#Ml^BCG%`>cLd6QLv(cL|;oinK#3Xjt%fe;3=&R(OR%!uq=bvSOeH#3f2nNpMvcI z>rKJ>!MeeM9(^NVg-@_{Q@_fgeDxjuYQAIawdF8a6`1QI@xmWI7SlKPQ|0viePIR4 zfJ15ZE&h5iuPs$zHDD15aTM`a2c~Z-n|mh^WcloITXr{*zJzZ{m)mv|OU^q!j7>~V$R^XC`nG{_iT* zwtvJXl_^l-Q%|HiF9t8!N1tPt)tv|I4Bekqx@b5pQ0!Y&$eKkB`cIOtLB9G=&i;eO z8`F_!7cJ92Y|E7{bP}jc`hA4F{lu58zqVq|eJoypaK5Tz20>gQ{|%8Q_c_*f_P9Gb z-}{wgNyi%pDz9J#>&@p`hue9@Jhr*h7G$*~R&nh9u9!5FFA!sPn&i83nf_|GIAL^G zA-8WVIsTaV2O9%xflr*dzSD+*W(dxTL*JgU9l{retO%mJNEdx4-)OeeMNMCw>DTPT zltuMAjLh`AgZ6r$Tw_5COF?P_+e;T}4K-rg>Ui4cR zF~$UQ&*YeW0C##w&d$V^P%!)arKD?Z=UdozyPfZSOZ&SR^eIvFUXvNf_bqB(%GL($ z+3}RKBcry`g1*Uz`KGu!Qug@Gcb_u-QKvbV)0iiePJ^%fHGq6%KlyWhjK3P}9K2SC z5t|`R3u*G?FAl{{)<>ATjPgxx0p9o2QpRxoW=H-3zA0|YIr9xO(MZfUWY-4dDv{f4 z%O&Gn_2`7Qw;m(NHIt@^G!;t2{LYkJ-Gp85=AGgmWO|Vqk&OB-O?PMo=(1D}{Z`24 z=lNE<%}Wpg9E;K86(#W`hie5ydhUgKJO&D1Tqzw%voTZd<_%3gr&b^AAxMNl=0 ze8a1J$NvxM7H20zQ3Jm z^u>Ip6AJWdiJm|n_tQo8atkxhl)&UkiJtk!t2>a3R~Z2|EHZ0`BKXD2;46W*|JK(#=+UB#Zy!Ure+f)C>A#zFN}8NL!$^ z9P(EKSS#3x{3W|~A(tEB+uus)#(lpX_feoB_@nS^vkbraIP(`iSCtuv%(JGGE#YhE z2i~1#^>a=h$+pv(Y&(jXV&poItF-4z$-TCF7(DWW5@u*FU8uUMMs{dbn)~kj+9vzJz!%ypeYR|&>Ax{Zm7+Xb;1^we;ye{s`fZ>getRKHeG%Wdv!|4y%sO}8ySSQ-r}egL z9Nm+oDXdDf+DS6U-;K%m&ViM4Ej~w1`PhD;e{kl0MezDOI+9bTEce5K`&Et&$Q508 z;<}YT8jvfvf!`dk>q_s|X9l`48;6MB0Y9=i&Hd(u@t6JK8M}QAz_0oozr_$VwpPuV zn`t!_a@(W*;RxxRA=kH6h;pcpU`1vP(e}Hp6BdFQ0%2Wv62i`B-8?T=JK|WiL zT;&(}eFj@@Zku;odSZfWe}6*1t<*%Ceml*)u_haOkjvx2qas_*9xJOg040V>)7S1p zt`Ir1hCo*BeG;tfCVu}wX*q-iH!vRu>oADjesaL-zLe&EbGA0ITydxO$f_TVBG->x zz0%GZ3z*}`CVhd2*i%exMmFnaev3k~>WY{rYs{3MMtGC;jH}|6vd^5zFw<{btBny@ zru@h@ewp8-MzNlXv!Qv5v!V`%ciVP60iSE96EY;2B|0v zl&*Yu{af*o-Tx%-{n^uwszm>TF3GQ#u3Py%4`&QD^EfT`dYpD*W&V`4tn6+jP5t&n{2j0T7=H~Q+mCFC?Q2(7Gq`NqBs+Fg#z|y{n;7pn*OAL# za3lRF7~vuxnRqhak=k$9eIESKH=Xvu8YVT4?2Wr;6KlOn@AA&V0hYhcsYs>A+XIMr4}4OPq18Bf?)>6?>}prrGZ#>z!kAOwMI>1Uf zM}J~%z;IxeQc72f%m6Z3lHm}x9<0x!OMS{_upw8@)R|pJ4dDIY$$rPy(dOt-Idw>H zwuk4+WAj?l0UBb)0pvN2!cIW*@v zKL2#qGEiqA`IF+Zk~CxAPpl8;=r4j_2fwT{u?{^}Cu8FrrSz4uWtVIrozqVj@@Q3| zJtEq#`>@~ZEH^P8`8@tcNt4I#a20S)S*s|k`B{d_tYuYYbQG>!=+pFzZy~36D@3jU zIk&!*M=4k#n9d!4nmo#FSDHMOcC(Wj@Wz96Ua6qTW!lG{bwE-^p_^wN3QaB?myC&ksr3@J@sTK z=uB3Uu~R;-AkF$N=IVBu)I8Y3OKgjqJYvW<&XrHRhLu*9U)%DgqTg+SmHIDvaN1 z(XsvqiFLBwH`qN-ZK6`htCTDONdDR$t+PcZp zFU4%nr@j(wUJGJR$e*=rzP)?>)@S#wx3*bzT7hiukCO8rVbx%rDOf#NE12xlkc2g(k2Lux1JAxku0A$@Zr@?WxXMR-4i(sgh*R7W545A>M31oLCQ!y3MCa zdIr!_I+D=SeBypGh8{#x`bqUaS)V6no?*|H^OSh2_tuKqPcgE^&nEZrmA)LT0?e78 zdE;nKnq69!8exS6+$! z!Jakx%iRdocp>@hFHkmq3$=EBd8P$+XCnDLsYJdWdD-QZ#mxT@Ow8fL+w92AnU6TH zoV)!yQ#I2+wj$er&Y*O1$mhGj>O7eIsKfV_igNSoeGA)G?g<$vSHI&|!l~?=%;!7C z#C+)GPaQRf?(H7C2@_~c?%$XVlPGDc`Kx*t-=$Xi-zCy_&-+YCU?{0U59X0peMBdJ z`S0et|HOId@q~^!&jlu)rY4!jf_?mLzR+h4Ws>G$Ck<x7*)O z4}Cw8m-dv+exdFk^v3xvEB94G_ayiyy1&w$MmOd^<_3#B4r^}G{$IR9ud?0g+PP>l z@ov8PqP^WGcOz3iY=4I!bbCT}I8|0Y+lOrbQ+zW;dcT;EZ4Rc+*_>}iDvh~^v!8c_ z8NWiGNTi7!FHMv*rPON{&!9%rNb^}I4f`m^Jcg;QMxILwdio93uwq&Bs?eSGJEEaA z2_4CM*UppU$Y*lrhb>_{Hq_PzNLxeNM$SVE6KU-|Jnfg7Nu({WxR^%1Q$QIy}8Kt8^g`Q5Ni@En{__!SVp;0@T4E66#o(B#)PfXZ$ zs9ovaSZSO6eopAVguY|r2(P)%ygHAontl5|(jUH`XTW677ZT~aU3+xj$b_A7mv*Qr zH-FQli~XzpTzu%NM7lX`Nw;;KeRua6WMjC}7m;5+&lPG8A3Asb^v(vL*|>Kq%ZL!ECmiPM*Vv{$c4A8&rTo>A zPwnYEH>0v9+h6y5_lUVS!HtQmIbDV#S#i{dp7OJKF5^AuNw)hF+RL51kz;Rka}!PF zXKq-{4ZzAg^nKy_w-cA`>AWM9Jx@_pryI~$L$K?fV(5U$HT3X0sZg zy`ifUU7LAXsgLu}V+mb}IJ9p>QIqB!ANv8FD6*rZ%l`_$F`$_Iej=S4lWt$P&u;B{ zUQVw`&F%N{h=<18gI4<)==fqn#|iyRT=dTu7iuT^?Y_PIs_7`_p*s`$Jbk@;x7#g_ z-Py~h-K5>ZI6S7^3LHE0adQ-8Xig{ft8B2Z=Q1 zzMTC7FA8;b=IqBlO&iOi5_$^K%y*zX^SWdkt5zO~CSocrANy2!!|!J8%lV1M2=#rn z$d0_nULSeJ2+y-W=3}Ipd~(Zfg?ik;9f6%6tx&!6qNnJ+>^I7n7bMCzSC4&PwBFUE z$H0@S?1#c-e=v)6P@!Xga=b{C+a2R%e<8Ay$nN4i^g?2cn47Qe#yZm=V{$T6<1_UE z_2{Wu!`ejl|KvD&Z2O&CqXZ?X6-Pa!%`dU{(n3E-q)phHnD2S|!gA70kglF|mh${| zB3Mep~Lz(`}pdA7AEaUeUGWg?RoGgL!HIoYL#xV_Yp(? zotTfAXJoiXY3fXUNda=3k<+svq2DIt*wdf8pwQzoR7%4a7tb`<_lrXhB+{p} zfBWfqyZI;FZgi+G=tf7~XYA)&LboS$R5*`%*i&#P;+06THb$EIe_{_zc7Gv}rrGbY zduP`DSRHk(Hv3HSY2;+ah`KcM9!TiQMB4sTpGFcX?HFB8x_q98DOFvZmq_P%cEX;7 z)p*jWe9fdA|BBOZEl8xBTRt4=%+1YS!vJY2Z?T_y2|c&bowqp8;A;WY;&fc>Nsz}h z@-;l8qIGQOw;p+qKlDZ9O#g`6br}CgNLv_Y&-trq=6m3whZAYtcec%(xe}QYo^zRD zeiphjA=5uMPIiV>-Wal>JM8D!LN6y|lXF$|?M-vb+e4ae(!@ybvEP1A-Cpe(dtsYB z4^iJ&uC{{pXZy6Lp_tF%Z|}M3_MY_GMUl_&Ea%#VSDOE2{9i}^Dw!yM{dwv3J`Qpp zH4?TTH^9q&N4i;mS!?-DUAq5EnfsO2{{hjp3qNdMI^-(*%DiLNO4^zg=~gk_5os&# z%xuZH3I(^^z2L6UN1lD=vXQ5y!Dj4CFVD2E7yg5LviYvi-3vB7^K7N))z3VO8XdUC zyk}PWp7g|Cc{W&xx-Rx$l2!RMel)%}-HHfUyC{4aZAC|fzY=&&@EQfc`-CaGD`DqV zh0G)}#ggHO@K*w>Pc5LV0B-{Unzfo4lL`k+Sbip-@a~kZ9}xTp<~_b z#7bXweo^$N8CLAnWs$%$zb+~XuQ;XbiWR=-cP{_(rkRLu*|WX5R`v=jc6I*ZnoP;8 zKqmj=H8U1GqG9%?cUajMF1E6dT=iJ|ujF|Fk9$;o%Gz<{s@?G@z-B9I=o3lj;V{NNv_XjpUtMrmthm5YpU(31p`uiC_({kjRU&N=c^2JX> zwkAFL9r@silU@#ggDQI0rkdg_G8S%+;)}W?8)t4xkIs$;vb#F{(OJuP>9d_&FY{Ti zD9yq1OTyzzA2`GHo#gJjz}GSwIIU~?s_KY6rn|knD-BY7?JUzAchfhvb{dIck){|ebWmez5XZI&=+f()GzfN8E z)T2|@KMqVCnAtxS+4bkC-8)~g?RoDVdzOEuZBO);_NfOK{$OhKs^3lRe)q4Zs_uAw z>cBx;Cibtl?pgcLclT8N?4haMdwZwa-#;|9yz9TFVjns<<;y&5^t^e?WAQiX4~|}D zJ@o>ATP#5g`h`GdSDDY6&86K)E}!!QM~)6J zk1aH5Ir~_{WTeM;`edPKgCp}xYSC#8bk(p*68}K1oo}flw+^{a z=UNQrwp{1!%Vq*jEZ@3hvQ}xvE?;GA{T^xlO?Cw-t&F44Q zUzzGB&~@cYFBOi~n{Fs>$~b9z48LSXtsUo6mNd$J^mcrW?D=J@!k@)HonMD7 zFB~~Kd3`L?_~L^fuxmc6!S)?XocyI*wm+Em zim989HLJo88~@q%F2H~4OJ%Dod*EcJelAJMs*VnyIoH?o_1o%)`_*UDcl-F+Gv)1z z+Wmw2GF`8we5S6J`LcCA_~a$wj5nWaB&KiF^%eYj@+%v{Bu)JO)a`>aO!g#4o+Q>5uN?~(!*1q^_}$V zlm6v_Jx|{Eqp2p}qF+A}`PJ0t?tNkE3h;NWwBqMyShIT7_y^~-?pgR~-_)U-|1fpM zpVrN8|ML_w3xE9^csIQ2H!|B}ckJ2z`NyWVKe2o2iUaFr4?S=sDKqPzJs>?_`{1lgRku!S_49ajvg>U&=Lgt@H``(z(|M zJ~a1wIc-_;?s&8O$kBT=HfbDxg?WODXM6BN9(;iZ|FZ|L@ZisR@W1!qzxUv(>()%m zjEl;94?NZ10ot+b)A`yDC(j?ONS^yDljql6lRV#jt$kjz`qn*4zwPBU;m(^LoT~Z# z$W-I2qf;BL(4qXw9eeWs`QN6>+J7}Qxc7~zDy!^obI0ptU9LYx%XVxxy5j$XuE__d z#_OM&%KzG{Mpx(3#yx|vN2bOf?Vl?9;N)BC3bk)Dx}G?+COjBv$+c(GU;fYSas86<=l^@Zxb`THU7MbGK53KYu5Z^* zRp(ecUe2)Ru{XbY^d6Qh*}`5Fw$8WXi(qcsImd}@H_!NU|KIc6%euBa@kg&M0aO0zfWJ%a;jLn2alYodi@TJ^IKF$H`Rohy7s51$ z^_>)*)tokBu}+|m^3H3IZ2b55YuEUgSNjunaqxG?+VX0OE!?N;I$rF?@c%vMM`!P5 zZP94Ty`(E_>LI&pccJFlOS|q%*t*!Ubt$&4!xrp41$#F>ZtRt=n~{xCmtW%%d(G8d zImtXyh%cEVTUpWB_l1)4T+M5FWh(pVPq$iR?~#SkS!^-wa6X%YXZu>_&Hp4P{|n~k{{s3G`DZ!#7uhn( z|Bp`oA9Sr^3U=NJMXdg&dpoCdEA?S$jN_YGXI}D`Cl+k{;IEcGv;{L zFuW2xXN~f!1y)Obj$JQax!vH)!G$TFrK@iq>?g-b|HJukvtIUfb-_C)9GyMjjI~ke zcX$sgox@YCXgpM(>hkY9LE4t%q}9H_rIvN*a^_e^wmufe_d9&d1D9RoYZ+g_JlC>r zw0sZT>z03%HaPWO#rs(^ZkwGB1iBR08?C^h3_jcSD)H^=Q9p2Wfi=5O`U2MZqwG7> z_^b|!y?xhT@r}x^era8r)zj&Zu3q-<){Rw~$KL3>@j+~Qdg07Z2KhwE z!N3Jw7r(jnX}dg2yO!;auD+i&`DmuqbAUcmw!DU(E3v1M{ev3pjIn?49sF|VwO0Jj zOlx*6eg4)=>y;O%yUz6f)ysUomD@Sry2yGZPM)2MZ0rWk*=o#wP}r%_`pdt*{pw}D zjL){eakR4W57GFKgH}(5FS`0tY`hMcdlts7)_UwYM|VbA|LRK}-Oo9?GfuKGukMUf zqV-px`%+)#XGh;WTKRff?CK}{q3tVP*;stkpYd7t!Ya2>mgoE#hh&TG_a$8)&?|A+ zv2@c##m}Z&JMN}zl3U?h@|#P2ryaVUbe3<)cGoZI_+-$3+V)1|y#25pAF9hG#(xVO zy|1wUaTDk2^A%$;&W~Q^d+NxUN*|1`u>3!`jkwjETk(@QyY=afzKlbc-@5JIOxnVw z%dJ<6ZvDo+2LnZ2%c-+L>S7ahp}G3_Qfv0$4}5X$4;?+tnoac2#MH&)GaiV>B|pAk z?T`3U&LQ%y9JQ=R{~lhQ-(t)wjUPFae)%$cesq97dVQ*WLFrmrF&5~7N?Mv$X8|C)8b9c}l z3yvQ3#mCRH;x94=KXbBdUpAvT$|!C10PCoeXm|gA#KVdFt^K&;#lr}^WBkoer^?^? zj=v8OV_tu^;y=r8JsQHd{|lK^|K29Pr#@&aiTVg~9>JF<^!;W2r;dCkEk3g`o%zxN z;y4ulC~-P_@dIc4GRvx5e3IMGUD#Djn|U2yt8HmMcp#8t`n%WYFRQSlf1&!krL_9Xi8OWhx=_;rh{S?*Vf-xWK0^wG!sOAfvIv=x7idBk%1Q; z)$@q8hXe7^+nH~@VC_&`K9B7?sf+Qzx_F5G-EGs$*`}Ai-D=y6pXz_OAJ{VeiM8=p z8DoA&pZ35F7ZpF|UwUX8W%>#AVfTHDyX^L{w5tF=M5zyzQ}$m&J`rPYc9(3D{Aus; zwTQpin%q;h--_R4+Cv%s{}A=?!T6ESd?0@GGQXMM$&atzZpCF+75)5k^nq#{qZ2E` zXEHBZiT+XQL49fo=T-Qx0IGGw0s5nhP5zM))tS~anhWT>5r0n8N6DU--))c8*W|KS zZ(>L+o@!C(2bVna2UN5j_|Mr;$_$q|YGKkqsGhaBc-C{q*x8neH z_c}58{8?7ZtLH{%`FYNs@%Kh&Upp^4TgLfM_*x)fX+hd9y&yTRakl$m}qwU+J zU4O=|*N8d!{+aV8!y)SKCgQP%y8Ao)T10)$z<-X|c>QT#oSpjFgNmc;6-UcVoA^CC zrmtTYfBod>Y|NkIjDt(MNAC15Ius?oqnKi5VL8E{1_D2^b`LCkS_~mhH$Nl)^ zHGJ}89*Z2uj@R+Y-|$VyxA-^9d`l1gGGVjkE=#+1Der66#bfBS$K$14IoSKCf9ZDV zWzA~znDp45f0(vx%DlKswzoOy|47`9-oCqJd}(yH${)L=#*Fz_P=A+Gk7e<{eda^) zBNu30W5@k1r1Mc0#j5PAAznvMi_VrhvAXL_CsrA&U%Mti3716s%1@xt17&iX;~!|bjAHh(EII{Pv)X4b>b9t81lzVf-$^53X2=sIL-6cfzf z6cd`K43Jj%3dSPYkiBVn@h1GYia2?|AGr~q|MVQvHhwkzBN<`V_i1OW+P1{HpprTn zzrb33@M3HAnUY->z4109mu2KKC7W^Ps%`H{zu+6l*Lmb$Tw|>^_cqRsT`irGUC3YK z)u&#t?Sokte1kOGNz;;&=Fp|q>YJQ0*zGkt>AMT;c(91aLVVYW?_8hBhez-szA>8pEs0&Ywf zIcp)!-R-r|wTIPja?jfIrJA>AcYTLEBUb+<`NV|V7liQPMU>Z#ebwucQeR6QYot2r zBXxbCy*PK@OED$8w_Uj=EE~Be_KJ6pcN1+>bt{=0kkQ;hWzao^%ekiz@hy8cV=eb) zn4el7jmEi0pIwLi1AhA+@8y@TG52_t&b`kopLL#LkDpq@+w;)uu9`cv7FZisENh=k zYneB1`9=J%%vsI-yW6d&{>prJtKZsjC_v1uweR5-Q1*k&VKZqzTYbxB3chL0EW|&% zm`^u7c-d?W8@D?5oS%%Y2~R9|;EY`#-#jCHf^w*A-voEDavy6(u+M=#kmg%C@w=qF z(fRL=PWmv(6(D!$o(Im@{_)SwRH5@lN9UnS*Mx8Q_zg2rFs+jwJNNzZuJbO9KS(*o zX+JNfTP;dI-tbawMw+!lu~qXufBe6Alv3x1*l*MQxWCemym^c5k1JS*D$Vvw)`T}= z+l$!d<{><~<)vED+qieJmbK?^ow+`9-(jsOtjyTekH5>;!k&h){ql?L`f=;V-Q%!W z^ZAf{jTm(2-RcXZJAMIa&U@etw~uhss=ce-OD4mSad~Rv!k<`Wr*-iV?O1JB{ejz_ z-F6(JU8|2!n{T8|-=;QAJ6=wVx&4u`r*KVJ{gO9swYEy_ch)Jr)9)xw<>Oy)Uq!mk zIo)UOnKphk>m%xWTzh1L`XSjN8#FHS8%L`T&}S(9&D5b{*7Ot3UWwLLZhzprZ)N!3 zRR_*<13%25Z-7sGp^oWW9Netqt_s9y3p>;fugUVY9N=7eF!yaaa@A#VWK19A>Svys z(CgNb#tEgro%x=8)7ZL_`Ia>sOOHNc#bPD5W%gg6CLXDme+41`0)>B8pX5cSFZkFpyp+@FZ zTjpaKz`J;M}gC*5vxaH__KMvY)5xP~WO> zqu-jXLHC8kh8cs?*2V9dvf_6g@x|9MH;`Q8`Q-8ODx<&uoM=3By1l2W=VP{z)@_eZ ztgzdo=Fn~%(H?6CYzfImnu>jPy09 zGB1EyX_nc~&;RqPaQ+vpxa^YM`Gd0a^sfA^zW9;Ttd`DeZ9n9azs8sBPgwCA&|&;} ziZ8tEdTaIh%WYkuAFk3*j?TJki8sDy#oxfMB@1o84xW?rtDfYQDE1KF%CzocZ%I#)bz&_C5Ps|L5h3D&*Js=~HQ!jZXQi zPNn^wADwMyzS&8Py7j!`>np?Yb1yKyEBnRE;l~1tXF3DfGxFy{mj#}B?Dp)LUHDP? z51yGU|Bor-Pbi~34_MlD6?^%T$@j>(d2DM~Y4j=|x1Pp-_HtPIJGU0aTU`C8C-vvy zPwBhvFE1CWJP%QxCy5caJmR%q{!*dn?Kj(gDP6iM{7}IE)B)zn|H^#(wTG-7gGZig zTy~0e<8>L#U01DJ{TeYazBGLCC^0bTw+>zQ9;@;o<;YyXezWGVjO~l_GVkj(^|`p~ z4dyRD-mm-rn(Hs^x|#mwD0?Bw_lfMev`~o1I0?S0^Tyl z(s9O8m#00N5PHSuH&@NR+M^GzF?CYYwkq6t-UV^hi+tC4uJ5T8#E9|Tc}4LLIKCS{ zCFwiqyA@xtCOO~U%UIm?&s?jG!aM&l^;=hcfHhqyYr4x=)2)ji{=(Y$o45R@nFqzG z7x}G-_0va5Q_r7l`|_UusgN&2=UA2FfzbKeUOgH<|CXBi6&ab^BlKm1M>aN=1+Aiu zmj<3W2mk4sc)#&M=J0#Jn$ft;vM!YE+VhaiLG0W1=Fy_Z0;lo3TlA+qgL0$htDS+5 z%tW)aFSDfUoKs>~Ur!o6KdJd*I(_O`pyl2tR{2`m`Le+s`98I=pUN)nfy?gS(Kes4 z^D9ywoBp=oBIT*PGdMp;>^{RBt5We%#h5lo+B2+w+_~(OwX6TYyjuNAi1_(Abt6A*`01+f$IlDhHy+67 zVxK>rbE~zZ_x3YqF1M^lZ)H59uAfq0HyUWX_vc@{ApRi!zUm?RitD+z=sW$;bw`d? z)>+oY=acV&#rWf^8yg=xlF|4Cw#c7r@#jK#jo5nw_A7tc^&Jxb_~ljPi&V}}`FFh8Zne~WjP#%K?Wnr#iIN+B>EBUzPGEJ-r|j{j z@s3sDy5|F{tKJj4WK-8k@tPxx;y=!^dQ{dpW$nW56~xHk$3pjA3*GXmz>YInchr2( z7uTG-CS>)*m*AU|LfcP&<)5D}deGX@d2KY_w{=y#5nrfHU4lQf?r6MiQcU=wXT zk`{gBoQ(cU+82~OvVw8&p}@T*@xYlg?F+U%qC9?b{l)Qz0)HrpF0dYn2R<>gZei?_ z%eha{c-tpuq=$XF%57a=jmO_r67QrA8I_^x zvwMlJr9Wt0+;Wa@$Le!^uiQFwv@#nT`?eOxJJX|1xqV$nRkxJeEvw2R{q6K$uHG@? z?JxXYW^}Kg=|cCY&$_sV`hN6zYsX{$(4pPaM=NWt)m+0~?~T%aZbpuAtf%T9qO;G_ zJ{qsxYUTo7Ir-;v$c5PJklbG6^0A@uT6ni?Sef_ipo@C#lw)~z4 zt(J?3FB*Fb<-JHNUwSB2a0zs9FN)-J-<)vgMw&(Jd&x~@9I-bcCZFDt`7#)R+l zjP7GcjxN71S=JFqUlV`NS$0>u#d&M*F3uiP%XK3@Uc-PE&c5RZmz(=(PbjGMn zBk!B@JhZzu_)<7IXL;hc?+YKZMo+h&-S$2gSGCZdyALns{;adcaMuX#I_}_;o~1d-f;FF>d>n-+AM2| zO9QOYSYvEuZ86QY)*63kh@lCZ4@}p8F1!%;go+ z(;r(GKZATSxDGLockhe*Z}#3kKC0^4|35PWWM-1U0O2uU9`GSEK@d?O3e6;-4M8IU zZPix8LtFEpV5)72h)IaO2BP)`ZM_%T%abjc!Dt`ULLV*xT1%wbBCWl*?Y)H1tCLIs ziB=O7@_T>InL{Q4YQNv_{qy(7yw03+_FjAKwbx#It+m(MXF5El@{PZ|!L%LZ%anZ* znPKH8$bPhK7>~-t*)WphchELCntJ8Zf5P96-82gr;CEVZKiF^aoBofX{=t}>kqx){ zBTM&}O&y7Qeh)C!?hVjbIS0M2v8LXYy3c}j2A!bf^CjI6)0S{$4w^RX7h~|$mcO|_ zpBDo|?WmsWsQo)Aul;1&3TuAE;~^LaUo?E_2}Wqoq+FAxb(5zdhbDB3o*~9!J7ZyTCrF)85XxZDJ_r>dHkUhEz#^@M~I?CBFB0JPZN?0)>k%YOS zcJ_gI7VGp6);R1JUne%tiLeiB z``O>Wjs4|p?Xy`l6U>8NaHXE)j^beStTA}Xr+xGYI;-w8wtzQqd)Z_B8aT^0=)ga1 z&N<$PLc)vvOZIc({ScZf966klOH7&IbnLppjOEulZ~NXQi|sjeO#8O<{kdGzzgy7? zFPvZdqu-#pd=o3B5J#b>*kAAd8!aMY{atUBn;wKR4O z$FAk;t>@3j?(dJ?zwoLhKTd71-v5B-rxU72jQHKUMVZXoNk(&*;C(Sx{tD{9P5k7s zF9BqtH{RhV<1gi4qRlEbthwi+$`yFf))qMwgD!=J(Nygzj;_5|bi#tDi%q&;!TZ0RE*bcYh+DC76Te>TV355L%6xUCa?Ks~LI+NtO`HdMj&K@7zI$&#p^%=ZN|7_%}HrE@c$%|=&ofS)Im$lTS?TPXrWq|u2WupdH?xWlTS1<+7gcbLG zCOE5154fG;eVWHvaU$DMY1aL`Ge_^iz7Aba zPS2}6MLVZ&R6EX`M<|`DSuE$8l)V%H@N2`^q1m9V^6V*xGnT->5THFY(1MW zV-#fs=WoDEd62y%$Efnuns1HY3#VzZhVz%RCKF&P;EFzMb*aE32Ni3vjnS- zeJa6wnZ2s5=MrYfro%yI@_B@Q7J&Cr=fvoT&`$j`@F|ZZ&P=gG*s_6{b6NkZoT0Pm ztL^A3flz$WffSC(*m5Ovnd5P`fLpV!Gmw$GZXkDn*vKvahOE@} zD~^hcVaAd#4aa9kjQK6#vx(~zaB7Lox9ei_t>uFGmJ*mW)L3$m`KGy+!d$zKxwZv) z9UN&ad5byt`;U6Bm+tTu^DTw>wuJk?xd_{$FXQpI*=`9FvMckx^?aX08c`(T>8Gx>jp|2CX1n}@xqf(t`rDJ)4=P(a>x|hCci?@OCmR8!B@X^Sy zU%kJmm=1gTUl>zVV`ON@p6MqTQUeNty$ZD zeT^@V|L78?-zAfK(efkUn7-5Jf}zZ$eEeNnOFiLhljGtEOdo1rp6_J*q_*z5-{(jP zp5v^7U)8_4b@B6E$09%P+G@0P<<0)@uIBR2u9_1)ravG)2J1R>@az90X!-?&+n5D6 zn7F|A+N;_adD!wj?JK)}mSjZ_I1)2}U1{aQDaTFgvg`>Duxy{gq5KK8Q;EE+rWWT#h^p3OtWCyJe6N5mU!%T;_eiU(; zdt)}yTS3bvl6@cV50gDCItfQVWo#Qd0~|y`%8l8DNjKi$J4xNA?p)d3jQvqX zT&ZyR?G4uRldEBmx=p-G_82`UTtma(ymM7|D{ur~ zIGN)U)AJPnp*%~`Kt2V{`?@b$xy>!t-{7nJ^4_kJKOOEW84~G|?R7A*_RYrUpYJNE zYwn7?6zNJIkSARi8{5+BWGD9b1&YVCPAMlR-nNvlEdCrC@3b2(IEQ95K7Q?E*yqrc zUc=KpVt8WxQhd&G*4Sdhvc*cT?r&>9l$jU$7URe|YuS&J``hm8;k^up zn|Ym999pd7xeENedPfAkvPu}MaQ z-nZhbx>%X*YcEj-8MFqzx9g}+fh&R`#W5?M&*L{j$V}7D7cWMBaMI^aH$vxJRu19z zxVrYUUtQ$c%W=;{k7{^rA3RkJpKXJ$mcnbx;H{(sa*_?u7%gL_}F-(6=6<679ioLb+ zaTKSX621mr7OwGimQq&!w`G(qamLB<^Re1i-8R-;bg>KSzYRTP!`e8vq*yphcG>+> zKkInzzH_;`zjBbXI1~%2wOBblLHZ*-Pr9+zFZ=m@JeOQLpJU5>5FUSxBofvuBjGuAx7i$EyUn=z+J){6c(I{+gOi$}>bCBhT>q za5^#m_LyxL*`Fs$kwuz=pGg(#@uC$+<_4=zS^g-Hx>zvq~*p zqEU3_`Ad5Kt(%ciX>WJrM?-IwE7bmVV~^*r!BE?zA)&~7My2FbTf(4F zd#w>_%ZA<$8oyBdPX~0)jh#{B_uk<9ka<+N47-MLI?LK?=A+#C^={c=@A3Osb*377 zBKZE(q2qSfNOO&9aWf|P6KlwYJp(O7@7>9)m8^r2={4rLP1B*j>J$g9aSSKGOVm-> zKU3fKk4kU-(op8uAZw1=`;l43`1+CP4)0-OSH<+C^}x_Pwt3)2%2Yd3>#MK<1lObY z&-T3+FSu{|w8;Zj9^L5hB*97bv;#Gmu4n4D$p}(`IA$V@VnHXvm4%l-Y_ZsGW>fMnX>fJds)N$OEqw)tgPV>#DOvfZwLprf;+5B&twY}CoJFt5JB}Q4OzyVEB%bDG9IR;0{!Kj1cSCd4w{_)GaE<4I^hx<4**msA zqW%=nXM0@jvB}c2*x0C!%H<`TxTZ~XOSW_wJ&OR}u4|V&mU69TS4Ol^BojOm_aBWE3dsL@%*-8d2b3?d4|%ISkmIb^lI7MOo-4c!fAl$g-nNb`*=*+&WcaTk z)}hZH{(HJmOGl0wgK#NbXg=@7!y1q5Dw>~-w_Oc%npcY!YR zkLp4f(}UiL=|R$a)R*;bIX=m1^-XK6-7o0&uC4nx!CmiU!&rIneY%dRUjwHFIMPAl z=ecyz)0bI#a~kWLL4Dg_ueKW(S}|^+bo?EUWllL2gLtVjX;WIrQz`Pio^%E}+=b+%YfP+jl zZM(Dcj->s{W9mf*-|t*F2|sVSzy8g4)4sFlkZ)&~hr;2;VgK0$M{=FM=(RT`r3OFcTx>j&oN7EW zeDR2ap{d55OII)YRB+>n%CD>rUgKQ7XrXhpn|R#9?-IZ9q~mlWI*#JNI#|baAHt89 zJ;%BDKH4nA2X=xs%hx8Q-bef8Ylkl`y&@^qkAEwvvUGJjXCFRa8d#05@$`P+6b?2H zXYDhFw^JvpG+_Fv1p|4#I`(We=O{F^)7BtxzXDrOGpnMT=7+%0`wrlQ(W$>coXUFWmo<3o%*HnMd+1xJxYM^`z}c=5 ze4glJVa0*%A%2Z`w!m@l_$zp{lItP;dzgb5BmAE^@F~ym8{SNxZ=&z>put>dkNm|O^Od7 zpKM9V*||qx?3c=^|2(=VnBj==81bo zjM-ND_!MX7oG&+=*l;}?9`}rMSK*x{PmPLuHqJT^bHljioZ?s;(mRxop8i+kwx#g8 zXDl+Y*7%Y3GfwjS^|J*>-uSMuv5B7>e`1qkX#G=zgBxF5>)m{Ra#Fp~d|Uk+ZsX+= zV(8NxuG3LhayW@Iled5qxzS6WJez+6pW@*++JjvNyz*Ok;3P8RbbHlC$6t+A*L18o z(&ZW+-k*H*mYeTLU-{`l53hQ4Q01ycoE!82|F`RY!p@?5dG`*_U!9O%wC}NFMQ=~| zLDA!}Xa8)LNiTYbXYcU-?Z@6O+8KMNvTYN#7v=LjkN^8PBOqh<>3ez|!=qEkcQAT- zm#lL5>P3fB;C26~p0e(Ww5&8c*Tz<3O{BiTce%@vA&cnF4852K0-D4mA$i0>rqvnEo z$;)`1<~?Mi&exK!T(Yq^IIjX-ZOK7%zfk@Wr#b(Y8iu*QCmroNSxz4g4`JmzmXzgEW9+|q-+mt1p8PfWe&ntN9H)%X4!Ip;v$Ig#|q+z-R4;clYMat4JrWO!8gx zt=N$m)xNFHVR#++|04S}_n+&XaqHk0KrsqxSM4uA1|&MWXOz=! zRgJ4c@c>n{v4ZO@;BYJL7SaZ1DUW#K)|AE1j~YMymQlv^`?POKUz#cBXRLI_mDb-4 zW$m+y9gbY0nWIng1!_V)U>Fo$EI}i;U;oQytIw zRvFLvre;2uzAEFn^r?fn4&fS{FsUe$-!^^+_ywWWH-=bn((;Vc&jI5KU_1tlf1>_l z!1*=xUZws&bA62KI}@%bdX=B=vMY)n<7zzi*P{H#-en%XU9{q{cZwt*h2J;47To_0 zn9G^t?8isbA8RcN@-vw4hU5qIDs{RK)p16d7ygl4HGW^cNqZ*JAO1)_p7eoGOcy9i z>ons9>(K{p!DcV_B-Jm)28^)wOPA34v^6uI_#1l-pUWJ`XD;N0bl%0edw)TPHb&;n`^6imdE+ERcn*mT>JFAZ~e6Az0N1mI@;?T8BGW0 zco+lMQOQf;taDphSsULczuLxAIGQ@!TymT~!tZjxXb_*YqWi-0rrvDEpk5WrcRhW= z7r_VGORjuxt?Tf)a(p(@*T3DlP}kSK)#*!{V0=aT`h3UwO<`ogu|y+ue551PKFJwE z_j)-}8`JUme6s2+;Mzf3k*m=E=UVSV`;a$N$uXVk4wV2)`rOt*!6v(|-(}T(jcfEk ztS&INyPM|-X2{9&?1WGU@UTmo+QwARiCpC-|4DX)$Fd`uX`|NB&|2%HpF_g!lko5K zCWbmbZ}j|{{>e5ccGcXo{WW~0r&dayS}EsZm7$+zvj$IQU!(kVaHD*J zUt^z&Pp&k4E`zg?lRbyVEwgB}>FHiy8|9HDFSX)ZIY!*E?bjd=d}h{)U=Hz9Iq8YP zvuek`qbgJ?`2)XiLyjCA!P>-{mg^WFg|AGVNIr2l>z{t&gLdMu{5u}~zo_S6USLf8lT`eJea+(O?Pv z|08f_K?A3&ug{L!zCJfQjhW5lo2y;5736;GTy$UBQ#(3yDTDQ1^MPE-a0xPX3vxno zRQ~zatB73~o>K4d1VdRd-;_PyR7bv__iyVvf9nkAZ=F+I`CRKPB9jm1vLC`XH6nx3 zXC54qHPijEWjh`xzM>SH@gz253pS%~x_)46c? zBQ2ZXh1a&DH@LAK)6Xq9lI9#59Uk{$J3P+ zs~2HA-X60Z(LEZ{o^YJbA|~fk^rM|Phz`!XNW=!{q`l-MW5iJK+^ZNAY}HlMHjGch ze~er_+{#*f!jYW#WNFqc))mwCH0x9M|E4~1GN;)`U*cgr1Nf4nw11PsbLfloPkmmu z_r!VIaxVOr1-=pNPvPIrIC*9}7neaRl`UW0xzNAXn_33l+}N#t?A?IFo7zer3bA?1 zD$7<2epzYO>a;A&=0(3h>}fWJOWw?s0#S4m;@IYvlF})U*6+FRf?!cU)Z0+95c#*15u2 zUs!Ua1=->!ekO3s!XvvF&#NaF9APhJ*5ui@90?E?Lri{7l=DNfiH(^|ybS)gN^JR@ ztO@gubYyepKvHmHlpHgC!vq>%>n5JYPd&HdX{M5U1})OAM%N|pO!;e>tkdnO(2}@w66$uyC;(imlh->$LR6;j7rqC z51(Z+uKPJ(PdZRmN!hFjF=qHC8j20CdFJ0$j8PW0oLjaWduK~2Uw|A_`E1IUlk=_e zX~aor9oLyXrN}e+Ni@$o{+#fmj-y+;g6Jw4C6U>g=mdEamK9|V8q0NCQ2^bbc%P%I zhC1#%!SO_)?~?h%J4k0R+rf6c5BhWx%c{0?)|cAKf=Au#gKG^fgf0ap9?qy>IqFz5 zp@H71Tv{VJZ?XG=+2lx0uiLbLnDG+9s12qLNBHAEkfT)ru93?k7)O zFrGTwJ5%+F>8JYgdD?k{7_k>iSI+7sPPdxA_mV3r9j%wVZ%3lBWGLm20%N(?STeQb zEYn~8ouwPe+- zqv+Gpv5&q?46fp_Yo?aJhQ4_tWryx}3_rThsF({rGlk6pgG@sgqF@xn2m`cuGBw)bRU4n?Qe^L6~UaliF%y-NbXJn2atakR#Yt5uBp@75Z_ zeZ;V)U29aVgO*#s=N8%@qj%%d%(l~h>Z+)rU++#dMxJk<`}6Hn-fkzktJlP_Nc;kf zc-}b0x+2|Q{Sh4ZAZt&3SdjC-<}nvzJfn9j`DvVcYjqyy5OmE1bU8P6m&c6v@}viI zxe zZc_m;oTKVCmBiLUY%kVV>APd`_lybKx<=~|>$&P5`|Al`KK1N(2Jae8+f@71qr+O4 zBb+mnO@IBIM<;ly=a}E=TOL<0NDjMjY=4lr8O|1X4PTU{k6LZFqf51zzOU*z9h1C% z^|yV6!}Q_Je{ia=nKOVQ58@L;hRc84n&4<+E(!| z`^{w$Rx6 zY;3vZ@M@KFEb)8B5}kMGWz8rCW;1gL`x|+e6kUdl(ERgGG#Uz#Rmi*Oym7%Z%a|XM zkzU@JXC+csx`gJl>K4c9%8st19iy~kwqru(%8bX37o|^FTI4_ur%%{gl!P4Kf}9e( zLgeuKrtR&C27y!V^d6GW!0qtXmpjHDO2Qu7LK|BttG&`HWVY~=T{M|8ZdX!0=er}z zM@H9?=TTQyV$vvs|Dxx|@bFoF5oeG!%(!0lx*6wUXoKE}+~K=5_%+Y+Ol2a-ebH33 z6(6tTz37_`KW%r7KUCu!U%%HiK3el$FK3ga1vod>j31TWXzdkV+P|yyA>Icx`uB?6 zbOcfu^G4ckBoE~vys7ceo7@TC5|Ve z{rHjWazR6OjJ1cf?arybH$H|>T~1<3pc`#@{slc{F7qLQJr?H0G2}?wFrBSz?ZebP z%lqk;{n!34?4`_QPv)Z`)?N+vqwKv%uPgLWPv3qDz9IONHX9tL55Nn4eDj{Qp47nF zq{XGIeFYhwRJFa{k3Pm)lwDb}dObQ=#N`b{k30F|Gx{3iuJx^j=kCE1uI*rtqM+12 zOL0A_8@j4vorUtYPTihg@?b!YL|m4IG=#;0wQlzHgXmI%iAsfUq5RqK7>K0 ze_q9jrkP^nYaJ%%5Phew|!J!QS?Z6AJ}P>wwyAW-V*w{JA#j z>bd+mt;4G4v<_*W(>jtX>su@SiIdQ2smqv|4j;dS9jp0kucLLH-+eP{^wzGd-|g?J z`$=z?&H&6>{mnNI?%2~+H}XHb3a;z!;w!voGNfmLL(QK4I3Ooy)_4VTWLP-0#{9>& zJ$sJxq-{G^cJ7(jnx!+8*#mB}WrBa%33I#x)28}*S#Klt(C1!94l<(2!1vO=i!-!2 z!$!GIi<~;YI{o(!)>rX`uFA0%ep$}Wg_^&jr{Zk%Y~56N0J)+6k-M$+nS8iFJANtR zp*RW&*#phZeO?=mqthq(5D&*Wr4LTy=Pfvz>jUAK`@D1EA#C;tc1=4rvA&7t#kQ5* z_++v%;wkoit+mV@zHQq2@mp9c(U}i-#_ZzzOVdren7EG95#*Vh^I`qied6yc9PUH& z$d6Nh_80Wc8`-r#fqw_ujZXS6n{kfbL9bu45AC^}II;9#P78A0&fh!8f605!C`d^| zS8GkMV!BJv^*o1F);H6*!qTgb%{#GBe5f3x^Ur3VSeVJPEZfJCY2?^(kEUM9m=Dw| z-N9haSZGtnS~QcoZuSesOJC2Y?sTmw1?Cyo(vPGY*?Q5}@UbYqTzZZ4xe_NjXTZX( z6B{n`lONq)v{;tvy|G$bUoqqQ}Fuu4C`nuYse}Gd%sTT;X-HR+-c5gkbE-m{q>aJ%o?8nU511k z@%??kzW7Hsa?UnuY&ZKq=lg*jFFn=PxsRiJ{1v$XoF08M=N$6KdOs4Kd>ne-c;2Um zyLsMz6?)60V9t8@udb<_mUT9*9;=Q zVKBTt&>pqsz38iE%)Vm3 zKm5=L<0b98{FIzn&OvQBi!Oc!zL6c2HaA#NqOrWbZc~~oy<$H9v!>N;I`hY1XdL-N z5$A+M($B{@Cf2v&L;09^$mGwlCHRLkqy4>Zn467F8pY-^$pyQtV{eS&TTbPly56}SXW=a=u6z!)j!4Am0S-!xoDZL3!Yk3tgBHy zqi(@ti^LlRiNTqXLBk+#-Bg`Ie9MLME8}lgd|4-cWIuJEz)sh>W+mue z@?k%T|F{l6vi#QaIm<^RynX{-=&v)BSNhNp10BN1nZ`SE9|AU>-n;qK)~ySV486MG z$gqj?jwH>o{NTv;jl#1D_@*D6^;9?sZ|ugL0KQHFgYbi|nkB#Y6ZEf?vM0a++t%!h z@RuK4eTag)`k-^M%fbIX<&}bS1iyD#0KD;ggI|w$x^#q5Su(}QkzHCBeQc2@NZxp+ z(arnH($PkxZ1QugV~o8ichBp{wbs6B?w~=Z+rDXp4#2-|r}&1`wqUojwiZ;^nmG&Mm(Ka3)B~=m`xm72`r5PICXDDLV6iVN*{*g< z-=4x=v9qC-wMzbo_DO@W#fF6Bhq&u+z0dAPXApem)Dm||n<*z3@V|{UNBg;Y*T&gI z>ySwWNyh2<=<5;u@TSk7a%y8gc-ncoB*#9hcalS1!&#q%Pu*j9>Xomlc#tZ_ z!^_fD`s++JoF6PdMP@qRVBz`%F*uRA*7~mXbaZSz<=a4kV`ErPQ&~^3Kg{(8eOK#g z`q_k;M>!)v>*@RDtf#AjIqXlH>uH^pt6vc*H*)Y-EZIh`zPX-K<^#&6IYyU{srmN! zqp|ZMe$ROky?hI*nz1>ky8KsN16;P1ea%`=W9{l2d|FRA-xr@H>pRcYPthP=H?-Hh z{NHJQq?l{obMMO!WBJRhZ|h*+U?(azeKvONR{Wz&hgkMrUDFgZcSB>-@!y@M-^H}) z@%zY^^1_GacWutoIQ~0%)!30%-f-U;YL|Cgd0g5%^DG>-cry5A^Ar5n-i-Q^2H#SD zYvp~rn{FSqc(21#-VBe`*VJ!vU-87I`|w?Jo{bp`P$xfejd8jW**3qjWVinN&wl1e z4do;Fs-kyUXG1LTxa-RY2ltfou8uebH*>y#^4{BZ&VoN&<#6v>3Vn0$PCVU79r=O1 zhIc`F%@do-DJ#4tgIAj2i8eDXPvXONJCYOGw=8eee2Dq69pI%{?R1AR!p>*IzaCKB zzhLa&KgD+p_%}g&-LKnc$*T) z_->(NTEkS~T^+2*XHQ}veuusV9VvgyzeIf|eiaiw89(&kT%YUoLG};o!X6L%n;y!dh1zAx4a|T*ugbdyI2F z?7pS*|6+Y>hVL)cw=V)qW3_IV#iu`Kp6lLe-gBPSr0PX?vv%v*^03NqP6V;dcf_6< zp((yHatHm$6|La`;z}KyxlXL8^y-)8*UCgrOFoJBb>^?~jn=_uvhRG_mtr;N_;O;tJO#5%K_mHVU|D?j;90rH=+DZf0fd=x!!D0Tdg zj4)xUY?8_+Pq;QfPF6N`CR1nKfI6M%m*9mD%^kjme%kX~aSk5lpdITVc`ZG{J8j*j zFt&~6f!3ewl9E|fjMY+P7IKI^Hn+L|RS@KCUj9RuC8NMg`*M0ljNFep|D5onPWBkH z*CuYRVGnUBzs!=3*~D9~oJ+j*@(Ig}=CT*JoOtO4?8Q~ZVj>FhjSD8cxuk%Y4CE3z zOImoc;jMoX-CDAhwQ&hDj{UYo6AygI__>{CPFM+gvT)Y!+A?%AVK0m{q48|Gd_dw^Zi~1_M}_dxkdi<(l3b#+{vC7>$viY3V5%*hy8Il2+sn_N{7+^vPKK{ zV>e%j`%7jHa%%UPdmZ=!`}|1qi^*@K?_$b7DnH2<@^SN6-!;w}JHgjCpVE*YW62fI zVJ&%>ecI#KVbi+C?#jO&nP!ZP)^R8xS2OQ5-dpLIuJK9~lX_MlG+fuB3 zTG?a7SidWZYaliWvLN#VWhrEZ5GgUI$Y+RU8FUUAv| zvrcec(H2K?`CRzJ0dL$wy)6z;^hCmjO&w!4Y`Vpf2)yL*N#uaw-pBh0@=lit@j4cn|%Sd!1*}t3BNhdzUN)FX7P^ASS$Q)vQzCanv!aobTb8 zc&wz}QTCosCDd<A`IA(#XuEMp%#5E~;61qm9D>@8x z9U2y{!mE+@qMv9hTv>;rS@3QNvDf1g@MBFP=8H95_>11Z19z1#B{tn4&*a!Uj$ux& z9q_V?Ym|7tL0rFqyt?@t!H>%`jMJ}9$Qb%6ap%7#=KNJ+&)-$7IWgylWAW!doOgQV z_vgL6@(1(YUAc4KJ1YlW_FU2N2|J4RBiqt1+gh}b{utcn($~BR&lMTOfjNT4O0Gk_ z$Ue!qa;N7|J2~jW?P~NG>w_czTRf{Pkr_AQ{RX18_aJ6zXX0M{6_K{!*4jh(fmg78_Vxs zq03z-s+!8tTMKK9hA*UL6n%H6d)32Sf40-N>K8kGQy<-#zG}_R^r@fv^tqMa+3BA8 zX4;R6{+(wxe)^-8diJHX?-gyP?5kz1x8}GKKZ7;eT z_;%eDX*A8EUb?x8K!SDCM-yw{=W-Y52pRN=<__gi}EUa!>7-zyf6 zijE-@R$RuIx;)YEUG}}AR$@vrFWX*p?6Mye`RMOs@W88=eV?)3&KmbU^SW-r_lq*f zMN%B#1w4R{AU9#HnG2=-C-H&C%Kx6lr)}`5#w>55yQqvgE*_0K6YIsNUxrT$GmIa; zIwAA_icg!tJ>#-%CZA@APwA5ZpDIo!o=?%`mq;#jmX*$u{h)I9W&CE-26%LtX-A}m zqu6x9{grzLhkkr5XAgea5qf^P5eh%*483xtE3|8o_NJ_TfNu5y&fLq{MhQ1{YpvGW z=3Iik0SrIqY3M#bc7H|eejWFgzhq=MJ@%|^p`Ph`!z06Qv;LWIZr8JM;n^|0Zk<_1 z=b4TS{}*Nam?~3Iu-|6lx6Arw>3qtk^gD|IXfM!A?QHSO!RqQX~(Pu z^#6h0-Wfj5Vm(~?z^vR6#4kag+@b7|$}aO775nga`H+RG=ezcsn|#FA_=b!hk%rxU z4E=lox_JjOYj~{8G|HsycU4StHs_>E7kkr+Wy~d(@dWqz;L@+`I+YFPq_aMxy+T>8 zTWY46v5bnHx`tTBwEaef3%sUt@B36N&Jf%p=yJKlFowAbk4xdX4VVjvWxUk;bmoE1 zH@(K2IwEZy{sh|6-o}VWjp2)kX)KP#H0}kjB;Ls<=Nq3kLh+3IxWDEyW29m!%lQBI z@K}ay6CR3nN`hW~N8%x3f;oT2TVKZdGx>7n z2E78FK1>XHAU*~iec0S<#}|3g81m(}N*A>9leO3Wmv=ARYu`TI;#uvtFXO$9S6m!G zPQN&SwH9vT^H>}}g&W+612A#>N8-4_=aiUsHZ(Q%@dbaf86J8a}{rF2J10%5Z;O1VJH(C#@4)$`Z zfOi}|ss~;<@XCQ#4!kPhZD)C(7oNE2?tpw~e>_ zD6*kF#M3HPLUq>l_P!JVk8Bf<#Hi{=!9y@I*>BrLoJ?PPfnBxqX+C{Yzkri-j(iVv ztsHx=G&0??D@xxuXWAiwQB!=9$C7t>{8v1Ya;jvTD262(i(z?@^B8QNGv~sIu+ zAlEjan1I5y!Kbsow_#Hmu{y=Ea~d|h(_x;|a6dUWUF6)n?%Ua=*woj_xfx2%O%pjc ztH>jHfHmec@qs@g=cZNut+lUjp2Ro3pTN&v&N&Snof=){7@C?%-bwx=!&37(ry*~U zFE#(p<*PGh2RG(bE?d2vT$^RFT$?AzwW*3br=b&{obE%+&HLv#7q6tvC-DF4YzWRX zPF+d+P2{cYx_W48IXNpumF48DU@MhQUd`M%y`Q;34DDg&-tZ`O%1S4j=QIdLp~_m% zRx1WFN?YyFWFEG|mmEolGS;Ro79XygE?Ud~pBDaC(V6c&*s%@z?$5^;iBsvF>p48* z8pZB7!aDnZANlx-IYaI&X(Y}__Iv@k3yt_?-CVa4|MpI@;=S0TW3K7!7um5T#I`hg zjh9N;doJOsZ=!j>1m+{F7u~`ccTXV4k}J!*b{UU;mH0IvI%G4Dj8%k-X zt&VH=vgtuA&{y`*PZ;inUWlJAt0w>*mmolRliq{>UO<*)wcny=46? z+LLa8@Aq9}XZ51p_XI;P(Qlox5~AKin zKh)qGd)0H4C+B{@_vN4P9lH|7LG5c?GA0HdI2xoZVg6{&wf3GoI#y9n7ua~_C z;p2x#_Kq=5cjAMAFME*v=DP4KyurTd9_dV9g{D26AJ)RVt(okbytnp<&e8aYy|SNQ ziSHsIugL@MudtW8ZG~rZdb6XUE*qLY?rW+}ARcj&qbKWlhu)V2#e}XG#Cx?GR`-)@8qHbqQ&B{h}PAJLPu@_I^`n`4qE)<5i+7;XDHv3Tep zt?)s<$p>S?QTAwWh~t9~#0RS`&Ibi1AB^wE2ieq(rXg!$W3BrC@K~?v8|x2es&BV7 z*<;MP+ov@@+G=lU(m1zg&kMEHdYW1jikiGv@NM70oTsDyCL}hst!kbleu%usw@{!> z+vlq1pbPYDy_~*2UI>i_HMJEt&pBQ@zo~61G&m6KIbQ2(a+l{5|7P?YoXB}M?-2(D z4|G6x;{D)@vElK^PvVurij%%^Sz%@IRHI@?COX_0qaw<8d9-GT@4n41g)xhy8GDoq zr~mTPR;60s|053~{0e3GR?i;!k5uOa=8npnyvlVAy!sZ;O8z z2-v(j!{pW1mcO)Fe0pZ2(IZ~1@LMVK8;*qEcp}rls12E!+w>Xac)>Dn}%pUmMC z55>bNSaV~yU@!ap4V$a^36@|AwqObFBF%kZi?*edImocM8EVYB2ZJ#6> zOpMN7GrRlXm%WwJMR?xOGBmZm3|eaKke}SYEx#uK&1XRS43pNHYY9!(T%O;gIjnit z`uLEh$N~EEY5McD!JHLMKiy={xA&2aXa0N>yf~zHep5(u^@HieOY0k@v3awVZ)z?bWxUj$h|YkF7eA#T$H#rg*^>ni zrI$N<eslvV(EdScv9dt<36{o+dp(uu4W7J)8$LW;FBn zPGa%C3T^j6<0{wqawqHc=Ul0Wnum@u=N#V~AV)1R{9AL5ffqRb>ERo@`F=ro*^1ES zt;<83+gU3zSt~TxZJbOV0tb_);HACPm#kLbvf<&sS61s#hp7uJY${pNxS4OeH;g%+ z(jffoHQJifw>3?yu=E-)eV5#pY?j=XTyE2xq+j#t&oTOAu+Jra`;O)@ytbE^iE&!f z9=^WYiBHAt8r3i#T6inhbeEg_mdJcgj4puJ?ts_w;I$Xf4-I0!9O#G3|I+(xE`5>? zzP;sYbnr3ZWxUHg+52n`&&_i1#vPQqK2|QhvtyykFaPz`J_oroQ#9}V=d@FjaQ9Y!g6By~_e&{D#;B$kGmnTzJ|259K zMrL9g5F-#_o|JHHWlZ1lB9mC_e$O?+{6L?6Nn=ssGG3P5uX>4U^HMP2yI;j%91slT z(?N9q_kk@v1zGg6-Tqjf|HLpj8p4A6EI52kcmU%i6UNB!gdQtrMP~sjzQsSt_~APA zNw;BrAIZy^&c*l==hv*?w1IUpm$S64W3MB0`T95KZ+xz61HA8_|7KVI@RURAFK_MI z;`Bt@ui7#`W9`}_Zrb-At2|PQ4VB2)6%2PDYJZU&7uIkmIO+RG@*5mq!8sbMi5GSS zLkC5#*m=j*@GtB6%Rl{d@3Rj%_1&eu*ckb4^{v8vyc4c<)N>EYGtV;7I|shj+_B_+ z0>cRQ#UGk{@{0Jx5#C0BzJ3CGF?Oat1vxLn5jD>rf7UujGxX@p^XF(P&qJ}a%BO|z zPYafx_wnaRf=kYNWOfLBECl?LOe3e1|Jv`9{!zkzVmy1~gR0`c`HcH2Y!>tE=h%0# zvJC5--SDk5&2v=kGf@p<^z40RGY$qE>c-2DV0|y}%hBFvZJYNjv>lkA(Mud|cP+6x zy!(i271xisZsDqa+vk0MIG30fr_u0P0~t;m?bS1VN1;vpImp{J`3_^S#Ygci7jz})%wEwmb=Y!(6R4W$OXEDa`0og=59ImlM+Emgu_&1H!*+WCD3E_W68PoAALibuQYX4YR#RpdWd| zHvQIT)7D7ZT1;D?qphj5MZEVYU`Egl2g3VKVsOucz$}S_>3z6A%u-;M1M@y$-Ve-` zz^nkKa91Dv*xed;&t+q$m%ZTdXXOnG+_%Wcx&O09&dMpo(DJRD3hdv#jKRRXlXyFH z4Y+6@u0M`xfrZfC9f#kBulC2!LynC1#XSG8F~Le7IWtM%nGBvo!E-ox{s%IBEM*$u zgMqMffZgc2r~5oTAM6i1RC~{?A3S}}tQU9OvpW3ZTEZwfmy&0Vb0_Y;5Z~mlt&6Z5da091rW;0@9=E zC_~&rXqh?p`_8kI{EEj@b5L_HUM{tNVIZE?JnSMaGuLh5IiE5E@T}Ymo-OFH!m|+G z6pq4|?@3qMbc)AS^G&!`shsG@x2F}?ZRc1E*H4l+XW*-1@iz4Qi2qe_GACZn4D4ge z9esVY<;+hNcgnoCWlT3RrUw}#*|VN8il6(1pX<-7>zO|rm= zdKdSHGancWkSDi*XAyXQ23U8|zBQlv!@31n{pRQG{b4arb~hfd=VJf5bL_gz#g5x9 zoQvAOGwau0y#Cd8{g|vREb8C(q*(od{kLVqM7zG)zvaUAn{KS~@0$LoKa{=JuY3R# z7uUsc@!GIrxO5cu$3-wCPoDqDqy85T6z?vHjd^Ln=`X+5<-h-iL4IWPo==j&5#-JK z+SKqt&e$u>H@cBmyX)Y+#shcnet5gXzpM7{-P)6pJSq=6{bASLyUV`s@+2vc92>bg|t&az>-}#2v zy6V}>*450G4z7ER#aEeoqQCU;c2h4-3#h!G?-MP}ubwSA)#f@dds%+X?2fs?Jhv*!W@uk!I(^<0Jjht@*4-c|) z3pCG?+(f=Nst=;6-5>tjw65dbrDVM1=12IDFRG^@Cv81V;<1pjgaIyZZ5A^j% zeIe#9@Bn?GJo=cr1 zIQ;ibar*DSgbwu)cEZ^;uQrM|Btu%5!{RH=lhrF7e%8J{;xDb!I?LwlnxBRE*bh14 zeX)!hqrWjGdl>_b$Hms<`9tH!VYHd&JXCXoHR?mwsBz>|{_7CFxy!kjdBkFP$;I?A zS90-3Yb;(*p6QEXcW57P7vHr%fgZiok@l9G{7&`(O?|dWWzc6;M(5|81h!SyW0oa` z&4hI-$tu?kj3}@K=MXXNYUi80t0Hz=@2Z*YoJzFnvp3gZmGPKm z%=rqPq!-L%48cFg3x4uT3HI|tX8KM5S8;M``^h`+@7@aR~CKX3;j$Rbg{m}V4f+=9B_sI z+zDUB?2uAm#mlB!;DvVJsSi!19lko`<9$~eq5Cr(A=%TpiAKf2E5E(T>lj-f+8zvT z5Z^%q)5b$~m2z*b9er^|Jtp7%%H%ucWeIlZ=!|~flCfimZCftudD*6xE%&>2*>V@> z#Sh>`$%7BM_QvFb^h34bNtnrZBAj9Aw(<6wWJ9-W5OTm3DEZimS;}HO+;>)*-~QN3 z&hqvZid~g0H8!x4G7-_sUf-cv{Cw`XGS45yZkvXVeiGa5YV^rd*l+J*zjb22DW~0* z?}~kujGxUjs}0GGu>rM-E&a^FxOO%;`?vENwqDD%!JNZ0f;oq-%?rJLS+H`|*^(nG z&n`Xk0N=HoM$FGCY^-Vbj9vT&*Q@iB1E=o3%U@zT97Se-{(7u@WiT5;0vt*E$>~Za3Tyv2*w(fjh z_V9cQ)e7yt4e7j3FI^PuF?3FZ^ZJ&<~?>KPx?vI!wA0wB3 z#yHo-=0)JIGcK^tH(Z-X*mz#3h;hD^F)p#kmA-wQIZ(!Z1m0@Jem-XE=3^P#w18~% z5bZX`jcxeYjD9>60&X}nnDhM5;GU+@!O9n|4CX8Z-pf3T;Cmei^IO0(b!YheHRjSl zU&8#Ud7(mV*&^)RTd{X<;S7E8++955Te1D%i3Z=N?^$uWc=*5i;};kith^dHe&9|9 zmS~U#yd0iKSXblm6THWf_XlINeC^K{;`iG3gP}vrjUvuWzXkrim2a_Hx~**QKKcpP zTlmc$1YZ1F82-=x_=R^q8roG`+%0&5C76YrD^)th5qfRn>K}*aJn-Yfnxt?^zzBVk zZp+%(?SU2>){P2(4Y^mijlBSD>%x8G!Xek)-DQk*mZQ&}?qsbr^~C|-z_^s&*wQ-V zyf00%NOtwjjAw5Gd^s2WaT2~j*XRJ}j3B2R;Zc%5$WY0&vWtv$dunh`2lkQZXRk}r zyA(sv!#f+M{L(sKM6iuGn1zh>K>lgZp6icJ&o|F{kzP@Lts^Agopf3c^BNzksVh{X z_Yt#jUiXXt9>zJohhgiYW(-vE5bWgy>}Jj}L@r-UhiiXpAZ-Rg2Up-6c|n$LJtiRh zzcRc}zi#=%jDBm_K>b?hpV;zOSIJ+kWrDXflW(|ZaMrM=dePex@gq--^$%Glxh1{o zVrM6vKR4+UY=qL^_V3Sc)DPsDY=p%VO<%7$uAw&u^r5zSws1XGyKnXdcEVfM`B}0N zS}CV8ZMFMnAFn-dzU=aK>t?T5S36t2b#uJ>_2*{JJ?XO(^zCAu2Y0S`Fyk|X@nMWE zH9o)Y-yhCIF~{fI1I7n?;Jp3Q^6G%TeS-a?ekZAKlvCeLdj~s7`vdA%$z{xwi}vf& z;7{L*>6^Eo|>oAey{`L^vppEB{be~3BkHst@rCdKwgFOW^TNjhoiC2W5;dhMm{ z(Vr0;?;sw7vv%WbAI>-KXN%hWaWOgO-__R-TgJamA1ofR&Nh`Cv-bwR1OLR2XE@Y< zEE2QxLZ0b)e(mj>vW&jkGF~=ke;ZPI>*&Daz>HrrH|*>WQ$Cwr*;S$E58Sg`a<24R zT_ArKJf%G1W zSM<&EF{W=<@??a@G^ST=Sbw1`*(F^wkQb63S!~lp`U+zwzpkFWOqrje4{MKVEBcJ> z-&LDukY7EtUl(hmVNo!=wm;2}vw?>Aqhc>w!RiJs|r%e1Bsr~wG zrVW}>e-K(BGfjD}JvZ^CaE_;my)UmldX;&Ib$sA{*FWMTFD>rFRdPNF9BuvAo7^Yc zL-!2O5ig?uD!0SLF(%KX$N#VDz_nK1fqjPKrTDVtv;F%d`|T~xI>zQ261bgWx}6E8!{^83q&|GhO~;sE(%wSV=1_B%c`pnc@G z;A?1kvafQU`~HHZ`k$GQ!ZA&6F!KiL-@x2_5a(dzq@hngxwdb|Ca&v z12^=qZ?8o!+HhPKhtm#k#N$ymt3RBdpYMmY4)))Z9Ub^jyFNVYop~W&?1F~E<%t*^ z>Bt*uQ^V|cc0Y`bw2SpxdZyOL@_;k6Ywj0zhh1OTT^<#0nN*ApL zj(pqy&-O<4g)-@foKtA+ds)0<+Zvj$Tj-}-{2yD_!v6kr@}dtVq7MyRD|K#4pFJ5L zb2K`zA&z$8t1hIS?W2CyJSp>5a8 zrnhCB9p7c^=mTY?WcCcVs@oj>20yw6<5Q9@VvOgSb zg*_WyAl~E^;!Q3K?)frsWHVTBtaU?h{wz3gJh@?be>nBfiZ#RkB=k}&h}H)ShUIe+ zjMspX6_e{;--WdCQ~zq}X9G+7b~)!^ST?y}>;Z;54v)g2{qgVsBa!;az)@_7>~G;A zxn_@1BQR`T^tGf5@sNGpcp!E6zTO88u%2voJ02u_;GW$hXL#-8%PAb$k;hfQkkwXVMnos~ULJD%+IUL&R1Wu8;qLvDsyrsMYheQ*zU z*BgU^J<8G0xic4(2WI?Uormx3seER#`;fi`dO`VH`XK$>m(+J8&H~Z>AXNsqXV7`12n1wfA9A-FHHr}2{1GeO#;7Ye^ zpE83zQtM12o9+d)W7E5o_jcLQQ~TDKAb94v*|U$~8oIhauG007V{_d1TlU*1qw~K7 z=ipr><{qnjSBibE;wrgE3?rci=(*HWSz=wEQN8#&ODU^$)3#X#+9P&d`PR{yFR1@E z^svs>x4wnqxpIcD%4xN64Bv{~f6=A!3ha?Pmz%hjkO!JZK9uq<1N3JQcBH+1DO??d6fc>DCYzHSKg@2g~m`pKDVN7~tMS6|zSy^oK>H*L5TM#FUa zi*FHrvuvPH)(oHG?A7;y{Us(JJFI_9{y#d_rIY^gzW;x`y?cCA)w##LClg>M1Q-Z+ zR1#t(6V!5-*dUX{+9b3U!Mm*lIJKGJ1@KbijX+wP5JZEt^k7d3pmz2k+9PU-$4Ve= zPlC2R!Pe8$){0zo61>#lB~c*n_qQ*}ObBYvdEfVs`Rv(yuf5i@)_QL1SIk=De-Er)@ngajw5;|69?E`oeYL zx$wO=uA3+Cer3zM_do--?|l4xr;JYQujV?1IrhP0&FvQmqcj z-gA69oc^Ny45G{Q)z8z>v`L&&26*bvS+~FX(6092y2kd_SvGCALf7&A-q%Y%k<4=O zyql&^GtcFH^uHN>#qIz9WsXJLtpkt?@U_1B{=b>ywtlD1u{FOg?>bFCTz>8y74lSQx-owIm42b>Z=F5>*n2q(`tq@mz>0C@y+HFGXho2 zX%=%DV@`F3b-aI4sn>W&_5Fx)zAG-~e7EthDn?6kv5fX@c}iRr@-*VeQ}_Mu-*;_v z|Gxqc>#MBfR(y=jqkPl5FTs!c?#pe^UMVza_4mUW0m+rvfF7OAh5cFH`DP@-{*}*x z{h>J6AN_)`-`)r8Melw-?B+YChy72y^NmP^J@s>7Ula%X?Ozb~O9cCnvpPPwlzi+C z4u#+Bfjt9!>&Bsz_gG`zdIZ=!_bT7MtD9K3GM(>y9~M}+lFoP1MPA2eCVu{_99(hV zPp5tNe_=0}-Ll=dG9bT0y7HJl#=H6NJuoNWJvzwnL1`}uCw#xNVe7^ZzVZFZe&*uq z&R+2L#%*8R7%L7XzHe^XZg`#ev3Gu_F&^Ksg}Ozz@BY=!CHHdOwoTTx@#9HWJmdf>Lhq^YlqZ|9s>HXxHF9yartQmt- z)@%wByEun->xdtmGsr$q+(_qJ;2+O@KF@_0i-?KU^J{s2;_ltn-g#Gn(Q=62N<;Y* zIpdppmCuLZs1~<=Yn*apr<`@+r!KwA_*$z4^WhIB;ScJIPJj=F4Wm;rqAIhBI8>cm zqq4uI%(hJVkNKiWl#|`I4w-M&^VL>s3?7Xuvln=@Pu$&Bk=@q!Gq zJ!X~lme{au$!}ezx|1od>u>4|)x%w*g0z`6qBKQywn8T^4=@0%~Aow?%hTXPQ=ABOsVsv|C zMeC=U+tb)88;zD@{8&%be!_Jjeg2p$w#M2Ik%u4fe>49(xQ^$Yug>n9e(BVX61%@9bMU*Ho z*l^-*dkxvmTN*trZS-@YhjSXavL2{~r?o6JjM}%k-^~3{t}D5ci=|~e*LS$z&2=#J zaw#GY#Y?hY zOtt8}==YNX5%f;kFU-uZnCb9+*#JeqyP3-Z;P)llc^fpEo*O8H5323LWb!>whyEA& zh)+E3F_kaC59~Mbt?7JU^n7X{?0nz)!npxI&-I<)(77T09fs*Y;W3pjU|>4_$n@C{ z{E?sMn2~Ls7QtS9|EGZnuRV)Dt~vE^FP`Ycc(19S@wqYHrIXI>jth9_ugG90W_=}b1(scFqZ6JIVoR=`nT1xzNtFg|YnUU0el_bMa!WiyM1&0V*O zU&zm`)ti)k5c&}PkPFU=@6mX={KLXq;PG7(w)9TfgecotKpxmC&P1_r<7w%pJ@%0* z;v&{4o*ldYgHIL&*PRopFFnT-ESpM<(na3j6kuCQydiNa!+T;b;C-@NGe@d3{ENgo zBh(qiS6=l*;GObIR6W{9mo2>N@ldel>Ac`N+FY}@AUK4a6^dPmu-_mb`ym4==09;^ zzPTg6z~l_b`uE}cVf=G$eBNW;v^U8t%^ncEWrESUoH@05Zg0DWokve&c1lixf~1zm z8b|HoOcYBFe#rQ_dr&dTkIl2?Vv=Wk$7X}PoWSM>9{JG9Us3&SeQXBySZ_4T*R)Bo zQsX_|qw#(m+QXA<(Rrz7p~F7w9o1nV*O0B&8Z`W0|{^*zk?%PpC zPA=KUH5LPZHY4y;@~>?5{yjJ9UD#ptGj@c*zca9*;;%hBWeog_{uthd**ubAc?y55?SW7J zABqe68L(B6(|C@v7EPf{{9XmH9Oy<8|NchfmdgM;%PmeXG~^ zz$o4Kk8NF}HvGk2zv# z8;S7vDdXa?>iPxnEQ^Du3I5&P-oV|a;y50Fm$}a@yEpo%d%mdnYiX*tM)qjo=bCls zh>l(J18=|V58(@Q!S|wcPfH1SK512`{^hRC^WW~;u_Bn^sa@$wU8?WC4qS?1ta0KS zeHotjbwAg+7aqmL=T;jz;ekB%l#N>UwD1vt*E)NIWi^R&F25tqrTl0$ zuO+Mr+8`%9|66y>b}-v-cJ3u-EZVbPM*ZS$@Q&ubLf6v|5JYO`Tc*QkN&@0 z`NjLMSUk7y8iyad{bw9gR?|M?5Rdb%Ple1j`d<1tzJ2fujX@dxhZd|@m{$0X-rd1F zjl<`T0kYwwb7t--^MCi}_dnHmdRYA2-+0$&_q|ZGxY)=MKU935+xM&8emnCXy)$v% zTepYIqv|(vDgGllYxUWgbJZ7yhu%)zx7}dR`3Jqv)2*bh;CuYJ$SdJ3mY(MD_}1@B z-t`C9`w>Ti97I;>Tx9t*N>&L+ZC}jMKvHYFz@km)y7;2Pvu{-6xh`FF!Q2# zMD<kQwa;&;Oc=G2J-n;gflMWA98+Y!{iMwAgKSq9id)No%?PwHo@LjRb%Yo*c!>3U4bS7p$D0>e zz}(1- zsAJOO-1=`zgr_w>uS5LfD1Ihy;};U%>*;7kUnJfb-+-i`HGgA``YhV(fql3I`%d_3 zm7^o>TYf6tR!2i-4eL$SZ`Ffe?o!~FKhqra0vkrz1^m&WoJr6W2WRokiEvtRx9Exu z{n2wo3&hEn5RYWlJJQO%V8z-+7_aEeJhKS@Z{d>igG(ok(Z6?wRn!*|n^;U-?pkn7 z@8s)Q1)Pczx4v=WY@5(k_tBpH-2Nt&Z=%qg|b$z2*s%y*p^N%v~3Am7kv4M zu^}Z{=kXa)e=2?n#Kf~6y*xX~c$P7=_04M&?M5FsG&W;lYe2raZn@|+iDPQpm7M%J z3mYRt_jQdLaaqWG{jjlN55GaQ-Mzoh`v&jlRR84zCO`PijE$?GyR`O6fvux=HrjE=F718F^Ocl8#J=Z_%Bl!92+$#ol zD?GJsz`e-TH1NvuxwG=|T*}2Gvmi<+Bm__7e)H&zkU&oj;yFEA8SX%ZoP;+Lgf!?FXFestWncU&tQ?3n0<@UQDQdrRemx+>Q_JVkkd zB0Q^0YM5=ZpQ`?-A8wyizL2_Yygj{6T?LFy6O3}ZZ*HtqTK2v4%_m(2o);}TwQqi6 zk&SH)xr*b6?_0LhFZIbsnWl5xx?P{;wCr4so9cFWRrc2s@wFNsuZl*-jW}DY?mWir zOGdp__fT?MWf-RXeX1y@xqlX4&qLlcYkr#G5gMON+K7M$&)_5Fwo`U-Vmpjmecj%M z+$j7oz8%ev;MY1<{Z-lrE*M3mVw-waArdQoJx^quI zR?!F5Z{u%Tw!YCh+RbMf^~cB?Vd0k3#+o^H8%J4>x^0C0q56_N4Y_4!hU%ZAu2Sxq zOABV%gWP%#C)>F5JalpnR8iF>j~T(-BusN^YO^+*!Ac zdMX!o+-=u$XVZLp{^Q|!)2+uHzYBZK3whSudZxPdTzP6ee{}1iUu$Od4bR`)@2@@e z`(MX>pK?Lp-@oj9Z{fl9%JtSWuFtsNJN+n|k=PHn4~*}f@iZd*3Pby%Q6s+o#fkO* z)UBWXrA+G^KDUkD^+(_bey0tS|5@;Y6Xc&|ZtrZm!6ltBy|-@V6or`?NSJY2rsi9C`VoKs-St53dU%PZMfWEX^gM-^vZ zc%8kUYRB3tPF}jfi0Fl^iT&^eA9)kzIAc2Z>_nZXsw`(#4t5H)AzO!Amw2J{XZf`m z58HgH4jW1I*jXK6o|jHAa_X>cM2=nGQL?@wSFy4-Z|vL7ul>P+lZjJfztDe~QSSHQ z-(ng$1E(0~_eAlgT{tNenV*cpOX8`wJWRLIsX7%U^6n=wPR|Ig#)h4c746)glwEB z%A_yYS>3h0^c@3TOzVy<*!N#y-Eo{}vZ+-tr*>|SanaxL+_lx38HwXDV+j6-&`2rI zeb?o0A8B{B+rR&NX8&(jm^@fi{AJwCPKQI1Wzb!6iB zVCy>($;S@*gU~l3*`aci{5H|^@o%4baqwoIqZ^-K{a79*hi1bn;~Qn?dxJO5Ob(LQ zb(F@)_oS^0h5Jtm{F1#7^+P&Pgt-#zL3E)k^dp;(jEQEW6UjcQF_ewc9>0YGtsz(Ix=?xP3ZvZTV_i12 ze=vN9QNHvpqx`1(jPhGn8|BOHH_C5bW0WsnXOzq4QJ3G4o6z42FAhGHjc@<8q4Hl` zA1Z%(aj3lRhEVyxmxao=+#D)b`^x2WGwqgL)IX@Us^HCP=O)@&MmsBLr-XKv)6OE= zDLn_9AM>`}8LOo0Ck1SJ85iB~r>>2adhYD?WcLR@pn({4(!#m{pb?s~2dxOlyA%55zy7vZC3|ntJI_s@@@RGpB?&V+^^{k3BQ3pVaogkFx_ynK%7k^|jbC&WL(wNkvMn?%(8{t^>Xn zn=E^ydQIw0$2g=3Cc4 zLhwHHTGvL&CY4pWj65688JQHJo>jp+i8$8E`OD5T6Cmq zRK}kq!)>4FzV@M9J}%z&S6a@`&jTs_?GITZY!786I(;UC!fp@n$f)Aju#7pzu0$ia40@E;(6qHHitsH z>(0+NAKF)7t_8*iF1jT6wl_6fe&1#6M=fPP>PC3w3i6$l&h!OuDJDO1w$WMA%^Q2~ zO}%IygZId$q}W`}H@5f3p|Jyu-*}^EPh4Y~m1FPQz5L1a!1xT?AD{~Rn*lv!tuks; zkpr8Zb^iI3iO*H{4z&2>19=^P@T6{k_kzu1Z}xCj8h+`TpG!NZ2Y&yf&12ua=H`wD z>dMYDYUffH_Ui3=|8mFlz)`+gi+y_gtwxUMuMj+zJP?i@vuqiAtu?>OzrFsGKoL65 zH>Pk#^OPiOzd0-VsYO%fA?P8%`5_rzvx>0?m(6O;!36Svp6xMZe^DJTJ@iSy{Bo`t z{qk(nyp=uuFWY>>t&Z`FOw2J=kJ)vh8N~-c`wi0R6bBNo zFC97X1@xsO(-UGjTsz5!e~Km8N!kuiPq5kb)|dYCH@3d?Bij5Pzeaw2=}UL;ysJ_B zkt23}T;Q?ng|7YZIQN?wXP3S{<$k5c9h`t4ckfeT%d>1e#c$a#Y2VW2)!V!yIw^aU(4M>J zTJ#3+4Q$%9QhSwgdYPSvj93!vw++Cu=cK{j(n#t*BN{S{4b#B|tADRg4{S~v*S$|` zRwlLNW;lDT{Flu{dAIDn))Ce?$=(q0UTN{?qQ57OkDaIM466@$^r4RVQBFtoX*Ty; zxECDI^9J@!E#7VZH4z5Ua07OjBF3@_9%bo(jH&dh?ci^gLvI$|A9!n-wU6VB|J{_s zmNlw^+$sl{p98GR)Sl+*S@`TD!hgr_LcY!;d?Dt_p6l=*rd#nuyXDh7sz=AQ_kGhC zhY`_78PD5+sT6yupFYX2S$(UU;D4ka{fp2i;X%B=g7kqb!?5^EaWnWB=NB=@@LJDP z^ql!~ag;f-&IoYz1NC+Bm`Q;jqeJL_68~+T!yCN-{ltf@GnKw5MySi1YSr2H61OXVC$x_7&FJ^_8rgZzy(jgN)bNcRn>k}z_5OZeBCpi*NfkAlf?pzLhjT>U zroL}nb7RMi9zW-ZkAGFTsqfYRzs9T>9<05Jkky9Kf`7yYKd|`>dyHRdnjY`~XPGm` zr4G-|Fzno;b9pa3cAu+G)jyEuOP#t)sGGcK8+_kqJqi6~ajve)Ed5VpK|jxw*ne|o zUR>EM&U%&Z;>I`mXn(xJ?~5A}$6e#-XI#?L{f{(w?e(bYUOmViZ(lU<%IBQVP=F5` z?YZ_>V$`fP9l7i*+wM{BS7OU;1^-;x=K6iT2M>D(UyI$m{|EmE=i}$a|D>ex9f#my z$i--$;obh;1)ImtVa)!4tP*^+&rc6Lh#Z@I?XnK}e%bj){qVMwXbF1!N8ssLXr$u) zBy%(P`T9D~otw)HaK)B;W9k`q=2rP=KZf439GgM~XYTAm7u%Yt^_=43Mn;2emcC-~ z$9=zK%&{%R@725jZTtr`;_}cSWxD#LDfxnoioS&J#-_jHOToCu%6yNEUT&D<{P0xo zxSCb0Cr0CIj4iKoJg^GJ3%D128|jAuE>u9{t66V|#;YhV{|@Fj9+Bi zv8y+s-+qU)5KbhSiv1e@CF~}Qk#N+?WpZhM+lL$69#|48_YX%03Xs=#cqr%XX-3Wi zgN)16?{EHjdSJKTnDP$e=;rQSi9e-ysOE4p>v!$HvtP12HemDE@HdSdJztt{_ zuk;e~qjD~T8PjrPYq;WX0)|`V{Yr$lI00-z8ZTg(7fsG-*fLx;XdwtBkv26d)`-a zzogfFqqpb1%2y`!+^hVyxc9pE_PTHET|Vw!^<{c`>MP(L8Ql%P-t#Qs-W&IQCGQ*K z%IhBfmQcPjuD{e}E{VIZ#AmcI?!F+Y#XJ;$Px;Ka@l*W;z2C>(H}Za4-1zDKP~5%R zZ;ZRwdvCA%LviD$@|nHf7sS2S_nC3`mE13hyVv)Xarcakxunlqz3waH`dO)c#$k5d`d>M;=fBta zuR0FJ!%p8a#++|~;hmc-rb`^va`t3SiL=_XPcM?;5tmrIL-$N6u4@4!sm5wBfW z_x>pGEc8N?TqCRjM4!hfr#(^E{_4u?PRh*bE<4jyS=V>L&Hd%BJ;=Xotx2%Wv~0<| zz$&ZUKcxelGlwL5)+g%8CHN*Ss@#)X!8$^^YAL+X#D7sffEnx=WFfO|Pd0|TLGbMjAUXBQawB5w{S1AD(2|3We*~ogNd@aHJ{wrmxOYT z79$tFNserCWxqQ=uj4)Jt^Yu7{~mt)Byjv;g|Vw?4|%YWKl_k7FNKYqmv1t1ez)An zX`=oz;B8(J+SSzJ>1bvzaUbv&0q=3@sQcj^rdiOxchV?3@^_K`W{*68Dr_lzx;tMbCkdTL-eawV$)(=)6qpnzJcB_#kNh9QI5Ey2weGO%8)?$kc#rqFkv7d=V%_G4QwbJsEEA^Ju?q%X-&bQ-v+_S6TJiEmTSQ?YCJejaJ;-=cO? zsmC6ld40}d(op;x%avPucr=w0@W2IO(#=%{GmC)8A*7>G)K}S0Nv>E;o`F!{0nw9gR z(bdps1?L&tbQ-Cg*YScE9&|7Mx%ebMj85lcO&IkKX!&JjTE`g+eUGS}*zMB;S2K29 zNoU~ie@0a8`LP8GZ^B6ncUj{@nK|fS@vz)FK{k*y3qH4eBy&KE_6KKy7y52v5&EI# zWr>G126&5pW_mB{kUds3cFdu%tP}Y8^B)`}XToJ2#h!T`bD$5}gf8ZHEM!k^A#oIX z7TIRSWOe$}E9Nicy^pd*?A@uKR)4-43l#D{x&-{>o&MkWH)BJzvgbZhc}2%}dG<}l zByvdn-o_#APagIT&mPHlN4#m-nOxJctCuuf3E1Ht}4`SgJ73aV4` zReKW`Fn*$s(r;dzb36JfXTGiw%+@?HrjaE{EsF<#%}k?B(SdO3KJfbv;PpAUR8%%S zpg19y?o?l-AgRU0r^VoqzK@_2{F1R5%YUs~WH-qNr_Td7OMvqu;5-0)Zva;WeIf*J zQ`rOLkdDw-&jaug$Ho=>uH8TN>q`E+<5WlepD1r)W>Skm-|F~Ie)+x%=n)Su2xDm)h6YMfp9Fp4@PBNitq$Lgr)*n!4|j;;6@(Qp5^ zg)5_?9}9Ol=RQ4H`+lR`;CqedlKh3cOz3+{<*gPzOrV_bVR=7;v)$98x5nXw zdG&O1we-S?ZJur%&gWcdcYGp;l3IpS*J}9M0eFc?eG9zEC(5tJCZ=-vm8)|#j-g8Y z3o2LT_QR$n*e(45A1uukYvj%x`9?8!8rN$XSJe@P9|n209lRa^j}R{%!x$(=Y$4}9 zBkL?aJ^S`=f5&lWKqlr2bVkxbkUtk!*|*S;25*IgQW6S~sAqTuyC zrZ}|W9a*$nN}ZCGvQukp7hYkQ;w|EVWAF=q75mv(Z2Jq-jO7=PE4r&evAf+j7C};+^I~a_l%ZiVffzJY~6sZ`ay1_k88$9l9TMl}&ewRS}-qbZ@joy_a`r z42nGDHJcLJrFrv#dnM39Df1I4@K|F+JX?Lr6l0gxVx`u5otrhUBTHqxo|bgVi~bAM zFWRfV@>HCXEm}CE`1uw1uU_#7J7>gs^U*!gV>FK8D}9edXd{KXE&wk?2YH^*jwoy%3 z=|mqIM~!VRb!`RrtnqD3Y7reDLI#R|i|>aSU#)AUTWU;y2_KrqJ6CU5^Yz3zjBZP6 zX+TCdqYns=LeM4j2@Kec{kA?K{_-q5(A6gjp$X9g^vr&aO%JXd(Ht2Le^MFIlIY0g zPokk}eN)iG%l&+#d6Arv{vy1$cp7u8Icojt^gxJl6#tPPBfckppd#uLewa=ln&^Y( zI-Nd97fq)RS@c2ccnb%3rg7DK#tWUx(*myV5yXGcZ?|l6$6zx3l&sKNPO@SSv@U)hFE6C$Y+-DlL63Znah8pJWwNm$9Np*SPPcfC{$Kr->4CB73(PCid^HQbBjfbp zu@>DEhgR!HC!RAKf8Hc`!|lfVdro#;?&-(619&bX?g_i!`!Q^*lH0-SZ2jsWa<9%i zA6?KFy$(40s@tvOyk2$q!K3GMb{l2z z1E`+_AK9L1u!ri8R)CAu(D>W%+@-uPhK9p@TMa#z@LlN^&RDHY+7&(z|3qMxZsGDo z!7Dfqp3T(U{z_mgj4L zSNdZOFpJ-;0WJ$Z;OvGE*uubQ_5oXFMv*DFCtL=8pJ}`w*_PZQzV7msb->xAab-M( zU&ZN(d_+27^iXn3GjM8*UHGv~Dlx*V3@JA$K2ZcEMG$rvjf<#*ydZMdC9WOY!L^96c-S$9KM8!|X70 zqH!3QF+CuhtAHOV_Ch`zRgW2F4ZKNwT=S=~5MOiosBlUAOgzlht((uX<)CDQ%jaDF z))#MyG6UHwx0 z-{s-1E+lzsQ2%52-w59|9@V3*dON`ri_f__wB8$Al51b2ta1!|mH81LxmZ_tK|WW> z3E7k9@L%#b*LpX<<3j6xcyy!l?G~=Py`Q}=pmC2tH=ie4{Ar&pTjIy14gKXA=23QM z@%W^yiuqgUS4ICbcg*BobD_0z^WuKB;^Wv~(9`pFL>BaKSv~ly9g^P_{m1VJA(x7g zOO_0mY(yTJhm4ke&k@tgJ0I`hLA&ndjNo8Z9~G9UesK2Gr0HjngJ>vXq_o&&R`=S02Umge(4oz+_h)izz|vGkyZ zO%=0MugXNBwG7G&#;O-RmY%mN>5LuMG45K+{_A2-=TGtn=4>36WUcKVc|4Hw@RNa@ zs*616rO1ALSIqxRn`W!N$^S#v^IcV|Q*tE7gd0`&^S{y7^{cK_`2qH~H}HRnrQhzV zx@2IE))H2oNl83+>g>;d>`K=26#iqUvi=X|KRS<9|JDF)Isc>lf7to|bRg$k);{9l z5qP-16TcEZ_vTa8ozpBEfOwUZp(@eJdH7&bl$ky3y4e^^MAkU%K`v`EW-H z>^>Xtn-bboIhXI0@a!OEq7HxYw@(XbPU3l2A#Iq@UGO`^R6k}cKYPrTcV!TxWYTZ( zq_X#?1w>DCSd+9OA6(m%WQKUBWQK33@$X4fHvdI-iu5F7gZ$L>ZOT8Uow6@$PP!|& zJrny&7FmfB%tb343y!Pq$05t*+p;+-Sk|tT+C5vTahE z4d>}(+sCY9qpX!UpJdmr%F&iB$AT4`j|=PXdcnHp!2dU}zI6DrVb%QlG{2Q&y5ZDV z3eK(DJT2dP0~w|{cgA#IFE}-~ns1%`r#THV=fVNacQ&zpn%5`v4d-BLyoC>%_Y&sZ z#RcL08fVUhBO2FuUE!CEmu+d z*spq%FQa)=DLI%&px^WT`-1D^KiM>E$3f_{Tv)xnm*Ssl=T#;XmKHehJ0c zScO~CYvy?UM{flub1JJhVQa8ty<6uT?`X@uZ|z}#qO!3w91y8OEkK-TFe676}(KvmKog?PW%7!1T_uXy#EebB>fo43h#~AEC zuy$gsgqt5S-)5%8Z+4~g+>*hK{aR$#b?qKMN7pr%P^QtSRhezvM-KI~{H?T3%&PR% z{)}g?PghcE#r!{etU1G1vvpfOod}is~S~C`TSU)mW;eFEr(E|1dSr2R7uIJhdws1<-ZF-z~kuj=oJLS~&6S5&t*8?w23&?&YoRw|L&vi5QE9tz_MTNiH(JivD z?a6Kx#J;DprO0;Kwb6&mP4r&bp(Hm%89R@b*+SOcuI(zu`O4|Q7Xc2Fe(Js8 zHfTqANBr0pO!{VEf75e6YaCZcHh*)f>~ZyK(UvQNqVP}ktuU`MU>>sVV`k-<9no#M zrsPwE_z5?EQQ?Ll`J2XcL<;(~NG3%OVYAxiX_4M+uKbE=PP)+Y|9pb@2p5jBJvI*$ zeQ)O*$rJ74m9l47LVU2hR&dt^HCqgm^%?rf_*XxRFKg_uz0_-8UTwMS4z)9hcipm+ zwTIRq>f^v)O$&&JxNDDiSrXrt=vMRgzggoPt^aJ^8t;pLoLqOVAC8slA@1xv%eR!= z4J!@%T<+it_I!Rw9G3EryF6ldQj*0#toQH__xVJ4<3h()-~7D&t^4jr7diQdc&_io zcieZ{Lo(<49vS(q5PJ>s74=*GQqN*5i+Rz*ojtW|`Weq(4^%efDwlxkm*n~_xj8N3 z&x_Ogs`v9lACXUywzU?}-dO|~KDvth01NZYBrErZ{f%;CTtwNg#kxk-@N;tp{N(RF zW9j-@x_R2=`xkL+QLVMp$Sa%_ox`|_Pq_V7UMj7%u3){i1;4Kd_!)#3`G$IGaQN#S z{*A<3RhDmId_G5P#>(gT=27|~f4+X9^}@N7r;aQe*wq#IdN@35sXw)*1iW_POJk39 z#PU$b@b?#X2tL`Ad*>N!U6VL>D)Y))iFpS9@Rl)L@euJb?2EABdz=_{?e#3`@7WO_ zzpnE@a{h$Rvo{GI$Jus#FJB^fyydH)Yb(#t18dv3MxpzK{Ez&6mT8wA+g&zN8TX$! zakmbgozwlFePsPtEcs^eDpsKO>}SUsZGQ->USiHw&W$Vol=8@;+D^*P;flYsl@C!i zcXxjG;A_acPRUM<`IVK^0$uruwnNFbo_zOXq5*8x@#o%TueR~olG~Nn&k8Y5{-qbp zlFaN~mv0od9mSqu3zWRslWbf*6P~91r@Dvj*x3?q#q6?U24|gp+luKf$}@6GybZI1 z;(fKFcK7BHE77~wcdcW0iA#LdUi09LQe6SU3E!#mDyv{;~=w0kzf`Qvk60bI2 z`^h7sUm;&ao#N6dAAug!53NlE^X2#fDxYW-ZTd2e@?WA0_J&(F#@C2}*@<2eb7(K+ z&Qsw(u(ct#4Y`cXE&={h;L4)yuoGXuo4Nli-^Li<`z9Lf|0|bd;uzKf$BhmQWw%WW zD8_rUqaV5B7-lU|f_eu|dMg66`_l+_-cUE50 z2OgwFr#R!UF;`!T%j`BH-fOz!BO-jOaX0qh<4P>NUvcry94Ou%`)s}1yO;J%%3`~y z_fIqG`|8)6`1$@W<5tNUlm7Ic^O7yhH|yc>&_vwJcJNN|cY5bvYLxfY&X;J%8avI; zza-+e_G6{P6!CqCc8Y0-oYAA$`zrU{VwCShf9qYRa5oTF=bFWdb*jEFcwPd{ltDA` z^gdnlAR4&Ujz8~R_a}dI_?^bTtksxx4Rn_QkbReBBps$K}5f+VXeD(tq3XD>&-J()(7Tmkaj>uun>y%N*z? z9h-C3m#{J7>n7j03HaY+uBy!aJKx+e$r$eXO`BKr_)Qn4%0I$)G`*r~Q-jw!ho`>v zsjB(a&5;%O`Yb)bx=GI!FHvfQIu#3o4&S2vq*81!qK(K+N0zN2)^Z^FLa~2fO^7)! zT@8J)-&wla==?oAqxHn*^?u{&cE878qdh4fa)|ue|M2}_WbG;6qKnuu8;S=dRums- zd#`HMrqUaX&gZGm{QKtlt>j3PKhmC&#^t5GzS+q)$hyu&{qfVIyxx}<6BpzBkFP(X zvg}PPA`W(2AMfk*p1A0r_VMmt<6*nd=zO@3G7rU<$u~IT#fB#yh9Ab4!6%>G&Nj@% z1>WcWpI~&}?f$>W=)BYYe+fR2&j0v2SH#y@W^~@zM;YQ*d&c%=@}0VMt}{AI`Y8YH z`109C=T&a`^O3Rc|EWgjW$yoP8lB(dzxZJ3>g1fc&hvOU3*+mYZ*=DOQD#njnHBhK z^ihUfx;=fl%jlFoVa@v;M(26%|N9K$vz_Pn8=YEnSY^(!&tkX!&onv}yKDVlV{|Ik zSO4Q-=8U|awyv~uPW3KBZt|Wo*V^ZP_AcX(FLOOPtK4=M%irJ5C+^25L3Eb25FWy{ zjBC*FTfTBN&a;1%X_Lb8D52vMBcBEJNe*rkuHNfcXLT+#NrdeyDBH~-5gQf%j8F|j0H#w}NZ-kxw zXyxnPf{vqgNCkUj;w1!n+GlGx@jK|9zwn(29DJ*|D%aoZ!)cDL`KyjV*Cc#CF0k)? zqk@6hLNDtIPc5-YwRym%x!F517%0dgK4)RFx$Pu2g+AcyWK3e{zi$k%`yIY?TA=sZ zE{6VWt?!l?wXZQ=u>tv7+jeSgH~Ks1yE;!6{u*OEbhgn8$f5(4dvf)>6~BUXY&7zb z_~6p7GcR+QpJL{zi1{ky9O|!`%wMhMQERHhEv%_3y)BOwjM(*P!N^_i+Q#(pT9HE9)&wmutN!aeXTL+#%L3yP?Guk%EfUMbx4wQ= zn{p;LZIW&C=WkkTQfrKwXWC=*3Am~p!^Qyed0>ykXOO*^D>Ubw1~j+DdmDH;F1nRx z+o=1|0^)2`(ERoaowkE9-m3 z1**JTPJY+Vu>WhY&bIosHMzF>XX=yu8tu5hq%T#>pGCXE2P=-2ed+1k4`gn`jKxyg zE`y)Q=3BLc{DI7Cwdpadm?MqL|Bxg1^VXuSP3kWP;%riJc>=!#kAPoWIRdkhg~Yv` zk|)p{pE!oy@$av_nHc|mmc5p8*Ouy6d<@xu=j<4>KmC`n;g|fpv>pGx0lwASmL)q@ z9`>whZ*`6GFfMxUckEf&V~>-_<@fo=;;G3J1;FVl=bJd#J;whu1os#R~&#Z9;hu)X*Uc4xN4Xtxq z>y)b!+_dbM*`A=z?cIvJkRPUUT#RKdtDHLajX1Rq<#w>?`(tmww+el7xXyLXB9|@kFV?(%kG##w>2y2qZzHd*J$HTQA5+YNXoGm?mXqUb zT_*y3LE2DX*X(}N>gy_=9bjI%loxWMVYb1CZ)${h5U(pf0sor+qn(@^uX>z3xH0;K z?!Dm{@UI+e%VPiVUOeNfJQH2)q4Au>X)w2!kS|R7n%WMgC6>h&)yW~9og;kk=82S} zOw};aQi;WbRgd)5G~$+(n?PqfDyJrVJ(^B`KIoOpSTK8N^Sgq(z|L#jlgHZwzHm+o zZEQJsWS;uImGa8}-`MEnEasg*h4$n8u4I?A`FL9N$Lw>)=(`n9!@5;~)gE#QLK{ z>Uma*5xAAI`d|G*R$v^se@~_zFJBn`Byd-zoxi5ZZv@ISz1Df>e)5M^(Vh=_`35xL z;&k;VQxkH=G~ipIdHW7!T>jtMIW+;N1kX9lV;yCGLOJFBilF0}`JtShl0 z)-mSc2}X_wyJO>jQlEFBwI|&AabkVi%Qe6`*8AikK{l~}TF!sTN6S zk-cz^_fKl0`ta25I3vkrTi4`;%^6?pslR_VXM9Pg zvBt~E#qve=-@*qIaZ31d>_DQf^4)$1S;ld zGNyWdu+`cVddT-B=B_i;Zhilk#P7w+1?zXN>6s((YUvs8BQwc0oJ0StwyXb~`0Z`T zO~I;p(|_e;L55oS7Q*>Pj+LLrIom=y=1#`Nl4;m2KDg9tcBR?-C`~h2^RuU-b2biv zj}~vHUdf3!3c%BGMt#>;*z5VOk@N9yEqPI|zW9O17cVcsQOWW@@~y?g2eAGnml-*| zbCBWnip#UlT_{ja5{q|lrR-mUOLU-pW8snNHvg!)Jui*-B<`1Mti}+#5PxR)<%Z`a za%o!Y3*k3sr)@0#OUSg>FxulCSu?6r>*L`yA0eZ)PL)o6azH_c>YHFB)w~T1@n@Tx zuO*JPk!;Ifm0#${bpPKH%M^i^dY@q=ZI4~Bd8{Re(L+AC)XtgV|DUOSp0TTQW~Hxo zpCz1Oyl4q3<)dS3pdp@)^zJG$Y@m)7O<6mXJ+Bz4zuXCc~$60(|c&Rm@>ZtxN@u<_E&F<&D zTlP8ryEGSKj`Nu7InFp1{ULE2-Lu*I{ld0)sUNDxwQGIe+3e^aw(TG-qOec+Be zhh@J>oI~+Ujb--s!}C6Cou_fv8Hv5ud0WAOZhUM^97plaE1-pLzRkH`b+6ldDP2R1 z`Z-3D#aD`U^Bp!)cYfv=>DF_9@8_YA^}PAT#PUAu3|aGrO^cC(#t;8X;TOC(bz1hh z{ipf(-+F$V6#uxXL8sm}=e_D(tn#!$eYNOI$p7)#uCI@~chsM;&@;Ugo$9$Oe>3jP3j9KA z0N%Ur22(!9`QhW)V_JkPbKivw_6y#Cwjsh%1I|JT^#ow+q&Mnwn^(fc-;LxxUK*0I@EeUKkj|L{r>~%)f(P< ze_z~t_8av5hrHL?-g?izLwEfv;{IpH{Xfh8@2-bi1wZvz-{0VrtE4Yud%h_L%>M7@Ww!ctv;FKi?QCYQM7tID9GQm?{``yZlN_IGpKW43mq?4T?>sQS6&)dR z(~~R0ZOP=uO~U^nxpvWBf9+myw*(!s)Y(fYdNxt6mZVk;TlBL~XBKPny)(Z*e@O;w zJ;V6p7QR#a4R;<{rZs|K(7kczk$IO9vo3zE`=Xsw180v8eNuwnx#&T|ieYqVT{#2= zw|JSghk)*;Z-P$)|3;h7KQcMk&Aat3>)s>Fs>l9dec1}+HG87rAO72Z{~w=h4+dUs zFT`#a{@cm+(mNEtfKJH0hwE!2(ZTPNuZ!(7WWIndmWLl9d?(vE7v}4nmAlq6tTv^z zw4;VK(OQqM=7D~poOS+C&Vz$OIS&mD*Sx{ebz`_L8WLT5Cf8Mg=+Z5N zqD%J-jxK#8HCpCL*%vL#>hNVSN4?sSpG|3*>|_m}(~>ifZKq^F+|UBvwo_|+QLPFHtyZ4B@A)y~0J$HyEfzE)*JWk&*< zqdA-d=Z(XS=AYZRp)&WFwmv^2(jy<)tIKAsnH}UC-xD{98rkSi*4akV2f&H! z!6%FjRdZ~eb?&3K%xXgh`8@d5U$kte+6&`b+D6P_@$6;mucOU$z73u1`Y)p-rFIpt zv46e6pBrlT9iR7#{6$CIQ?lMSJG4G}ZCOVwUv>Gd9D*8WA9c*3A3^F@S=B#|_iL`R z&kS;5Il9?~CB=i!B5=f#d=?z~ew@e7&vKkT-{bTdU17sIexITjGH=;`#`e6@>6ia! zi8NZ;HP71PY2Gq5@IB_U^una_Zn^+A=^*adYfhRL9|=@{t8#txg8TQ24pP1vX^-aY zZx2J$Yxca^9-Dx!!T$pjLY-~=e}n5T&bnE-I;p9bD16hbDW0Lx!JG+8 z{*tGMgm0M@2rrwp7CIReUOsCrbTT-+V%A#dWJq|?thLa|TIgggbh7r1A<-JozrQ2^cR+d z6urA+MOMJb338t5f&R&sJ*FhzZ^;eyWfrhlG&j$txmIG9T^Z!B>ftjlRNH*!cAmL7 z?DCl*^v~rpt@r=m`OHtK?{STpBMXauaC$!T0cUu-v|aQ4FV1H)mUW>clf_@ZMqB@H z^O;E%{~VvW4j69Z=kl2s>6goAitbCq2ba(IX`?rvxqHp2xS_uK=xggcKU}}>+A53J z?EA@c?J2))ws_6H-*)xMYZ`{LHx8e1Wv>5T<`(*nm${O)s(<^8BlDJqiDgD^E(Pz) z!hOq5UtuUG8=ozIdZ_$ut^?~HwCGf{ne9m_4xTxBSP)uY3N4qWq~zc?Q(JVVF|7>x z5Z#t;vgt1NovuJQsb6{7`NPcC_KSCwLD#MA#;($+P199}e1S0ZD%kdAWd%5s!J^w% za6WdjpDB9QyTxbr&~L%Oiut$WN83EJVSkJqp$EtpYJETRNMPl)N%gJ6hz-4M^ZHV3 zagv=y@ND@FuY4YT>;`|1cy%j$I`*&C`(w}ks$G0La_?{3eZM);J-*`Mdl}c2Yiu6g z1w1YvU$X)pxhRp3XPtW_;PP>oSBi%ZhF7|LTs%`eTzpeJT|8X8TzvfHj8yoy__+9a zZ$1tW-vcl2i;n|G0v~5@Kx5H%6EH4Md>7BhUD#YcE?9uU;p0P2<>S5T^4%HA@y`mC zXXW{_i_T;%2tMF9S3J=8(fnfYB3rmHwqJP>x|8^OR$gJT&Cil+ZwE(8z6p+82;L-l zYTs!8b*|_A2Xjk#9vVeHuc`U<>O)a!qJAt}@&m+S>&%bRnRc1r;H-dr9!n>9>RYpI zT&a5RNT3Vc5#KBUFQt2LK`(XnWd98|ogM%OTplSIa0hjlOvtNm{)BbqYa8dUq|VrI z@)f+M=wK~)1~qRCv~=jt;su(&(tLldbOejOQ_!dJ<0ym1 zMfaL3^AhNMt4-5|k9T&{^wzG4mfUsedJ$vTI1*cdL*E}l->bbD~8t@r%STC22v|AY3@ z#ffW`YZzyPSdR3@Rk`V2V@EfAe;eA_l1SgNAxGwkzNMc@CzHPB>S}SEyEg_$rKe3=>gZ^|kU-n)O{lLa z*8tzYC%zMH_tqa>{Ys60$7j%8KP1ghaji2v_u2U-3%zt7m zwjNJPaTRo&jz4c0`p)_fBPRw-!RyE)2VB)Ny8<^pd5Kwe346a6Ddw8IW5@R7);Mb{ z`6F9%^{cEv=>1i>IxoR@cF&qC|J{iJ;Ya0A&ZJEDn99?%02{D7@6E-h&bu$=$h>Cb zk@aQy_Pm?Nez@QKWP5u!<8Riy7fnCXUOGQ<-XCJ#$r)9@aHY|$o3|A)rg@2TUUl-Y zHRqaZ@pR4iWO(`@=J?BwZtn7Q&AE8Gc)6>e4~3sU0bjTHIrDDK_af%mn)9I!Pq*iM zt~=+zkTBM(^r@3e7Z0aZ%n#w;1D`4`JEzDjTwvrBz7pz4 zXYHKTVpwsF5nu^liH|`)TekZqb_KGTQ_0wpYeSZvUG}8CpLXzddoCN9OCNc+Q^@1! zXAk^`%xT$OcG;%mhnKZ(S!eM^*%N$wR`1XHpI^0y@56U$)64C$EBJ=L)1Jk<4E~R0 z-qaShEyZJ6G9r2*Jc9MrTx{IJt*ZAAFLUWs^Cp@Q9k{gm3%;Mi+)efbqMEPx^_2Lg z=1g;#IA_r4D&}hobGJuw*q*bmm#^%oDGPS^u?1AX8*~*6Yi2T6Q%*lu(g|F->&o0g z2cq-WdIni?mpbiv>QzTo67fiir&-Oo7)B@KS%2I8q0YHl3ph5Q;51vdxxBH0v2yvU z)*=?2_9~-iP5(H&EIj$a^<~(ZC9BM`|?e`xBmZIY}w}*&>Cf*-<3%_r4YzT4&jp%NeUB}{IPYh&m?;U2-+X(0_6?)rl z#fT4&-tr`C%`ZJLziKeKe$U&nqIADMCv4eHdTf&`U-jqA+TCNHY|c#7zd|QA&u>Tf za_y9**eMS}d(w$q{4*yc;@_K;zne9-YnvKKUW+{Je;KS}R|9W0_p(dL7Ns?IS%OhFQ8w(JP0SY0aA9 zsoEqvV30C1`Cobie&qOXcGHWiZ=7Yz9o6~F_?~sOMK}L_cwSM}_ig>|d|-rErNgTt z-~ZYE@YGOeEc{rz@1>WplfK){$8Lshq_d94Z)5y4W0&d)pGg~xWp=r32ScaZH4vGj zcjx~8a9|L!XE3s-CcJdkgV;T@f!aM&!Vta_Y4>M+S>9gRh`RJs52GV0T?iM{iF)()! zd9zdZ-T9XNT>fykbKU&Dv0*Q< zqa~~r7IDA3Z04+*7ummoH>wVEXd=#j2tVr+@6l&no4oM;F4f`Cl0T4GM;5RP_rDA- zSMXo9oB#SH<-fCTe%38duE>(j2EG1`bBxX^-pAW*_TiIq<#GJpt$pi}(bvXlpH}$p z$sRx3SRq@Z>u1{+oPsTW|6m?)TKfm^a^&%_=7AN%(mhpUQ-Mu;n+M@7RnX-k_7YsW zj16FI(yOf&)!y2j0SrafjQtaT4kuW}b3{D-k&wKHamxZhnieb(=P)iZAX*G?U`Jn&V1mg3bK zyFzecEA6>Bp|*O)FW)?lzr=~V?eop;F&rLkVGK2XAM?|ED-Pb58+y2yd1X$Wt@kyoV@y%7r~o@ z<|r%1F5X8j4ZeIaWy$%GozL}F>SIrK_)eZ*#Pt>KbGbf@&ErB{d5%96<>BXg&;4}W zbDgU9@Os5t?c}}uVsGUt8*my|d_IRy)_<<8M_>OY(7t$aU$>nprZ*L$8T-s)kl ziq}q~y!_i1aK+DP_%zCE-+KYq;aqpp=V4saSS#VDG5i(o2XnoZtLXL^*AzYHd*S;6 z?&TBsFjwvO-O6+M8!Vvx1DQtc*X_Oj?8LkSebXkqrZx$`WYMSgU;Li&yL{NGT{`t$ zkw~Lo1J`=f=(IRo3;r$<*Cew!GYfUpM=1Uh5uC0c*_+MDI-p8I$*1e&f z;LlF^(w&ZOUm1h%C-`=ZpJ9+sBiWcDd}`g7NOKKddtYDg8fp&D({=BQ=s=8d_&?+e zVPwaEV-IkMXB^^3?#pgn#l?Tsqq07HqXe_dU*t2u+H-^J3-Foy{|;U2eBY0ulddHB zjdPd-*T>ZL5y1B=8@vCoYT7J84=Cgp@1dWU&6u?{j(0RrUpK$~RbqXbNAV5$VZhU}w=tIy z#!!Bxi-Gm~yziTToc>(A7;A>R4;toT;IVU%jEI)DdCYSB0p4P+|G_-J$Ita!6d#mt zmwd!ZoqqeDopMU-ndn0N_B?phBHAcrEV9Wf;)g~m(4(_B^NY1oXIYATfbvWO-P!NQ zFU|3j>uT)Za$+a<-yKEV*=XX=#vt3qnj`8$rt0ur;eTW|`I3ZJKi)CL^2ZWfZr-O+ zi;R|!;dh?PR$4Ju#F^D^MsHF1>L786}Vs7Rvojyx^{XhZu z!n12M*Z6U&j@D@6+_*Z(0QDO`{}{X{FK#|0<9)N)L)KYO=oCIDW^i}XC@TiBnV28t zjz~uy;|s%F*nV{9kL}S%`uhIBU#-1SJMPd~Kl*>3NQ;6utNidX|2H09pZ!*)wQrGq z|5NS7OI~VkrC-HWAGhP<(`m&aPGgUh|8H=8ZLqzb?^=rAAM$M*I%@M@1# zYxz3H%3af!)q#V(*5j;a|EYaZr|v%2_P)uXoL0ufcl((6t&9fA8zHDNkEz@VkV71ZTR(u;`oa5D&#vG8pr7xW6>zU|pKYFCD?~89=dM$Kx4fJ%i zdGfngnJ0GQyWiN~^8b`SDl*^||8YAmt?ZF0 zzTCh9$uPs?kGg)PUF`AXqd(*?v(H0*blt?j8~BDDqW^b}#xC-xu{;ZzU`(#EVuM>V z>u)-7H#!;bA15ZFtFm8<{Cy(ckydO#4E}9a#+^^psCT2f-$k7Bl8*4rZut9~Twmw< zDc`O18nq{N&(*GTjP>0iy`#)qT)Vic4dwg79^?FvJA)~+=NzYl1-9Hi%yl#0;iGHG z6zh90dDyzYxBs^j53&&1W7luz4*EwWzG!;Jx83JU>A!Pkk}H?mc(#!Gj&fb8dK*>V z?)Q6@!0oo>sEt?|>`2o%U4j zZN7P;au_+`-2bDk|B=d(9TuEUex=UFfbCaTDcJ2CNHLy8oZLspdA^ysapSjaSnY9A zobVj#cYVNIAG!}|dpCXP_Q~V>G{t%GY%MhTE;7NGGRLeM;mF2hoBxU)U*0+;P=%j| z0UwD_&j51dMS!8duG@^3e!4F4v?S|#$Y}BE${Z%?n(1lr=*n0O({+i_GSqr+v;=hB z=4lzi)y{1+IQohAEMnLPMc)pHpDTt;=g+1^@8(<$pO^KIdp^$QDfrRb=i|IU4kLGO z#lUaagX}TMg@X;l%s^J^?0}hvyEabad-bR2XHx>4hnwIZvx}TZ8QaKFNDNxiDs(8~ zyAAH2A$L)Q&T@Ij*ib=ESEI5b*TeZ1_2fuR^BSGcl50kFHZ3?jxoF6y^-*%x$nIEu zY~B9WzdYG)Uf$Fm{`WU68-56TN)g}W0mnUqj19ZU1Eld>iNE?vZ1^kT0ZHVQb8V1o z{uN%mDAAT*IQQ`666QfR$Q|&I^RWS+=NS~W{cKh~eJ-{Z$&V?Hug%mr9~;?fcYOQm zS3KBlcdU{eSw3qAduiI=*^w8B%ErIe!&&Z@EU|rUmb&(Q;7G9N!;jVPwhh3zJn`K> zD^F;{mL94hC{o;5#Imd)IGR0~AoNS-{+JzPCuvYMW=)|Cd_Hb zXdnjLYt5nNw}g2toMn{f9caiEo-Lknc=96VRk14HfH#bH_6b^7C+cv|c%jSd408mu zBp$RAx_-8Do^_^?cvK2wt@?a9=y_dL)*NeItJeP8{@Cy8+hZRzS^G6r!LIh_$id{! z?fuNHnTbrJeVwbLxn0Plp!yeBd}EzcA8E+4svdT^~<=ldc)6y5XLOLxRBTi%SVKE-%A zVLmm7g`+{It#(Cq~-+(uiUYpn1!MbGWC3AB&O)zT9uFUIfWo;M()~dzmBfm9r@Vzn1 z^5^DArju{CWBh|2^8bz9H84ZDn+DXj?KN_gtGH=3G1!@t$!Brxu7TK0WSe*%Jxh6^ z7vfWp0-r1Mtl6|3e3T4~pi{Ks1IhVN7H;pmaY{h)P%uZ)^U@hRpYoUBPjuk^5Mv}e z=IH35y`h}LzYXQw`*Kh0dU$E#dhI&qCi2uh=7;c7gS@hSKe_%`>s7HPEnzJhrtfXc zC%T2TW^4^l2^eESpTzr~#?HyN_-ab2BeQ|kmDhF1OPw$OA@TDrO*d1AzVARMao3#6 zjiJ1JoV#q6EzaxI`81-vR;$iLSzZUdyXV74IU|&{Gx3;Lc8m^#JAa*R{zdkNb%xGq}3!*5j)0_zhD6%I%!~K-T#_F!kW>Zv1i_8oun}}X|$m}oDOdBt|+psYUJpb zq%YO~^?Rmr0uNuc(CWjq;D|?_6+Q!BQOT6R7Wjqw7h!yQ=gZL*dHy(jC3H5*aPT7?yh+2y+#pX{1iTS03TH*f?s#TjH6j5 zJfHdQ_spHg?l#4bv#>GCXQ_Ma$&DeHgV))%3Kw78ddn_973WmmmvT7J`jjK%$<2(e z*crS2C;MZ|ceQ7g{;@qBT`ki7ZoBfY=v@Z)XQP8g3XGOi#%(jP`=U$L)eP*epW^l< z=o7>T+(elWbm#gj&h;Gb_E%i<8NP~s{1h!;H2e*Ee9<<}_uU&irP;#F3lta~6!opFGZjZ|}i#0-~L|Nw=D^wJc$OOnuV3tnnuU zDL$j~8|;VazI5w=Kv9OhC#(C`S0)DJ_uz-$7Qc;M$D!qD(W&Da1%LeD!FF)xcKBd% zM#v0j_#fHso!?PPdu8B4ly|E40=|C&9X-sxT5-RWnk(PA=M~X_eCap*E@ZahbFc9m zy~n6EIJdw*lsroVj2%JN_9lE0eq)^jyih#YJH)#MIGx3BbhTHu+6H>{k+==Us}!e7pq#PXmLvwr84?eZl6?r^2=&N*-gQ}8^iSv|tQ~smC1q23^b*%+|DRnibo>AG)K?;CWc<A6(dJu9qWJ6%DYiK&h`N-!~P;W??&k8=;+70Hfk=3=Num0=4q*B&YuCF zq`&-xxSR^|iTKbPg;z#0c3kkRfjxbFBRspG{dDbLF2;Ye4nDY-cXiOn6aQw%MDB+7 ze89Q%A6@lTY*QDLW2}H2WBKfN<(aLS|9NABS9<^>q6Ao5-#_hq|5LtKzIoZF>%g(+ zuDa6{ua!=CHR~j`4_w2q;J=>hz251$&J$Y8_X{ume|UTI_$aIEfBZf(fjpB*!az15 zU^c+a1X%^ZnP7**fYzY$P`+h%5G68J+d3|2L-yidundiC7x#ygF?m6e4d#-R(Hq&pL zk8x$AEO-t11g`Q#pdac}y9byH^mg2JV#nP*Y7TpW*g5_tjvIe>Y&yReyfInv-)%ZK z*}^;pw0U3J>)-Ylz&C7f7P_DAzhJ%ck2|Ml?Wm}3wmf-+Hg@5r-}|!v^+(8Pak%iM z^FG4sGG49tCTfj8gzk8C-wu4=4*hQ5#<;!vO1_O;I&p*cb;vmI;wAQV@{yrAvK#a1 zTG-c9q)-pz(xx$P4)cb7S?cY|T!DQ}dz5b%?d@*ZMU2hTbvK7)xcnnuKM($(Z}t4I_1suP``k*d3CMI2 zWUp_7d~btaYA$>u7bEC%6^hT5XPHS`Ax2c4`M|#|A9q51!S4ejsvhy;8}}}4;x}~Y z4frmA&O&~v2!YH;>`S07n!hus>MNxUxvE)dL#}#&brVfhKULZ=RWTPrF>=St0{*5T z-XDcel+*&gb|;pJ!{wv2oL5om9oXr}XJnmGS>uqzo^_=a0#VmrOuDEvIeEB?BRZIK4cc#EBjNYGJh9pWexx)?)U2b?PyZ_jW znU6BAjd6WV4gPK1D`r{#H|DD_N(VddW82R?qsoN;LEY!Ix(5Kmm#DW7_!)Rm&Ci)U z3wfPI=@{0jJU`uH9i5pC`c!AFRsHWD@Ab#f9!!oZ2w|D7{^^7JR@jH_-?rZ$bKA?8 z#8SM`VZj>xZ*!J?V;+9=k%c}$uVH*fzVCfs<$FDr;>7zHzpV168*@5Nl7`)_K>5x6 zTI>=0cl)&q42N|d;rxg$#U0b2=UUA+a++pX-0&$U8>PDLIj|44bE|8x21Evns)~yX zj>aY7&Yj-%N3+9nTViHb*2ImtP1!3~#rPfJwd;~EQ155?z;eh3r0VaOt6tbpZ0k<^ z*7bP5F|x;nzPOLVJ5^kTv-%2>kQqgz=1TPQykbNLPIiPRbmhpE>wHY*nT7;~snUh$rZ3Ha|dTvgd*kXvLSwGzi)b(ssm?h=SV}aj-(VhQf)=nRI|gHY9$UYDRU^1`vH+Xgoka* z=*NwCvNvEiar`FqRe<&gTUQ+HG`wp@oj0)7XQNIw%4eZWJmTSXoDtt#yXoiFbGvo{ z-?Ys_9^O`yhj9}Rj&su$x>+%P{`dAxw{R?37lbUL}zbJVnT?*;JebQCrQN7-2_+7fb zT>r{b(J^&>-47lm&+jvsda|AZPT&vW_@9Rj!}B+Ku9bVe*gI4BByJd=cnxD)3ZUDq zgUresEqbZPRsycuW>nO$5B9}A*w>Z%VNQ@7_L0#0?_j1pHly_kR0*V;i7 z?x)MZ*?IT_|3jDuzi<7Pn>_pau2eVg5{z7q+;tzN zx%Xjjh(3cdqkX`QwRs=z)|d^yQv9s=aQZRi{ZjQ?Npsx|8z=Oo8&%x@>bM4z?sEYD zXF>KLRz;z$Nq?{J{lIS^qfHm~&e)eE*$3#yQu@Gi6Z)~DA2;p>LQb0)chY+335LUS zB+1G76$=`6U{1XW7&6cg{X3MC^o=aQf7!qGs0;H1>(b%Mn3xtTd>SQy)e8Ph27hv1 z*+-PrtGtl zTVc;dBJbNPTxb4W+#qR{1pYB$9Z3g2v!B9`RUBq&I7-GCLnKorZ85BkN)9|lW=f48 zGhT>1oG`H9Xrocht7%1B)+j@ZfR2#>nb9!<>oRl>n2E1H<449AJBvCaQI30}tIC}Zo=l~yIL_Qr5;`gV+9}K%4HofjM%-~9?Vs~n}TzoI7$-e%pal#u&C^4xIqL7J+Uy z@XH>2GcS^ul@M+Qjf*gsdS*)QJgv>BKdb$@2J_xVdhS9DojzYH{-n)kjTebueV%r~ zX5&1yNi`VUITUoN>HdQ)tTgpM+y@zEpX7t&=-7x)9;4fwvnP>VRo z(xvl|&za9^Ab$!zM?Nw9c7HM5at7-o@H=dc{~7#=5BzXG?9fQh1K>Nz+v16qJJ(Ux z>ajxwICoHFo9{MBG0t}kpn(+Hl3pr`aqidY7>9i#=;_B1$Gkb|oto{^gzgN)a;9I9 ziq9I&p5x$`bI@a_+^Oa@o$`>pY>`3C+P+^Dml{Rx_A*gy$NfGm%W~Uq7sWfi_UDFv zut8bFR&|1ZMQG7+z@wMZbEHlgk6aiS+q(yjSq9%>kCxA_MZN8kSj~ER`SegVpV8v> z>6U-j-r~k9wAaS=;)bKWCZ7iQM&X~(iFWsP zNzLKe{TG$Tv%%w#t2WO6a?B|ivet>SqlJ)R3jRAZ!b5mTnM@gC;U3E=hJ z+gJ6~qJGJGV=?PI#^<27jCT0$F_WkYjS^!h%ge@w*kHf(vpoN|8_hPzb0sem>r=*) zSJ=+tKTfxtgq_89>X8pOunpF4)!^82P@B)%e0>+=D0$mhOc@o1z7C;2+k5Z8bjy3# zyORNnd2FbS{V$&pVrxWO>{qYDva9w_XY6^}3n+hrFkGR|KCS;f3u-@&?aED>73Wsg;mC@#b4^FWct1!MVjU#N; zB3#kAAHjdZcoXo$?wByZN=s_4)}n^E4f_KrJx?(N#6w>^y2sV zJj~N*g#N0vaS_k2zVjD-eD;fi!c?}?YBUv52l+PakSySg?;H~4N?1z@{+liEX+LO}zDC5>-%G#EpIwc{ybsHbfERhj?6}K@?Izzo898Lt9L)#5Pk#gN z1q|{O)?g-iuvB?*-lu)Cy}!c-UK}A_CJYrXsQlhp{T(I0zEI{* zgy%EJ5ytX+3+-vAVH=-8y|b(b8OC~9F121NWzGubN{;(*JN7F0uKRb>l^8$X3)cl% zMtV0HA{86jiFbka32DeWIe4z?1)jpwio8$3v%C?~jP8$ybOsOdKN5ya_&RHe7u5G8 zrMus{sNz)qE#?RK-gNuV`;xJC%2L_5c=R8au~31#@_7|DU2md&%>J|a9DGKY=_?`3 zaZMjE&%sJ<{X30iQkd!p#4uW(sbH+E2mqCHyJo0z0X3w=>tz+ zxOahj-x$%`0DEg-+xBq=W2{26u8QdvL(&hEFc3EQbHpgrERJ=(b5h_V{uIM1%p zzg)#vY2jLhj^|Nd$M>D^T~RlqE|RI)Sh!D7>8I{dbq%M@&pF3=u~N&OPMe>7hoVe7 z=24y`l=;6IYZ>QiDf*`#$$FG?tV?}Buh07e0{cuBu};`tRbR&;PQe(Z^ly7ug|Y5+%81KVY1KeJ=2PFZ(p1 z9LK4{DGSNz4<6}m3l4U-AugDGFjn24gT39E(%K&m09LK8rxfEyU48ss@Dg(?wrw=P zH-Nmv*jxP)=P$e)=ck>yZAcb4N{08z_A>+8?fS*#@9hEa9oqY|=ud~6V=+W`-p4&8 z#y2q!cLV3R25Z)OwN2)_4Xo3FGhpgjgc#33)Y~0U$NJDv9mb{T^-8oh-4Cko`@lm( z?_w1CXYn26gZ`X2R`8AIfo%oOGuOSt=h>mcPKAfN*0PS#69?Jm{b;C7^$w9Z+e>c$ zHu~FmPmdbQPZ@BP=eADVfdSmMnIXf$XXk!0(%p9h?wtYs1N-$B4e$5#ElVIHZh@@0 z88Txr=J_JXnjGB06I9h`gnb8GA4pQ=O&M^uMO&XHD$k-Z4`a|j?#X$L>(kq4OI@GT zb8iCY-jGigvKaDUFqT%&t4_ZF+s}p!NTJJP-Vd0#wuq1n`my-ms1{m^GV>SXY{_1{Jw0R6en zvcC0a&;@|^E9S~N(=lDol~o5i{kvRQCN2KNI!Wb zL(v;z>kw|=boXO@r(Spip7YG6e*R34c|UVdjc4b4)MMJ|FAKcy!35b5xIPHPm~#IG zaL%J|muD|yFz<-J%13tjl;?QQ z^IbfT8bvykqz}@e>nB&HLkiAz6zv~VHAn}v@lAB7HMY*>o;L4+A#Ac?z*1}c&0K4W zaF4~^29dyxjP;f83jQ?7;)0%)$+Zsa2iIm7&Z)T&n_$6yD|Kb+RZ}518Dj~ZOuJ8J z=uVP;$k>4g|D@<+7e~eRo&Sjh`DuXcGeXx2awEnQcgq=+^K7zYaLTY7)+VZW>-rJx zef3U&Z9m5RHmbUZzIMp)v2jjCOJm_*i1hr@5Zy!hy79WFYv=<;Y`)C_U0pVrvQ0+TQwcJa0S-sZA0s{13otH&2S%vd3orqLls>@h!2d`qBI^MiSHD^Ni_rXt+QR$nyqUQHd>ING`|S z9Dswm=k*#)h_M_F(?OnHhksa$l`zFGzU{SAD=-W0pLYgz{O zMIxHBCm%LZ+dL6aQNVt$3%FGY^(y8J1-k^V^4>WFJU3vZ9QGJ)b-ADK6e}2 zmRi`?rEQj5o?Y6s{NZY^<9Dl@e)~H5MY++L&GLHFGjeg}W#wJbPb_srI+vC|_T1{G zkDo(s4Y`?cJJOzB>i({qV`w&Y4Tr_Os(NYiLmACYM_yFl?|erdGd? zed%yn{o9wKjY=V8Wz6nBJa(jfPIYxxugjNgecfmJ{E)9=!7(4>arHVneoT(1j>wj6 z)m<^WedS-i;WK^wfiJ71T`hYFOkIl}LHv1w$MoY}t3#fxthU{*;qj7k$@!H_vpw67 zl#hRLb=T=9mKGlWy^mvVdj9#<-Ijm&vTqtt@b3=&RdvNfuK<6Se;-s{9kYQ9A{q{q{*e_8T3zty>ppkN8wzgZu0sm{T*{N>b!AJlmOoN0 zhqzZeZ2J_t$hV`-OK?!$@#s?5Cp&>F@FLo}l+LccZPka~dW(HI9L=L-*Bn z-s5<;9sf__yA|JG;@gjJJ@@|wzDHeMhtuyI(Dl8Z1!x=PPvCnWzDtCptaa8D)wia{ zPMlMGg+|=r8Dx7Q{afz(X*alc^FsBh)T!;AGR~Ov1ciX2T^&Fmk zjZZf|JX_K8i@3dYh?V^AKwPQ^eHHDC+iRL2*6{r1Yq&?8cc3ypxf6EoF=K=+&M2UF zL@74#X!P|C^bM}jA(OCYbIDzVjkt%f3I4PXRK|Pt@OCQD4r69Z(T)pzLZ8771^)30e8_`gYeyR{#CYnkbYIejs!RW{ zkMzh#Y`h0C4Eczk&qRFuy$XEe;CIk{kFkUBy@+-hC#=Kf89N*{JK)L!e79c)+qgm4 zet!vUmE(rD;f5_=SN9-dBwFVZwvXYfe+{t;)Yof47xKqWjF;o&S*72iz1`pq9j3}C zwLKSNgmpg_V;yiuOaFD3!4;vw6>Vc&a3IdIqld#q9P9YrX}l9~#3($lU#~~~5q%Tq zfj$n7gLvk+o>#_lmogUBr%PUsv+*u3;`@HIwK-rco>9YLr~r?1E{PG1^XdSDO1IK^ zq}?LW&OV}a-ag0;eZToidldQ@RGx_%9RK6`I5fV?4(OZoh=-o5^GyxL!I);9M|NU- z`n;v>nTNJk28^TIG&Byv%(fgDOFrf;$MHVe^P+qf>>|Wmx%poPpJieWXX06=B>J22 zELo_2;z9}SL8sU0D{%J{bwuhpj1}a($=ou@!t<{kczyvf=*pd0+J4HiavcWc-(~en+nW(H{^{YQNSi#fZR}s)i7%N z`^KSrCok=fG-sdJ;f$HyXV!&jm)-ZQ_sRHjz0U&dC9q!?`lSu`zv-8Ko(LGH``rIO zeZJH@G|r0C-`wYai(47=Bc0q|p)L3n1L;Kjl6=52nDhVVyX;ISrVaC#Fs8!(r{OWI zbN_@pyAJ*r_XB2-Cq>nx@U;>CL|QYqzxFz78nf!|uWK&R2Dau2zg^%kUT# z@b+MQeAmBJcsS;mk&_4GT+0nY(Q67(KXASiC-)nKlXxLMOxSa#O!0nO>+h4x`$Ilr zW$YDr7sxMPp2YpT+Wjonk1wxB8Mg{}=H4dj>A2$ECK7m!z9iy5(3b)H5Bk*&j!D?B ziuC;RdF1GXPC>r__f)ut)rqqLk#*#qVC?CQhyFMLa^z~nRN-Egi~g_lym8F;!u3L2 zpE1Dw8{|c4z&%gA({C;KX#nwMX-od4=8QKr=*H` zwJ{HDZ5wemVWWhP0Uu-ii|;9Mk~0MI#GOe3Pa^jJsgZqeTn#>Kl*DU4#PT-dvN~acna@+6$AcPMVbwH zK|Mmul3`25T`zjea9#pFwaB;`qBjBfR`7wkr0G4RX@tF@oHT_WgfR>X{m)H8Ji=E; zRQmn^aNP;|-hq7}jlMDVhIX_uNM9ppYXohLMtp+m=LFIhej$avL49xh_GOj6pW{0L zef{--*!n4c!}d|>iyTBdr+))|jmVvd`xOa)2m0QFdZeWcnt1Cn;5$>{g^zzv8}_N& z!8^Q{vdxS<7l^0R{npQ+UJK|GqSFU^r-SrCy`!k7)8|(x%YFtPRPPJ_34Z&H!G|&D zJ~#!vfO3)k+jh{B_Jo+1F|hCAh+f>w*x*kQYrAg3op#8fjk7reUAKs|avAvFMchYu z&fYv9sJ9^`;H`>?U)5T z#M&DQK=ZR03+;w8arTCzM&Jc)UM0S&j=|iy9c_I}Yj5@^laSBjsvhvNZN+uJtfAaF z*`dtIm)m&m#vt|-K&EygMi0nvk{21DQUH3vN7<0P(d5DXSHQ~~=SP%@xQ`n9 zCqoftvUyk+{E3~t`|z9XB}|%RxnsxB-f-9c;WGXK;D_U+ z?tII0V(Zh;HA^=%&Mn)pe{MN)s@6iUb0H5vK6BaN{*l_?^m@d#ZLQVfrd?N0vdG^T zZn25wP(HY%p8kGOx^b>lx@oSYz^mrhk-E=+bAGCO zftnL46n!m5Up?UW&FGJI)aDsml=!7pG3}Peu%^F=z0vnfqnwRcV`VL_xNJg)qN6M{ zP^U3#=cssZnR;|LG2lg?JVkF4{+^{|jGI&%gzqYh#%V(hBr}tr{6f8~TRQ;lBw14G5n(w0$~! zyPnO!`6@?Y>Sj}zvlKWFu_G25cp8DW=rd;7$_o=MMHoAO?~m%Z&xp04g#cI?UX``DK% z96HP)8mxM`j8Q7Q&-D(KBmK|S4W}d9DtLdI#Wc`b4*VALfA)6hkkH>4-^KqGHPbA1 z;FiBVzno^-{DZAW7MK6($nM_fd|~VM`Esmw_sYt@`@*h1>Ra?qr*ARh4T_{t?Ae4h zry#!x@_T%hy?G0LRhm?t6%hR)sddMy}qKB4}94>7WVi2?Q0+Ql$(1{wy+^ci9rk**pB5z<&VOs zNtT4Q1$}mW$bW(7{+gefcGTM2>-b09FEt#tSl=0OQ$dA>oN%m`*$s()t)PTVgZv*<<0(b|e5?eiZw(imUwOPKLnU+KXEzU8$C ze6@e*_q98EedHJa0>j~pw*^BNmECR7H<>4xV_AL}W5J$NMZj3H9=`HeAfKv!fw8o0 z{NvpAjenZ!odeqkx_;ZEq8_?TKiAJQvG%p!##q`ORnHxH+a_9~YSpn2-}#7tD1guT z^evfipKP3a@wS`X@aM2q*nJ(1#B*7bg8Yj1x0H z!x(z?ZeYfvoPiz8I3@b&b^U?4(W(0+V2$@;uE}q!cR>>urayx>lI`wzoYzMja20gy z{!-`%t0NacItX+|93;w9e#Z7$^ zEjvp@)qe2hUc`gYU$qKw=(&jXT)EU6h)ddlJ&ct@TVC*-40sAK&QjozIdJ75RSz)% ze-X(XEBDdo_mSQgZ&T+P@1j@e0(_C?AL;EKnP{Po!!|`}>D-+dk5pPVcO%*^N85(d z4Rej4kNghzHPqbaY*+UqSbsj8Cc553x={D&djff$@y$A!o9%AW=$RVk1D?>w7&6j@ zx^^bYaG#fUG5rpN(F?l>cYzJYHD>)x?JF=w(8YS-TQ$ajcbKCda*cv!N_L^b1g`SuB!3}>SUApd7 zd?Qs62@>)S(RLZ~lW@P9`QCLsPq#U;U%*<3JjDwCC8#-`qo6nXk@L`1ZBHu?7T2#c zh$pCb#~|hf`w>+Lp7`pHy7~R54ACu-#%O2q{cCZ@nwa%5ZBT_bBRm_x8yK%*$2KNX zr&ROvZ^ZK#n^apM0s4Q@KE}S-8{%*_ryY7t`u%BXf$b!ILpz*f&uZ4$*Li>LQv7G`;#%ra*J|It62%!|A~zZBxvF8`;v4qy2gGOH zsg5PE2^+i^d-vTUt$EAlrA3JE>aMR}9iIUk`5^S58^qck+Kpy0fP2Xs{HIhKxf?cb zppDG-l1mjkOIfGldpnl!nei4?CtUdKBuf%#aU)@bEnRlep9{Z673|Eajs=R$sH&L( zIgw;?O#Yy~|bL#m=dml)V#-v{hz__QJ)Gx*1L;JR(Fz2MJp9kRV($L~1= z+Y9?rxv#$i@+=c%Cj2AcWRG#Yhv0{zeCF@AE>+j=yivu$v8N_kI_g!sn(q@8pB3Vx zU{fi+4&Z^$9kb6`_+3~%$wCD~KMcQF_F0UNV>|Y#+V_W$hx)sxm3Oe$K${QnJee_R&@bA8w=21RsRx69aZeJ~ z9wXx0zvewfqW@zfWGQ4Y-YaW}{~+=KBn2Ta@Cd}cjzpYMF!pL?_&!XKKOydzQiYb7 z_lR-D`9sRv8*zUue>?HIgK-aAAX7P>_T6_;~|`DKZAIgz5jzeU&xi_Mc>~t zMpv4O)+y&wKe=_9h52O$@X_;+#ax;<4LQ+>2c93z9F^%rJXj_7xr!*ifLqrR$Q3<) z95N_IimCK#e#mmoXInB-^=mfWtjb6I%z5T5)08vkJnNX?jt{rtd?RzF<>0((9`MNA zX*~0K3ivc3hgz4>YO~@Tkh~V>cShwnZvZSp!u=koKNGap^Q~Q4mSx^f5>=VT3C?k- z!|{L9c;%SrmM-{xDXXH&^7>3=aecpclE)~gq9B{IL6zma3j9XF8yY1PC~l-DEOM| zw<#X_0^}3g>z8p(RKgk2O6*w=uIbu3904AV1ixO#r=^6cOfIfZ0@U@#dg|1JJtR2#) z-O#6bo}E4tH+&@27hJ{(eLK-7*UH+B&(5V!WXHze%q_#($2=w-&@Zl}4=M8%dorY7 z^VYT{&i{~K};xd?|%*i0hm(9+aWKhi9T}+8Iqgw>{NrqIB2?Qj|tXnR6k54SD3bU)~)7xVgl27A#3J|Rwpl>VU$|0&nm zw>4#WTP*YAOX#^A^bDK}Lv(|V3%D}+hj8v@dysFWqmFZ~-E8+==yr$&QO3dcxUT4W z^JQ`DpzEORpwXD?1Ls(L{Csx?a#6^m?TCF%SNsXy$*SC@T%&w^75#90<#8jF_0u|W zl4U#Q((5?4M!Z*G55k3Z>1!rWP-iQjEV)N%>wtIS@b>Z(hxe(|HZT^Sa*6sh`%Tz1 z(Nee*^b5}Dqb|<1llT3=ma`EzYP_Lr^Fw!r&t)t1V4kbt{8rB<8a;D)Y=iYPSQhn6Xan{D-iI!; znqrmrgMFY)_Q7(?Ssv}u_w=7|v>&{4B^)vDKwzw9APveMm}oHt&_KZdH3jR3{2cOU z!8T&Q+!I{34fZSYE=vj9j@VzX^<~`kqObJ9zxt#b{^rZK_4zWMxv1LmjKxL%1?pX= zXm3I9!Jf~sSbuBmCG!bo=Yzgn&zBACTOA9$wC$SjUT&~AVE@9+yL}|w>l1jFL*-cb zl>+Z_h(jJl?cN?6-UaSWGZveBP5NC9wCN&np9AeU^3p||pB>D9fISZb^nLwqhd-7~ zw6x!keFMM<7?kr3?a=8uw3vRweG@I8EXFME!w2G+XH9J~Xu1@Ok}kKbpfl%#YTv;{|`raeVYO zLH9SD!93e%fDHi{bsraFmY#M-Lcc|UJhBo zd%re-W-{`Ml3vuAG45XC_zdLO+o;FA6v&Y)%6%Qq!LhLi>i14l=jEEr%gwY*5 z9L8FdeF1&V0*v1UjF26XJd5Qr=JgRy%2~j@74X><*dTA7R$z++Y*AN)jdDkJVc##v z?lwrUxxkYP@eTc|fn()3+c93<4&Zs|3-Ix9EQ9BzH>!8B>AFw>WPTdPgSBZG&fWg0 z!+eUg)7Jq$?mcrn6EPm<(%>10E`#{sDU8WF;zs4%#IXDPt`lhv+*w;PTcp+AEe>Ps zRrI<40P|hHk11ah3;y$|YKl=(Y>vvRB>fp`1SPjwdbBpx#ryhR7#O+7b!LGd@K z=Z3l8Mt=j=CL8bD^g+JpwAOi2+1mu&$d@|JIakEe_WQ`2I{^1d=&qOYXVaPCxFXEc z08=1;?gW1lR*nI8Lb*AfEAeNPGM4|$pE}InG>`sx1vpJ#T@L4nOW=eK+wTHhL2vAH z+8eyz0A~+E2mol{FP3Bb^Co zx9gPjisQRa`Le#-=G%^ZW{eMtUys~snl9Pgb2&^h>N$YZF03gg_zsy@h~ve+KH@6j z@4=e#AkK$*VIML+3HIC(!r0X{)zXf6rso%j&lm4AUim5XI{I9cygrEe`3kI?k?B;*};(rO)kY^HpNZ9AgIPbLlukI%OZFdLDcimyR>z z3gfi4|99hj9pfZk3cyE!du*P64Tl9X^CjC`k00r-_^r?LYO~Lpabthwj7}fU^fqJv z?4_*RM^di7l)dAZM=Ec8&et9Osn7bI8~Y1CyEs(lF13s`~VYXjk!5^2NmNESpP(1+mbG<`Ip5R*svWW9u|+qvic zlD_9XGG3+I?T`S$<)YsHTEx931_IgiogA!5%FSLFS+l220jARalHqfY@qcJ ze`o!1s)A?dTPTlM4?gq8PPAAN2M{kgYcdc+NWSNX{uKQ;zx7*Fw3NcgSQE zuZ0Yl57qho*8_ih(vgECLyIA4dUL8J1bs6vNua-{HQo5P^O%p+mv8tx)*|3t9&wX$ zu9nXdkY9>+5AA_GlyzW#fV7#pI9QRT?7#56B#Z6VU}ZiNR>-$Q`u@|!QRDlVUr5)L zS$=l(L<`Q&KXL9rauxS5`0Vemsq%^RRrgEd2IGVqM{+NZX9pscJm)NbXb*6y`Kmvh zFRJcG{2SW}jF~+OxFx{NaWHOO&wFz*cwry$m@d>ayN;KJWFu`#A;Ax=;hgKF7juAi zC;P2@ajKGAhIydlQ4aQg74DZv&B15U2CHnwZO~T;*T2iTQ$FG0rMCNW`{&?1S$WdK$km54q27Xql+xK% z-RaO1!tDc}JvowgdV@Ui;iWuxW(##Rbe+8^Z2-1xA8ajU-!cV$L2+Hx*VOqj<~xMZ z*|tEwfb*dJ^1_Wuy?&JKejNK^)9|c7edfIezxy^R@43z=+hejdO&QP57iL(_UA7?1W8Wto*XPd;BI)mAH!YLXG9aB;XgSg<&tptDdB;9K5()vbj z<)Rhb-$dVHpU9npzBl9iw-tR?qHn%O%q`#){hfe040&}!qwLl7fDiiuIVX(Qda|Uj zkVTW;sVS69<}T<|R^%h?nzqF1eOw)1U>~J#{ong&0Y^(VW|!8bL1VX!`25a%Sw=K!08x|FS#LQecdxfxXXKrWX>rX zvBB5w`t^oa6N0*Pg2mPH! zdggV8TWsQW8%8*d~$>9Vsb-SuE<5q z?Gs%y;XD0I+|r$d_+E|2q^yhqZ;N;(UJ~{3l2pH=GIAhZGSnxaT)bqgk54F_*L85j zE49bOEt%K@&4e!Joo4UHUVVMpNC_W9ec8CudG7Z{LUxM!8Q@pM`L7n(k0#F&p7<~P zxem~heCh`ubzxi~ChUO~PZ(pH)LetbY$a8Jefz2NU|l!abYoV-KiR^BCMoj{$u z2}AsCe_7}6=9+BanfDOx9>_fU7-U0M0`LL(^dfRLQMV$EPdKKg5+^kN z;_$w`)t|c@<2(nu=QiYc5W7Wg_wR0mZW0Yy6N_`26FdWGi*cNplDQ>Lis_MOY^#Zr zCiFzkc(&$AgSqa&-$ibTA*4tChsXs@aNde%7b88_9mah!*qgK(Wv3r^3A&7tIp+{_ ze*3nHnkK+CptK+D@nU>uA@@JIh@7f~8~l?TDspRs#HezV>t0}}Iu6_tpWbIgLtBm* z$XV;yyB#uGpTBK8)j1$1yi>CoxD5kt`TjWYcLKQN{H4q|bZ7gL5X@t&x5eRW9cyk$ zXsPi`YN@H0riHv1VsaukPtKGPV)VOGn3=TgLH*-^@5E?p1L7_en6?0>lYr@j2FrW5 zuiRTAg%83~3R+zYp07vSMS#TvS;F^hOP3|1)~;MbdGb=ido_zEy;qYhO%K@}66!3H z!gIcqLd=M_8%?-b=W+Di4Hznn;`$2oeH?vvq3?>@3(>D>u;0cSUB0OOZUGMXo^9wj zkk=Nj2}Qq@b1xt_IOcRtoiruH6B2^+;AZnl^hut}M4ycHca3p0bYq|1fjQ545Vtm{ z*p4}4$Go#+&e<{F>{1XuBk&oCPcS}mee%2hTvs&Y{d&+6a?v!SV%(2FU!Dcti8lF; zYXb74n7Jm*M_dEvPu@IHoH$bCW*Q?~Fb{h(@tbq6=Nj19lkC+6YrSg<%zbm>B)ND2 zVnlGpe_0;n^iJfI{U6A8wzWS;E= zOGPrurB4hlPR4hF5H0CAOSl}ry{Lz{US}I*M+xe?E*3QCG6?%>w^B|4)?sU1e7`^U zLBw4X=6=k99%BedDI&`!+t=_}PKFrG=Q47EnBf2BnnwC?Y~2`RHpW*e`sOUcxF93O z(ATpF_)Eqb(`~dNKNDmo_#Q{*b0vFLo}#x`}Zfctzxzpl^g?t*k@53D-~3r4ae@$?G!K%Ql}sHw)v)Lrjhl zv?&4%D=}u?r#1y`Zw4IYlB~eteNp6wYH-Mo<7+be{IiPy4|S*&n0MO^SlcT_F2|jN za|wB4!4sGt9CsidyyyJ6#KUpmp&WSd0uQ9~LG({Lmw?U(&iZp18=eO_OSo17t}Pl| z*G_sD^H_{=X>cXiiQKS3xK6C`p7zfso=T)p1um|gwGWHj@{#tmUxf(dA!w-~eRzho zCuNEC)HSzSxt;~iF;OynjsXXQbBuVVoS6b0VHM_S5$0AXa8(;7-@FQNa=ba%<8HgA zbY2PIS7iJ$#0+?lPZ@KXv~sR3>TAD7s0USE4Z4Y3q+aW`CEm8D4&QIzpL1XRf2)_;RTT!mmlFMyBHTr_2dbDRn5e!djiVhan+ z;XIBL6MDKSCwGB2p%-MECX6vdu8uVdC*@@Fq(e0h%yC(=R(7FZ^2S8;MO;_le+cGw zo)lC}x!$mR0Qbt=t(9eZV;qK$wt*o&zo^wZ+STEqN&-0*drZKul{1|k_ z9?$_p4Ayj0PaAL)fwLs-D8qcB$g|){gKXPsh$wo%5bY%0PXpEj@b@`gFTi>G^T-Q) zq0{a@als$c`KZ023G2imgRv*+S%2;^jFt2`Y6xe{~;>s}r7 zs%!7MYGx()CLD8KZ>tA%sE6%B+MUFBI=1<9v!lc;+|N-wKU=c}egs~Hd}6*PNty;CMeBUh}4I^}7=$a(uV=Iz^%f%}YdaZ-}V zb(ffy(YDV*|CPwi++&<)%Z9wOW8IFsAQ!uSAaZfnD0AkIzDxRG?EuyS$Y#*`AH| zyU{+!ISXZ>JLGH!Z3#R3sVEh>jhJVgd-E{&3TN5p#EHzZ<8Ps9~HTK5ZB7}SvmVEML564 z`NIN3q?x$kyv_n`CXqh-;*J;vGHF;9^kxu3S;J4V290730>R_ zJ)HF4t?A;_zbhQJ)D2i~6kBnb$lZ)`h&52k(bfcS^l~mcU~kTs)N+lts^!dKYB>*b z$Fr?0l*#;#*n@nuwu^v|ZRRggd6@jZIKgB??193w3a_Hh&JBL0@Az9(-bCM{$&V=s zVK(wM`>q3DuP9T?wJp}lg^AHq5};0MWp<-XW~u*r_Sv{d)tSQ*LT%Ku*%xtl{%d~> z^~^qeTT$<<@@-$+jr{GL6Ebi}UL_87e*G@Sk%h9H58Wv5fIRc!yHtqlO_Xg|+e#mT z-4D6e1zN;sV11JjYxo`5a_EaXJ#~Bee#i#MItS<|ug%}w!zo7xuRfziAJi1HCgbPoY=z9)u6a{{Yr{$gE_@EIHw$nsXb(=t%GZbX({c z-Zf1aCv}Vkb5RE49e{s{`fDb}%ik4~4?(8P)5fgI6dAezX@j+H^gK;woioTeTT%9| z5%xK3+l97WXgdyV?@ELG)#McCI^`gBQdJH@ABCJMfUe=va@`&QE_nqdg?OpJL9|tfFoBD?@9QTsX zXj-nX4Jw|MwE^dSO^}Vm57&$egEfSBnkP*#bG_&WZ)Snt+!`H{z6Y72;VK(ra!zih zOtlq(=knpNBCg~V75~}5Kj%jmaAi%fS8qkxbB2(dbnsdxODorF9_xHG_Gggso# zy)C!4EeXRo!g1!nzfKez0>*-*ftT@&tc)W(M~3}dZb-&B-m8JlZtgUO*;=7L7$FZ& z87J5{@7s-`@F%D`3H6!_DAQ*Q_xv4kO(&s)oG^~JQ3v_Z5aHx{RE~8w4zx?`_UDq8 z@GnM^x830TgXdK~=Wo)Fa`O*(#%?JEKUkh z{F#&^_C!^V#6gbOAV;!6qg>1n=z62ME^@vmqYo!&RkyBZO%LqY67n?c*dox0_7ZJc zFXSU-2x;np4nTX5Yjd_HKXL$L1z_X)tjZ5JI7o*PUNOw ztt`TNy$X7l9k{MUdn?gi9rUj-tW#WjxK=6h3hi;7qHor)$e-(J_Rq?q?qyQji@!=; zx46#m9?|chJ=%Jt4Q;3wK%3*nDYkI!sqek%&)858B4KC~$8<8D)Jm!$yX_L%yJ1 zM7_n~NKR!gCh7z(z_SJL5Du=FCxG8BJTK>Stse0}*!g~?kYJ|0|F~^C;-u4;$xu0nZ}P2cO*<@a%rA+@gSI5!$nx0-nv(p3M(< zc0b@|d-L#&^8bK6$s%Q9EI7Zulq6g8QZ~c;+Xvr&%D!6$2HGm>9~Fs?VZJ5 zSr2S2mvLNR1SM)|m+u*e+hLft~lrvKP+wHT9r7wk^J);K2fJnc{V z1?XSwfsK6XY;aoJg3Jq>!E+~I6M4Fm8w#Z>_d67 z$nA-K@<{W=8;=&Pe8U&?_sXR$xgYs<8ypQO)7pI5>sy{_Is1$1CvZRY=6PaF_CUtb zTh|9Y(>&5%eXlgSQmyS)w3>c$Nd7MJ$+b*=x6yFv>9)Vbv_s&s!EJO zJ zEQXQhw;p=+nRA9<=X!iU=)d>qxAFaJeBbkCWbB*xejDYkG6p#x-WD1A3I5;xxca>8 z(a6|UZ$!r4iSpyMx_A9Tt$Q`TS2)#jr5n|Ht6L&tzXooeI2ajQa_@-V4)`dE6RwHG z&uN^c**QM+<^}ky7$14F5ue2I<8NMY_4u3Z<9FV?eEhDP8z%(ayaS*2A-}x9#nQRs z!}IZ(A8)wl?s(%pk~G>m!(eVn46>)q7$aUzlI>{=#Whza3VYg*&~8#FaRXdfL=|zf z)L=Bv-}GF#-5|^hHth<}0A3I`a?cFVCm~+Crx&xNi6V3Yu>1X#q}$Z*zAoEsX>`bqeI9om?LcatA_`I#VmZo=pB zj8U;4psh{#e2$Ohkuk9n>Xep_iG2s_$&XOxK>3*1-{PBPzCiyo@%zvCy>auH*kkzq z)7CMu{{uK4e``$aLW3n||E4jq_0kCQmnNfmV^9#j4d%okVg6DwC1bhF)XC8dr$j|J&2WwQl6pr_yX6&Wp-ZR3UMjwLy4E9uR zp^0TelrrUj4gRy$9(>CK_u>xGdymR5kBQBG4WA6z=D{BA4%BhMcan#BoQ%7GyNu=I z$_?Y3jwIX>i91IgK#VNrx|4T76d)$$Ki&s1H_5UbvM8OoOCWdV$0k`iu>SG)3do>@ zw9=)icsCPrhx&25_RerUy9d{cJu z8GQq6zavJKrSW?G>(n;KYyT6!m1N0>JX(%VjP`6ko)M-9?f-^3fFJKiY5xz+NU|_C zo$V(p?IC7rm9m;LobsFND%Y}wYvYzpl7h_7uZdgsKVpRW z&-lJcj5HsM3AcWM-^Y+E=?_w{`O^pEmd%8${yc7!_5O8n%VI@@`9VB;R*E%Wyv1UD z8^7-`OfcVp?_|R`^VdHgZI#jXCdlEBhH~Q2Y5pVm}d9FP{Ac`w++Q zy&Y#;K2M6Y9;*`#kaxKSBE*~}OlF%1Gs72UHsODW!EA=l9QJ{1&N76WyD`7529p{4 zb4r;e{O3FR8j;68*MfOG1GFbUt^TsrGCNev!uUO2d7AdUqanNj^WGS9jG@QxGhdxTZDM@+_)*Ccm?EF$-t;$7tVcW zPZG8%iH-1im~GwbQq~|}inBzr^zK60zd`R=HA##)dB>_X_rS(Ij`@yQ^~g|@xRrC? zyYAXGw2$JZi{b^)XK4qR@ZPKS$^MG~%Sq+GtiWI;o=5Q6RGSQW8V-Fyo@aluRWcQC zz&`E^z{?@THit_~*1k93zdl@wuPR4>??__palpg2i%|bK>H}7DKI*PS8{1Kz`;){g zaSYye7D|@22T%KR%dtOr2s$-wokom33UlFRT39 z$|2`QVl3qt6Lqyre0Q1c^our79#glXJdRJ0ZU63$o}H%>EfQq%$7qv0(eYKHB?13G z)c^OgU)e+5@UZr51IqCKyZZl=EEBEBz9%6Yc}A%lvTeRFwrqz_A_2J1Lq6&G{6-Aa zPJEjRR-|SIA#M@>O$G3g;u(32u~>X>Myyq)tiCrFSk?EiG4=N{-phEdpFit_jO6ce zzy-(Qy-1kUT&=q_I1PS2!`-3U|M|*)kG7!%tu0WYvWdz;W6?^NUdrt9MTqmD7S zKNfLc*`bKP#T|T!V^w@-;yWFEx%ds9Gs5PsEL^d7K6EL_+1|>+*?T`6gF29}-$Ea3 za|Pxs+nSC4*x&WI(SD`D;B*2WJL1d;dk5^jeC@XZ_TgguUWC1+TF{U0SHi|SfHEub zyi|L}|BEEqc{84^!gnKR$+DZFr|iagx%ql|?K%GsD-XL^DGwcHH_p@@fQ(Yw*Pin~ z%d>5Hw7Q0D4ntX-?aM%X&V2Oy06s40{N#^{*FL@MENLF#*#NtbwCD^1u2F7hXQE{_ zaKU#5?H$XM7p_R15B}xza@_O26VJFm$ht<-33U$~OSE9`Myb02ehCWM(L;Ym1?WE?ywA2P3X@a$jQ&N|->KDSpQmy5jrT?>X9YG6i<7! z{|})&&jl*;5@l(N@^|?si5A8$^LIzU^9=#dy?9RFH_Pxjb)pybe(+yC;~3%bXwSUL zGXr$R2>AYF&vV-I4&^y@$OzAN?V0#fqUA}RUq{<6tsd*VrR!J9Gv;iJ@Z6>8PTm8F z7RpxEX}+sXiSOP2G2uX5w;$&su0d=#bfY}2kHlua!}&tU$KUGuSQDSAu-t@pIk&x# z7d(f-y!*DNcsDNZn-Rs(S3KT`@)`%q*umpAvzUWeu3qq7ZU}TI+epNAVJ??~|KL;c z)q){Ve3=>Bcs73JBr_Ea;UL3Y^nXY=5D3&Jz(qt${pm2!*isV&gY(sAxv=+Ex5 zr>5}PSkar0^-n3c3hg16Oa$lx82Wqu8T?95>vmrb?2tmqR+)#K6aun|aJ7tpZ4Q~q zdW!(>J6NaT|E$9Lr{o(&EO#W|Wk@pOXRx2VQH_ZX>0MuKhVB91WmS3x_CnF#O6>i> zkJjUb96fat^-I zo7AnLDdaTBFka<+6EdB$wgR}JzQTHZz5q0AEPODP zcU~R<+tT~FeW*jY>5C$cwqZORFMk&(=cq1^ z@4py)W1kX#eNesQCwbz0&=2R*k*9^@U!K%v*$G{Wa>W{kyWDYB0J%rg+6Vj}@Qen_ zZ2AE6cY&IZbOT^Gh57FPiAbaE*7Xy6n)hydS_Sz05I)2`)M-$^F>My0nA4u>gJ0$HBTGjzRzm8+Q_qPBhxDOIL0A%1-fwh--3&APkVy_ zw068;cb7<}o(-`5q8&x4h2WV&;P;&n#J|ILcL??xak1*bPnYmq=%r_E@=$jt=2-}0 zkMiD?ig`w%5NkW1vz?vrtB2y<_Vsv&-vwC5c~>0mTiRzo#at;tK7%7TzZ5@RT>n%2 z{%M$a?S9}VAMYBOyOi{b2acc%2ZL9|Rbmc#0=#c40sRXSMcR+C-=?-1Wm|(YlWoyr z&O>NJ$;X7gj^UYiBWQ;BkkgOa)7WT-~2J&L+-;SjkFb9Bj&K(GuWqY z94%hU()xKBdsWmW{~Dsc=NacM=(pA)<`h7#B#%R$F52U$`?5BcL;e2i`K}<~-I*AB z|8c&D?+#<#gR_S9TajOV`}~FAOT<8fo?gVybs=UEu@jzxkz$VmH@+1-hpJ<$2h5G3 zV$O1m3A*MU*!We2I0xMp1RMZn(k)jTt7LAQ&NFEGcz+WxUfAo(c%Q)d>=;|?Y^k^u zJfh=KnQwM;)dt|{3Dk=>>En;HE#~;;;dtE<+-A}H*%Z)^YrKjltQmU>z~|%*$jMuQ zn}$yTFZ+uOwa=*y6?@R*P$^iT4ezXDIbxxi_hdA3&e3N%!!+I9s9`tvExfhQ4 z0l6CvK5hjc$6FASB!%>}L6(ievy=P9t*utro*383hoxIv9k6Xt{^VWKt(@m4M`+LZ z{%7J=u8a4sfA<-`ag;JgcA)INpRGIEGs4_bBu%Z`2w6>d1;!ExYbIa??q^fhxHWjm z|G1kk?Rot7g7yyZgBLy-E9j~7OM%7@q&4}0G=&_jFFWf8U6Eq|@AC(O=M><%_l$oJ zWtRI)%%IFF8=>;DE}z7OV1+N*;3GPMoY|#ct98jg2|=nXqioa5+fVstG2f9smnDNb z*CH{V+Wz1<<%Tu5^AGgGevm$&i!mPJpe;=0%LJ+IKGHoHbqewB^e%M{M|SwHXS;!T zy7)MJZfAqRcdC5&De!Zb)?U6@Ywt{}GIvj*Z0Q-m54_2r`Cnt7;L|6` z$LKs4UIw+f7MO(UTDmSR}{S5Xo zc6Rifj{jrqKAy)+KQmXX!EZf5zP^ZECf)$e&mfXA_6SosKuIXM}fu z2l-B_`X2bMwo7dr>-=jLw8{3{x?1n!_r@m@6}@}mD~Xm4z_J5#@jUzv%6%+=@$@Z5 zMJF?1eY479Pa$#$V86X*1?;jE*r_F$s}g>PfL8Qh z4(Q8;fGq_4k&ir8={WD9$bYQmq%q|P=S(us%4_$zPu@?z9T^;sfdAN7E^^*z9U zpCT_TJ@){X<=~t9ATP`#DKDle@*-U0l~U}#@O&KcSN28geHXC~kau~vOe*-c8ErYh ztHi}F#HnDf(8jftd;;I&9_la2SZ7U;jcmhntaS-;h`v-LMN}n@F0WZWy0T_6`A?HW zGWvxcX5%`?HpE5d92}Z=${jO+LBHFE@4g0|lXp|^kb4+2X9fLi&^@R>P(P#&g1v|y zn=JY}FjuB%`a>ChJD^)o?*qMYM~irQCv+H75Og}^ZXtg`{~i3zHdHxYuE}xq2f9qv zQOXWCWrS5a79H;!(Wd&ir8$Jr+8*rupcM8n(qwySb0_=$t z6XVIH9{NU;VROmAOWfb2V%i6=rK)VGn__~@N10uCMm>o>ZT>gFU&}h7sIvuY(!NRX z1;cN%67}|B4`>SNLDpc*Qcv9kd-Vq7Mpz6TI1h1``M?41v3`$h8S0gz|IMhUuT!im z!Nw^vnVm(*JHT~n0qm@0=$|-do3yvW(8fOSJb82j>L;S!0%5KrteGg=VGQ-~KjTAa z11I4<{C~L{AmiveWE&<&2W%Z_2KJHR*BYv`5&g}<{|wlt2O;lRpY$|2I#bjCA9e2@ zA60ek|L>UrGLvu_Ab_Bdo0eqo0&-0Y%_Kq11qC5qTF+?$=rMp+Qj2$+grEe1XcTJ? z_Iwk>*32NawsI=H91`dS#2cdaRDnzq)Jekqnn7WH@6X<|AyDi&=Xd`2zP^9VYtLSL zU7z)==eC~pte%cBPoejC9L}2AhI9LJr!gVk;i}2Q7L@DoY=;KmFHhWo=4NjiYou`A z+)1tI>vOTwiB3qD!r#NJ(}X>B>W=kk^hbN~DqFp~*$b`(+>beklWWDpa+smWFL9`C>CZw%Nsw6{-ZIPS&1x1O|Dd4Jo}cWhnu)Upz6oC~o3slM6F5%`t3 z0NU_s|6ya5)&%_jkJ8XLR*dQEQ<{Gu^cS3*_Hf3V?Jh6pGTfa_tVikC)!)boINVk& ztu%BgU`9;TbLWgtKl;Z8=~wJG1rKEZ+XtQ#Kib*%!!<|PZwlTt{KF#+&Ve-)!wo!*t*#f7 zh%dVhTKX%Gx5k0p;t;sP+UuOUx%kbgoEg=gO*(MOb-1=0jIRf4Z|~S8tLqrU62?HE z4?jWQqrg-)rM<3x!6y^_lm5ckqKrrD|1XRu%2?d2-@h=H&l$&G9@~N(9M?IUwfmRH zTrcJraqFD?-(>XaM7}DT?&!JQc*dx`>#=?>{>x+Uy?7AYdz-5#cZl@r3}MT-f<7e~ z&ciP;MztMyIQdO%&P6laJ687x9%G!|nyl2g!*jtA&D{}vHE#aVyDxg5l|4Ln=a@6s z?aWF~I;?b!(Mf&EcWvMP31e0p{bx=XF>B1XjRBY7&vA47j$s3^=Z4U~9nStWhoB!f zLyuR$&onQZi!Dc^MXQLhr?tE0XU*PJ)5ee(lz!fZ?V-fT`nVA}>m)QoI+JDV;2y^~ z>F?Me9L7YIb-dhsR;SW8#eC9tbOPjBE0(x<*H)}tGPnrGO$ zn(Dotz8`{~6!IjdlVVVB4BhWW&*ZE@l5^vq*l$)$C_nna{p=wChr`;Z{ha*jXX_p8 z5rOMdi9PD6m;B}F%wf$+Ci6M#XJ#OWjoReW z>)~N!oUO=LNq1R($?Xr;&0O|i{SPv(*g9+(dgWR9Z%162*Wwg%i&nX@{L5FwfmE1Qy=OcxA_eB=;;2i|4{sn zZtYe6xY@Lwyh(e>{VdywxS!Hx7X(As1*~GniL5v4Hgdt$%Cwrpznt%Hg+{ zd0#}`;|E#&NU3RFC_AELO!dD`W5RyYoZ~2KLQhutmglTIoB93%-=V`3Zi}^P!?C5B zKE~=;NuCN|m*u*ob2D{>=RZ6tX$(A?|IjPq2v~NIQs(!?c%84ruG7aDcN{+}jr;il z*y9Gq;Iz^j`@?EC&Y1Wd?P`8TP!1pOiI2qIWs6!weWSmm|9y=K*Vr&&o=x4d#L~^~ zrXB-(f$C_c4rF>$cdRBx_xzvx16jzElH294tv)xg7Sh4*?tMmvH~UsQ^~W{}_F(AWldYbUr19$Mx8QG_NU-o}052 z%7lQ6>I5bhj{fJlI*01J%b1{j027`w5`f8|pk$;LU{Gn&!-2so7^88@(h2RpwN##K zCysiMVJ9{hILa7*kqtMy&c*cq474b8Qu_bE;1Tq-3jRaaB3qeDt$D!(z0b4_iQelS zn&`XOS`r;QvsSy0sp5z3|Y^N>8$ygG#FR+Sl*h~Bo_8@m`WnL5~T;sg(ZUW() zzEy+MUFsWm4%oKft>}5S-FFV{#!L6Q3O$wo#ll(OYUyB>4Oe+=8?N-TQsgYwOHk`i zZ7V*^Z({aW{eK=`EO;gx)ta)?OnU@-19n^-!CCxo8MNBANjOu`!@xcE z=V~Xw6Va{uFQDx@c+WlXo(A-Yx8Xf?E$}M*1B5GAHSL~(%x>A-i3eccqg4)#+03{y zn76Er&_({cnG=n{oD=Xz!v*7W(r7H^_$}4y* z!S)=2C;f@M7xvLna;}eumct{+citF^&V3K-JNp0^p*_!}K+i;bn#ohqji+nu44bPjt&(#?*tN_2$oaBj*w*Qb@VWyJOYe#_jUPi)XUE5Fw0S^E~S zQ5-1A4d%TK*tp7U&&<4HF@=q;;hMQv_2Crj06J<-)*bK(j#2G4^pHCCap+ADP_K)rLCfWn< z^l8ktj%wT2{tmzM&&}Z^>hqsDpSPw=IS2RA`i`8hIsOfO^JC{vY_{?@?y+zuJL_D1 zmy^E{+y6o6z4~9jJBD95=ki=io_hHC=j1t-7PHyo16R*jMOW5jlNTMizv}9;Y}$>` zkM3}Y=bQMsJ$~8q8*P|<618NY3TypEm$BBc{S)WH?33zJJ57E|raGzb*!wlc2f(f5 zK`{4a|RX`XK!UJNZn z@9D4mjkI4FCN}#l_NYNO5x?*WdV@vgeE$4f6f*Z=kEKXc2TxLL=Eenw>X zQmc;tq)c~NM>389ze6s5gFSX%<#K;OwmkU;=2cX#PJ%ugjNMZ@f0F$73ZM(4urn@) zHk4xrccFKWdIJ5Bb8&0YEwix4ArBTU!p5-$`$Q`8r0UyD9kThQljbgMos@))a5-r= z0=EkBoeS2_W12(s;$&c}xQNP=Y~$V47T(`a9<43$bsHuZ%2I343)Qal!@-8`iywr{ zH8{8vc}(@KqTUvC`*GSQO%uElgU69y{=lEv;Pg%`MSt~RL(5{1U^B8;cO5n#8>g+W1x7cY)z=F4rspQ}^BSY;4Eav<17yO)1`Qvws_P zIk`*R$(=|p{Mvf3H_(%P*iDHyOr=!a#^rKzvVo#bzjvf{_B2uwg1=e?e^+kkh9{11_4pcz6CFY>-9=@6F0kf~eO=o}YIXi3p?AfS}IZvf{v$4gme!^N0XS%Hc z;lJdUMs%3j7m;1GpKzhb}{!i!S@G% zX^G?#{1-G9^|h&e4|>h@mR`e}(>U^%az{6Mit0$`iG63{Hh9M%!&x&VQvU3}v-g&K zym89$X5#Y_%QfVxm=bbTO_uzIePC4*dWX)^$i4?$e~`r1n0zDdr|qN2CKs+voblR#Mh3W6!}E`q~}t8kYfmv1MM#yF-wRN@D!Ymb+v_kWSOR9-FpE zUQtYAXua;GsFnUy!M;$|PYu)Vp+49+Cm$yD_X_%`{ANGFyN@X^f0J}{niH<X_c0I}XoHje1Wg*IjMrCJ)ugPX#*1aJ0bhOA#OuVFY@OG3Va}NAt z4S1`2bTw}|k61Y70LS#crGIv8 zXW^8_HsQlP3xgN2ZA$h(iXK)EKF~h4&A~zGSgYt?iP=BvuEeY9Ux@xSU{AQ8{Tua9 zI+5&{&|_q7$;lVDb?SQqYf^S?^R8m%^wixw;aR=lUDmzD>UTpwi{FXPeuBS_-S